├── Source ├── Delve │ ├── AssemblyInfo.cs │ ├── RuntimeException.cs │ ├── Models │ │ ├── Validation │ │ │ ├── ValidationType.cs │ │ │ ├── InvalidValidationBuilderException.cs │ │ │ ├── IValidationRule.cs │ │ │ ├── IQueryValidator.cs │ │ │ ├── ValidationRule.cs │ │ │ ├── ValidationRules.cs │ │ │ └── AbstractQueryValidator.cs │ │ ├── InvalidQueryException.cs │ │ ├── Config │ │ │ ├── ISelectConfiguration.cs │ │ │ ├── ISortConfiguration.cs │ │ │ ├── IQueryConfiguration.cs │ │ │ ├── SelectConfiguration.cs │ │ │ ├── SortConfiguration.cs │ │ │ └── QueryConfiguration.cs │ │ ├── IPagedResult.cs │ │ ├── Expressions │ │ │ ├── QueryOperator.cs │ │ │ ├── ExpandExpression.cs │ │ │ ├── IExpression.cs │ │ │ ├── SelectExpression.cs │ │ │ ├── OrderByExpression.cs │ │ │ ├── FilterExpression.cs │ │ │ ├── ExpressionFactory.cs │ │ │ ├── BaseExpression.cs │ │ │ ├── IExpressionExtensions.cs │ │ │ └── QuerySanitizer.cs │ │ ├── IInternalResourceParameter.cs │ │ ├── IResourceParameter.cs │ │ ├── DelveOptions.cs │ │ ├── PagedResult.cs │ │ └── ResourceParameter.cs │ ├── Extensions │ │ ├── IEnumerableExtensions.cs │ │ └── IQueryableExtensions.cs │ └── Delve.csproj └── Delve.AspNetCore │ ├── ModelStateValidator.cs │ ├── PercentEncodeReplace.cs │ ├── Delve.AspNetCore.csproj │ ├── IMvcBuilderExtensions.cs │ ├── ResourceUriHelper.cs │ └── ResourceParamModelBinder.cs ├── Demo └── Delve.Demo │ ├── Persistence │ ├── IUserRepository.cs │ ├── IUnitOfWork.cs │ ├── UserManagerContext.cs │ ├── UserRepository.cs │ ├── UnitOfWork.cs │ ├── IRepository.cs │ └── Repository.cs │ ├── Dto │ ├── UserRoleDto.cs │ └── UserDto.cs │ ├── Models │ ├── Role.cs │ ├── UserRole.cs │ └── User.cs │ ├── MappingProfile.cs │ ├── Program.cs │ ├── Delve.Demo.csproj │ ├── Controller │ └── UserController.cs │ ├── Migrations │ ├── UserManagerContextModelSnapshot.cs │ ├── 20180212171542_InitialCreate.Designer.cs │ └── 20180212171542_InitialCreate.cs │ └── Startup.cs ├── Tests └── Delve.Tests │ ├── Models │ ├── Role.cs │ ├── UserRole.cs │ ├── User.cs │ └── Repository.cs │ ├── Delve.Tests.csproj │ ├── PagedResultTests.cs │ ├── TestValidator.cs │ └── ValidationBuilderTests.cs ├── LICENSE ├── Delve.sln ├── .gitignore └── README.md /Source/Delve/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Delve.AspNetCore")] 4 | [assembly: InternalsVisibleTo("Delve.Tests")] -------------------------------------------------------------------------------- /Demo/Delve.Demo/Persistence/IUserRepository.cs: -------------------------------------------------------------------------------- 1 | using Delve.Demo.Models; 2 | 3 | namespace Delve.Demo.Persistence 4 | { 5 | public interface IUserRepository : IRepository 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Source/Delve/RuntimeException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Delve 4 | { 5 | public class RuntimeException : Exception 6 | { 7 | public RuntimeException(string message) : base(message) { } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Source/Delve/Models/Validation/ValidationType.cs: -------------------------------------------------------------------------------- 1 | namespace Delve.Models.Validation 2 | { 3 | public enum ValidationType 4 | { 5 | Select, 6 | OrderBy, 7 | Filter, 8 | Expand 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Demo/Delve.Demo/Dto/UserRoleDto.cs: -------------------------------------------------------------------------------- 1 | namespace Delve.Demo.Dto 2 | { 3 | public class UserRoleDto 4 | { 5 | public int Id { get; set; } 6 | public int UserId { get; set; } 7 | public int RoleId { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Source/Delve/Models/InvalidQueryException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Delve.Models 4 | { 5 | public class InvalidQueryException : Exception 6 | { 7 | public InvalidQueryException(string message) : base(message){} 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Source/Delve/Models/Config/ISelectConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Delve.Models 6 | { 7 | internal interface ISelectConfiguration 8 | { 9 | Func GetPropertyMapping(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Source/Delve/Models/Validation/InvalidValidationBuilderException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Delve.Models.Validation 4 | { 5 | public class InvalidValidationBuilderException : Exception 6 | { 7 | public InvalidValidationBuilderException(string message) : base(message) { } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Source/Delve/Models/Config/ISortConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace Delve.Models 4 | { 5 | public interface ISortConfiguration 6 | { 7 | IOrderedQueryable ApplySort(IOrderedQueryable source); 8 | IOrderedQueryable ApplySort(IQueryable source); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Demo/Delve.Demo/Persistence/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Delve.Demo.Persistence 5 | { 6 | public interface IUnitOfWork : IDisposable 7 | { 8 | IUserRepository Users { get; } 9 | 10 | void SaveChanges(); 11 | 12 | Task SaveChangesAsync(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Demo/Delve.Demo/Models/Role.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Delve.Demo.Models 4 | { 5 | public class Role 6 | { 7 | public int Id { get; set; } 8 | public string Name { get; set; } 9 | public string Description { get; set; } 10 | 11 | public IEnumerable UserRoles { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Source/Delve/Models/Validation/IValidationRule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Delve.Models.Expressions; 4 | 5 | namespace Delve.Models.Validation 6 | { 7 | internal interface IValidationRule 8 | { 9 | ValidationType ValidationType { get; } 10 | IValidationRule ValidateExpression(IExpression exp); 11 | Type ResultType { get; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Tests/Delve.Tests/Models/Role.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Delve.Tests.Models 4 | { 5 | internal class Role 6 | { 7 | public int Id { get; set; } 8 | public string Name { get; set; } 9 | public string Description { get; set; } 10 | 11 | public IEnumerable UserRoles { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Demo/Delve.Demo/MappingProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Delve.Demo.Dto; 3 | using Delve.Demo.Models; 4 | 5 | namespace Delve.Demo 6 | { 7 | public class MappingProfile : Profile 8 | { 9 | public MappingProfile() 10 | { 11 | CreateMap(); 12 | CreateMap(); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Source/Delve/Models/IPagedResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Delve.Models 4 | { 5 | public interface IPagedResult : IList 6 | { 7 | int PageNumber { get; } 8 | int PageSize { get; } 9 | 10 | int TotalPages { get; } 11 | int TotalCount { get; } 12 | 13 | bool HasPrevious { get; } 14 | bool HasNext { get; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Demo/Delve.Demo/Dto/UserDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Delve.Demo.Dto 5 | { 6 | public class UserDto 7 | { 8 | public int Id { get; set; } 9 | public string FirstName { get; set; } 10 | public string LastName { get; set; } 11 | public DateTime DateOfBirth { get; set; } 12 | 13 | public IEnumerable UserRoles { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/Delve.Tests/Models/UserRole.cs: -------------------------------------------------------------------------------- 1 | namespace Delve.Tests.Models 2 | { 3 | internal class UserRole 4 | { 5 | public int Id { get; set; } 6 | public int UserId { get; set; } 7 | public int RoleId { get; set; } 8 | 9 | public UserRole(int id, int userId, int roleId) 10 | { 11 | Id = id; 12 | UserId = userId; 13 | RoleId = roleId; 14 | } 15 | 16 | public Role Role { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Demo/Delve.Demo/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Hosting; 3 | 4 | namespace Delve.Demo 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | BuildWebHost(args).Run(); 11 | } 12 | 13 | public static IWebHost BuildWebHost(string[] args) => 14 | WebHost.CreateDefaultBuilder(args) 15 | .UseStartup() 16 | .Build(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Source/Delve/Models/Expressions/QueryOperator.cs: -------------------------------------------------------------------------------- 1 | namespace Delve.Models.Expressions 2 | { 3 | internal enum QueryOperator 4 | { 5 | Equal, 6 | EqualInsensitive, 7 | NotEqual, 8 | NotEqualInsensitive, 9 | GreaterThan, 10 | LessThan, 11 | GreaterThanOrEqual, 12 | LessThanOrEqual, 13 | Contains, 14 | ContainsInsensitive, 15 | StartsWith, 16 | StartsWithInsensitive, 17 | EndsWith, 18 | EndsWithInsensitive 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Source/Delve/Models/IInternalResourceParameter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Delve.Models 6 | { 7 | internal interface IInternalResourceParameter 8 | { 9 | IQueryable ApplyFilters(IQueryable source); 10 | IQueryable ApplyOrderBy(IQueryable source); 11 | IQueryable ApplyExpand(IQueryable source, Func, string, IQueryable> include); 12 | IList ApplySelect(IEnumerable source); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Demo/Delve.Demo/Models/UserRole.cs: -------------------------------------------------------------------------------- 1 | namespace Delve.Demo.Models 2 | { 3 | public class UserRole 4 | { 5 | public int Id { get; set; } 6 | public int UserId { get; set; } 7 | public int RoleId { get; set; } 8 | 9 | public Role Role { get; set; } 10 | public User User { get; set; } 11 | 12 | public UserRole() { } 13 | 14 | public UserRole(int userId, int roleId) 15 | { 16 | UserId = userId; 17 | RoleId = roleId; 18 | } 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Source/Delve/Models/IResourceParameter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | using Delve.Models.Validation; 4 | 5 | namespace Delve.Models 6 | { 7 | public interface IResourceParameter 8 | { 9 | int PageNumber { get; set; } 10 | int PageSize { get; set; } 11 | 12 | void ApplyParameters(IQueryValidator validator, string filter, string orderBy, string select, string expand); 13 | 14 | Dictionary GetPageHeader(); 15 | } 16 | 17 | public interface IResourceParameter : IResourceParameter { } 18 | } 19 | -------------------------------------------------------------------------------- /Source/Delve/Models/Validation/IQueryValidator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Delve.Models.Expressions; 4 | 5 | namespace Delve.Models.Validation 6 | { 7 | internal interface IInternalQueryValidator : IQueryValidator 8 | { 9 | IValidationRule ValidateExpression(IExpression expression, ValidationType type); 10 | Type GetResultType(string key, ValidationType type); 11 | IQueryConfiguration GetConfiguration(); 12 | } 13 | 14 | public interface IQueryValidator { } 15 | 16 | public interface IQueryValidator : IQueryValidator { } 17 | } 18 | -------------------------------------------------------------------------------- /Source/Delve/Extensions/IEnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Delve.Models 5 | { 6 | public static class IEnumerableExtensions 7 | { 8 | public static IEnumerable ShapeData(this IEnumerable source, IResourceParameter param) 9 | { 10 | if (source == null) { throw new ArgumentNullException($"{nameof(source)}"); } 11 | 12 | var internalParam = (IInternalResourceParameter)param; 13 | return internalParam.ApplySelect(source); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Demo/Delve.Demo/Persistence/UserManagerContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | using Delve.Demo.Models; 4 | 5 | namespace Delve.Demo.Persistence 6 | { 7 | public class UserManagerContext : DbContext 8 | { 9 | public UserManagerContext(DbContextOptions options) : base(options) { } 10 | 11 | protected override void OnConfiguring(DbContextOptionsBuilder builder) 12 | { 13 | builder.EnableSensitiveDataLogging(); 14 | } 15 | 16 | public DbSet Users { get; set; } 17 | public DbSet UserRoles { get; set; } 18 | public DbSet Roles { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/Delve.Tests/Delve.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Source/Delve.AspNetCore/ModelStateValidator.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.Filters; 4 | 5 | namespace Delve.AspNetCore 6 | { 7 | internal class ModelStateValidator: ActionFilterAttribute 8 | { 9 | public override void OnActionExecuting(ActionExecutingContext actionContext) 10 | { 11 | if (actionContext.ModelState.IsValid == false) 12 | { 13 | actionContext.Result = new BadRequestObjectResult( 14 | actionContext.ModelState.Values 15 | .SelectMany(e => e.Errors) 16 | .Select(e => e.ErrorMessage)); 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Source/Delve/Models/Expressions/ExpandExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | using Delve.Models.Validation; 5 | 6 | namespace Delve.Models.Expressions 7 | { 8 | internal class ExpandExpression : BaseExpression 9 | { 10 | public override string Query { get { return Key; } } 11 | 12 | public ExpandExpression(string key) 13 | { 14 | Key = QuerySanitizer.GetKey(ValidationType.Expand, key); 15 | } 16 | 17 | public override IQueryable ApplyExpand(IQueryable source, Func, string, IQueryable> include) 18 | { 19 | return include != null ? include(source, Key) : source; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Demo/Delve.Demo/Delve.Demo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Source/Delve/Models/Expressions/IExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Delve.Models.Validation; 4 | 5 | namespace Delve.Models.Expressions 6 | { 7 | internal interface IExpression 8 | { 9 | string Key { get; } 10 | string Query { get; } 11 | Type PropertyType { get; } 12 | 13 | void ValidateExpression(IQueryValidator validator); 14 | } 15 | 16 | internal interface IExpression : IExpression 17 | { 18 | IQueryable ApplyFilter(IQueryable source); 19 | IQueryable ApplySort(IQueryable source, bool thenBy); 20 | IQueryable ApplyExpand(IQueryable source, 21 | Func, string, IQueryable> include); 22 | Func GetPropertyMapping(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Source/Delve/Models/DelveOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Delve.Models 2 | { 3 | public class DelveOptions 4 | { 5 | private int _defaultPageSize = 5; 6 | public int DefaultPageSize 7 | { 8 | get { return _defaultPageSize; } 9 | set 10 | { 11 | if (value > 0) { _defaultPageSize = value; } 12 | } 13 | } 14 | 15 | private int _maxPageSize = 25; 16 | public int MaxPageSize 17 | { 18 | get { return _maxPageSize; } 19 | set 20 | { 21 | if (value > 0) { _maxPageSize = value; } 22 | } 23 | } 24 | 25 | public bool IgnoreNullOnSerilazation { get; set; } = true; 26 | 27 | public bool OmitHostOnPaginationLinks { get; set; } = false; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/Delve.Tests/Models/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Delve.Tests.Models 5 | { 6 | internal class User 7 | { 8 | public int Id { get; set; } 9 | public string FirstName { get; set; } 10 | public string LastName { get; set; } 11 | public DateTime DateOfBirth { get; set; } 12 | public Test Test { get; set; } 13 | 14 | public IEnumerable UserRoles { get; set; } 15 | 16 | public User(int id, string firstName, string lastName, DateTime dateOfBirth) 17 | { 18 | Id = id; 19 | FirstName = firstName; 20 | LastName = lastName; 21 | DateOfBirth = dateOfBirth; 22 | } 23 | } 24 | 25 | internal class Test 26 | { 27 | public string TestString { get; set; } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Source/Delve/Models/Expressions/SelectExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Delve.Models.Expressions 4 | { 5 | internal class SelectExpression : BaseExpression 6 | { 7 | public override string Query { get { return Key; } } 8 | 9 | public SelectExpression(string select) 10 | { 11 | Key = select; 12 | } 13 | 14 | public override Func GetPropertyMapping() 15 | { 16 | try 17 | { 18 | return t => (Key, Property.Compile()(t)); 19 | } 20 | 21 | catch (ArgumentNullException) 22 | { 23 | throw new InvalidQueryException($"Could not select requested property: '{Key}'." + 24 | "Likely cause by missing Expand."); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Source/Delve/Models/Config/IQueryConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | 6 | namespace Delve.Models 7 | { 8 | internal interface IQueryConfiguration 9 | { 10 | IQueryable ApplyDefaultFilters(IQueryable source); 11 | IQueryable ApplyDefaultSorts(IQueryable source); 12 | IQueryable ApplyDefaultExpands(IQueryable source, Func, string, IQueryable> include); 13 | IList ApplyDefaultSelects(IEnumerable source); 14 | 15 | void AddDefaultFilter(Expression> exp); 16 | void AddDefaultSort(Expression> exp, bool descending); 17 | void AddDefaultSelect(string key, Expression> exp); 18 | void AddDefaultExpand(Expression> exp); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Source/Delve.AspNetCore/PercentEncodeReplace.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace Delve.AspNetCore 5 | { 6 | public static class PercentEncodeReplace 7 | { 8 | private static readonly IDictionary percentEncodeMaps = new Dictionary 9 | { 10 | { "%20", " "}, 11 | { "%21", "!" }, 12 | { "%24", "$" }, 13 | { "%2A", "*" }, 14 | { "%3C", "<" }, 15 | { "%3D", "=" }, 16 | { "%3E", ">" }, 17 | { "%3F", "?" }, 18 | { "%5E", "^" } 19 | }; 20 | 21 | public static string Replace(string url) 22 | { 23 | if (url == null) { return null; } 24 | 25 | return percentEncodeMaps.Aggregate(url, (current, map) 26 | => current.Replace(map.Key, map.Value)); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Source/Delve/Models/PagedResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Delve.Models 4 | { 5 | public class PagedResult : List, IPagedResult 6 | { 7 | public int PageNumber { get; private set; } 8 | public int TotalPages { get; private set; } 9 | 10 | public int PageSize { get; private set; } 11 | public int TotalCount { get; private set; } 12 | 13 | public bool HasPrevious 14 | { 15 | get { return PageNumber > 1; } 16 | } 17 | 18 | public bool HasNext 19 | { 20 | get { return PageNumber < TotalPages; } 21 | } 22 | 23 | public PagedResult(IEnumerable items, int pageNumber, int pageSize, int count) 24 | { 25 | AddRange(items); 26 | PageNumber = pageNumber; 27 | PageSize = pageSize; 28 | TotalCount = count; 29 | TotalPages = (count + pageSize - 1) / pageSize; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Source/Delve/Delve.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | true 6 | 0.9.5.1-alpha 7 | Benjamin Bartels 8 | Pagination, sorting, filtering, projection and expanding library for .NET Core 9 | Copyright @2018 Benjamin Bartels 10 | https://opensource.org/licenses/MIT 11 | http://github.com/bbartels/Delve 12 | pagination sorting filtering .net .netcore core mvc 13 | Added option to omit hostname on x-pagination header links 14 | 0.9.5.1 15 | 0.9.5.1 16 | Benjamin Bartels 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Demo/Delve.Demo/Persistence/UserRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | using Delve.Demo.Models; 7 | 8 | namespace Delve.Demo.Persistence 9 | { 10 | public class UserRepository : Repository, IUserRepository 11 | { 12 | public UserManagerContext UserManagerContext { get { return Context as UserManagerContext; } } 13 | public UserRepository(DbContext context) : base(context) { } 14 | 15 | public async Task GetIdAsync(string username) 16 | { 17 | return await UserManagerContext.Users.Where(u => u.FirstName == username).Select(u => u.Id).SingleOrDefaultAsync(); 18 | } 19 | 20 | public async Task> GetRolesAsync(User user) 21 | { 22 | return await UserManagerContext.Entry(user).Collection(u => u.UserRoles) 23 | .Query().Select(ur => ur.Role).ToListAsync(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/Delve.Tests/PagedResultTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | 4 | using Delve.Models; 5 | using Delve.Tests.Models; 6 | 7 | namespace Delve.Tests 8 | { 9 | [TestClass] 10 | internal class PagedResultTests 11 | { 12 | private IPagedResult _pagedUsers; 13 | private ResourceParameter _param; 14 | private IList _users; 15 | 16 | [TestInitialize] 17 | public void Initialize() 18 | { 19 | _users = Repository.GetUsers(); 20 | _param = new ResourceParameter(); 21 | } 22 | 23 | [TestMethod] 24 | public void PagedResult_ValidParameters_Succeeds() 25 | { 26 | _pagedUsers = new PagedResult(_users, 1, 5, _users.Count); 27 | Assert.AreEqual(_pagedUsers.PageSize, 5); 28 | Assert.IsTrue(_pagedUsers.HasNext); 29 | Assert.IsFalse(_pagedUsers.HasPrevious); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/Delve.Tests/TestValidator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | 4 | using Delve.Models.Validation; 5 | using Delve.Tests.Models; 6 | 7 | namespace Delve.Tests 8 | { 9 | internal class TestValidator : AbstractQueryValidator 10 | { 11 | public void CanSelectTest(string key, Expression> exp) 12 | { 13 | CanSelect(key, exp); 14 | } 15 | 16 | public void CanFilterTest(string key, Expression> exp) 17 | { 18 | CanFilter(key, exp); 19 | } 20 | 21 | public void CanOrderTest(string key, Expression> exp) 22 | { 23 | CanOrder(key, exp); 24 | } 25 | 26 | public void CanExpandTest(Expression> exp) where T : class 27 | { 28 | CanExpand(exp); 29 | } 30 | 31 | public void AllowAllTest(string key, Expression> exp) 32 | { 33 | AllowAll(key, exp); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Source/Delve/Models/Config/SelectConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | 4 | namespace Delve.Models 5 | { 6 | internal class SelectConfiguration : ISelectConfiguration 7 | { 8 | private readonly string _key; 9 | private readonly Expression> _expression; 10 | 11 | public SelectConfiguration(string key, Expression> expression) 12 | { 13 | _key = key; 14 | _expression = expression; 15 | } 16 | 17 | 18 | public Func GetPropertyMapping() 19 | { 20 | try 21 | { 22 | return t => (_key, _expression.Compile()(t)); 23 | } 24 | 25 | catch (ArgumentNullException) 26 | { 27 | throw new InvalidQueryException($"Could not select requested property: '{_key}'." + 28 | "Likely cause by missing Expand."); 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Source/Delve/Models/Validation/ValidationRule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | 4 | using Delve.Models.Expressions; 5 | 6 | namespace Delve.Models.Validation 7 | { 8 | internal class ValidationRule : IValidationRule 9 | { 10 | public ValidationType ValidationType { get; } 11 | 12 | public Type ResultType 13 | { 14 | get { return typeof(TResult); } 15 | } 16 | 17 | public Expression> Expression { get; private set; } 18 | 19 | public ValidationRule(Expression> exp, ValidationType type) 20 | { 21 | Expression = exp; 22 | ValidationType = type; 23 | } 24 | 25 | public IValidationRule ValidateExpression(IExpression exp) 26 | { 27 | if (exp.PropertyType == Expression.ReturnType) { return this; } 28 | 29 | throw new InvalidQueryException($"Values of key: '{exp.Key}' do not " + 30 | $"match Type: '{Expression.ReturnType}'."); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010-2018 Google, Inc. http://angularjs.org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Demo/Delve.Demo/Persistence/UnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace Delve.Demo.Persistence 6 | { 7 | public class UnitOfWork : IUnitOfWork 8 | { 9 | private readonly DbContext _context; 10 | private IUserRepository _users; 11 | 12 | public IUserRepository Users => _users ?? (_users = new UserRepository(_context)); 13 | 14 | public UnitOfWork(UserManagerContext context) 15 | { 16 | _context = context; 17 | } 18 | 19 | public void SaveChanges() 20 | { 21 | _context.SaveChanges(); 22 | } 23 | public async Task SaveChangesAsync() 24 | { 25 | await _context.SaveChangesAsync(); 26 | } 27 | 28 | private bool _disposed; 29 | protected virtual void Dispose(bool disposing) 30 | { 31 | if (!_disposed && disposing) { _context.Dispose(); } 32 | _disposed = true; 33 | } 34 | 35 | public void Dispose() 36 | { 37 | Dispose(true); 38 | GC.SuppressFinalize(this); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Source/Delve/Models/Config/SortConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Linq.Expressions; 4 | 5 | namespace Delve.Models 6 | { 7 | internal class SortConfiguration : ISortConfiguration 8 | { 9 | private readonly Expression> _expression; 10 | private readonly bool _descending; 11 | 12 | public SortConfiguration(Expression> expression, bool descending) 13 | { 14 | _expression = expression; 15 | _descending = descending; 16 | } 17 | 18 | public IOrderedQueryable ApplySort(IQueryable source) 19 | { 20 | if (source == null) { return null; } 21 | return _descending 22 | ? source.OrderByDescending(_expression) 23 | : source.OrderBy(_expression); 24 | } 25 | 26 | public IOrderedQueryable ApplySort(IOrderedQueryable source) 27 | { 28 | if (source == null) { return null; } 29 | return _descending 30 | ? source.ThenByDescending(_expression) 31 | : source.ThenBy(_expression); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Source/Delve.AspNetCore/Delve.AspNetCore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | Benjamin Bartels 6 | 0.9.5.1-alpha 7 | 0.9.5.1 8 | 0.9.5.1 9 | pagination sorting filtering .net .netcore core mvc 10 | Copyright @2018 Benjamin Bartels 11 | Asp.NET Core MVC integration for the Delve library. 12 | Benjamin Bartels 13 | https://opensource.org/licenses/MIT 14 | Added option to omit hostname on x-pagination header links 15 | true 16 | http://github.com/bbartels/Delve 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Source/Delve/Models/Expressions/OrderByExpression.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | using Delve.Models.Validation; 4 | 5 | namespace Delve.Models.Expressions 6 | { 7 | internal class OrderByExpression : BaseExpression 8 | { 9 | public bool Descending { get; } 10 | 11 | public override string Query { get { return $"{(Descending ? "-" : "")}{Key}"; } } 12 | 13 | public OrderByExpression(string orderBy) 14 | { 15 | if (orderBy.StartsWith("-")) 16 | { 17 | Descending = true; 18 | } 19 | 20 | Key = QuerySanitizer.GetKey(ValidationType.OrderBy, orderBy); 21 | } 22 | 23 | public override IQueryable ApplySort(IQueryable source, bool thenBy) 24 | { 25 | var property = Property.Compile(); 26 | 27 | if (!thenBy) 28 | { 29 | return Descending 30 | ? source.OrderByDescending(x => property(x)) 31 | : source.OrderBy(x => property(x)); 32 | } 33 | 34 | var orderedSource = source as IOrderedQueryable; 35 | 36 | return Descending 37 | ? orderedSource.ThenByDescending(x => property(x)) 38 | : orderedSource.ThenBy(x => property(x)); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Source/Delve/Models/Expressions/FilterExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | using Delve.Models.Validation; 5 | 6 | namespace Delve.Models.Expressions 7 | { 8 | internal class FilterExpression : BaseExpression 9 | { 10 | public QueryOperator Operator { get; } 11 | public string[] StringValues { get; } 12 | 13 | public override string Query 14 | { 15 | get 16 | { 17 | return $"{Key}{QuerySanitizer.GetFilterSymbol(Operator)}{StringValues.Aggregate((x, y) => $"{x},{y}")}"; 18 | } 19 | } 20 | 21 | public Func OperationExpression { get; private set; } 22 | public TResult[] Values { get; } 23 | 24 | 25 | public FilterExpression(string filter) 26 | { 27 | Operator = QuerySanitizer.GetFilterOperator(filter); 28 | Key = QuerySanitizer.GetKey(ValidationType.Filter, filter); 29 | StringValues = QuerySanitizer.GetFilterValues(filter); 30 | this.ValidatePropertyType(Key, StringValues); 31 | 32 | Values = StringValues.Select(x => (TResult)Convert.ChangeType(x, typeof(TResult))).ToArray(); 33 | OperationExpression = ExpressionFactory.RequestFunc(Operator, typeof(TResult)); 34 | } 35 | 36 | public override IQueryable ApplyFilter(IQueryable source) 37 | { 38 | var compiledProp = Property.Compile(); 39 | 40 | return source.Where(x => Values.Any(v => OperationExpression(v, compiledProp(x)))); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Demo/Delve.Demo/Controller/UserController.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using AutoMapper; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | using Delve.Demo.Models; 6 | using Delve.Demo.Persistence; 7 | using Delve.AspNetCore; 8 | using Delve.Models; 9 | 10 | namespace Delve.Demo.Controller 11 | { 12 | [Produces("application/json")] 13 | [Route("api/User")] 14 | public class UserController : Microsoft.AspNetCore.Mvc.Controller 15 | { 16 | private readonly IUnitOfWork _unitOfWork; 17 | private readonly IMapper _mapper; 18 | private readonly IUrlHelper _urlHelper; 19 | 20 | public UserController(IUnitOfWork unitOfWork, IMapper mapper, IUrlHelper urlHelper) 21 | { 22 | _unitOfWork = unitOfWork; 23 | _mapper = mapper; 24 | //urlhelper to allow adding paginationheader to response 25 | _urlHelper = urlHelper; 26 | } 27 | 28 | /// 29 | /// Action to query Users from a database. 30 | /// 31 | /// The which is 32 | /// automatically parsed from the request. 33 | /// The resulting collection of users. 34 | [HttpGet] 35 | public async Task GetUsers(IResourceParameter param) 36 | { 37 | //Gets data from database 38 | var usersDb = await _unitOfWork.Users.GetAsync(param); 39 | //var users = _mapper.Map, IEnumerable>(usersDb); 40 | 41 | 42 | //Adds Paginationheader to response 43 | this.AddPaginationHeader(param, usersDb, _urlHelper); 44 | 45 | //Shapes data on return. 46 | return Ok(usersDb.ShapeData(param)); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /Demo/Delve.Demo/Persistence/IRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Threading.Tasks; 6 | 7 | using Delve.Models; 8 | 9 | namespace Delve.Demo.Persistence 10 | { 11 | public interface IRepository where T : class 12 | { 13 | T GetById(int id); 14 | 15 | Task GetByIdAsync(int id); 16 | 17 | IEnumerable Get(Expression> predicate = null, 18 | Func, IOrderedQueryable> orderBy = null, 19 | string includeProperties = null, 20 | int? skip = null, 21 | int? take = null); 22 | 23 | Task> GetAsync(Expression> predicate = null, 24 | Func, IOrderedQueryable> orderBy = null, 25 | string includes = null, 26 | int? skip = null, 27 | int? take = null); 28 | 29 | IPagedResult Get(IResourceParameter parameters); 30 | 31 | Task> GetAsync(IResourceParameter parameters); 32 | 33 | T SingleOrDefault(Expression> predicate); 34 | 35 | Task SingleOrDefaultAsync(Expression> predicate); 36 | 37 | 38 | int GetCount(Expression> predicate = null); 39 | 40 | Task GetCountAsync(Expression> predicate = null); 41 | 42 | bool GetExists(Expression> predicate = null); 43 | 44 | Task GetExistsAsync(Expression> predicate = null); 45 | 46 | void Add(T entity); 47 | 48 | void AddRange(IEnumerable entities); 49 | 50 | void Remove(T entity); 51 | 52 | void RemoveRange(IEnumerable entities); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Source/Delve/Models/Validation/ValidationRules.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using Delve.Models.Expressions; 6 | 7 | namespace Delve.Models.Validation 8 | { 9 | internal class ValidationRules 10 | { 11 | private readonly IList _rules = new List(); 12 | 13 | public ValidationRules(IValidationRule rule, string key) 14 | { 15 | AddRule(rule, key); 16 | } 17 | 18 | public void AddRule(IValidationRule rule, string key) 19 | { 20 | var type = rule.ValidationType; 21 | if (_rules.Select(x => x.ValidationType).Contains(type)) 22 | { 23 | throw new InvalidValidationBuilderException($"Invalid rule definition of key: '{key}'." + 24 | $"Cannot define two equal ValidationRules on same key."); 25 | } 26 | 27 | _rules.Add(rule); 28 | } 29 | 30 | public IValidationRule ValidateExpression(ValidationType type, IExpression exp) 31 | { 32 | CheckForValidationType(type, exp.Key); 33 | return _rules.SingleOrDefault(x => x.ValidationType == type).ValidateExpression(exp); 34 | } 35 | 36 | public Type GetResultType(ValidationType type, string key) 37 | { 38 | CheckForValidationType(type, key); 39 | return _rules.SingleOrDefault(x => x.ValidationType == type).ResultType; 40 | } 41 | 42 | private void CheckForValidationType(ValidationType type, string key) 43 | { 44 | if (!_rules.Select(x => x.ValidationType).Contains(type)) 45 | { 46 | throw new InvalidQueryException($"Type: '{type}' has not been registered under key: '{key}'"); 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Source/Delve.AspNetCore/IMvcBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.Infrastructure; 4 | using Microsoft.AspNetCore.Mvc.Routing; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | using Delve.Models; 8 | 9 | namespace Delve.AspNetCore 10 | { 11 | public static class IMvcBuilderExtensions 12 | { 13 | public static IMvcBuilder AddDelve(this IMvcBuilder mvcBuilder, Action options = null) 14 | { 15 | var option = new DelveOptions(); 16 | options?.Invoke(option); 17 | ResourceParameterOptions.ApplyConfig(option); 18 | 19 | //Adds UrlHelper services to mvc app 20 | mvcBuilder.Services.AddSingleton(); 21 | mvcBuilder.Services.AddScoped(factory => 22 | new UrlHelper(factory.GetService().ActionContext)); 23 | 24 | //Adds IResourceParameter ModelBinding and ModelState validator 25 | mvcBuilder.AddMvcOptions(opt => 26 | { 27 | opt.ModelBinderProviders.Insert(0, new ResourceParamBinderProvider()); 28 | opt.Filters.Add(new ModelStateValidator()); 29 | }); 30 | 31 | //Ignores CiruclarReference checking in json.net 32 | mvcBuilder.AddJsonOptions(opt => { 33 | opt.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; 34 | opt.SerializerSettings.NullValueHandling = ResourceParameterOptions.IgnoreNullOnSerialization 35 | ? Newtonsoft.Json.NullValueHandling.Ignore 36 | : Newtonsoft.Json.NullValueHandling.Include; 37 | }); 38 | 39 | return mvcBuilder; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Source/Delve/Models/Expressions/ExpressionFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Delve.Models.Expressions 4 | { 5 | internal static class ExpressionFactory 6 | { 7 | private static readonly (Type type, Func func)[] funcs = 8 | { 9 | (typeof(object), (x, y) => x.Equals(y)), 10 | (typeof(string), (x, y) => ((string)(object)x).Equals((string)(object)y, StringComparison.OrdinalIgnoreCase)), 11 | (typeof(object), (x, y) => !x.Equals(y)), 12 | (typeof(string), (x, y) => !((string)(object)x).Equals((string)(object)y, StringComparison.OrdinalIgnoreCase)), 13 | (typeof(IComparable), (x, y) => ((IComparable)y).CompareTo(x) > 0), 14 | (typeof(IComparable), (x, y) => ((IComparable)y).CompareTo(x) < 0), 15 | (typeof(IComparable), (x, y) => ((IComparable)y).CompareTo(x) >= 0), 16 | (typeof(IComparable), (x, y) => ((IComparable)y).CompareTo(x) <= 0), 17 | (typeof(string), (x, y) => ((string)(object)y).IndexOf((string)(object)x, StringComparison.Ordinal) != -1), 18 | (typeof(string), (x, y) => ((string)(object)y).IndexOf((string)(object)x, StringComparison.OrdinalIgnoreCase) != -1), 19 | (typeof(string), (x, y) => ((string)(object)y).StartsWith((string)(object)x, StringComparison.Ordinal)), 20 | (typeof(string), (x, y) => ((string)(object)y).StartsWith((string)(object)x, StringComparison.OrdinalIgnoreCase)), 21 | (typeof(string), (x, y) => ((string)(object)y).EndsWith((string)(object)x, StringComparison.Ordinal)), 22 | (typeof(string), (x, y) => ((string)(object)y).EndsWith((string)(object)x, StringComparison.OrdinalIgnoreCase)) 23 | //(typeof(IEnumerable), (x, y) => ((IEnumerable)y).Contains(x)) 24 | }; 25 | 26 | public static Func RequestFunc(QueryOperator op, Type type) 27 | { 28 | var func = funcs[(int)op]; 29 | if (!func.type.IsAssignableFrom(type)) 30 | { 31 | throw new InvalidQueryException($"Cannot use operator: '{op}' with type: '{type}'"); 32 | } 33 | 34 | return funcs[(int)op].func; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Demo/Delve.Demo/Models/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Delve.Models.Validation; 5 | 6 | namespace Delve.Demo.Models 7 | { 8 | public class User 9 | { 10 | public int Id { get; set; } 11 | public string FirstName { get; set; } 12 | public string LastName { get; set; } 13 | public DateTime DateOfBirth { get; set; } 14 | 15 | public IEnumerable UserRoles { get; set; } 16 | 17 | public User() { } 18 | 19 | public User(string firstName, string lastName, DateTime dateOfBirth) 20 | { 21 | FirstName = firstName; 22 | LastName = lastName; 23 | DateOfBirth = dateOfBirth; 24 | } 25 | } 26 | 27 | public class UserQueryValidator : AbstractQueryValidator 28 | { 29 | public UserQueryValidator() 30 | { 31 | //Adds Id and virtual property "Name" as default selects 32 | AddDefaultSelect("Id", u => u.Id); 33 | AddDefaultSelect("Name", u => u.LastName + " " + u.FirstName); 34 | //Adds virtual property as default sort 35 | AddDefaultSort(u => u.LastName + " " + u.LastName, true); 36 | AddDefaultExpand(u => u.UserRoles); 37 | 38 | //Allows clients to select Id 39 | CanSelect("Id", x => x.Id); 40 | 41 | //Allows clients to select virtual property Name 42 | CanSelect("Name", x => x.FirstName + " " + x.LastName); 43 | 44 | //Allows clients to order by virtual property Name 45 | CanOrder("Name", x => x.LastName + " " + x.FirstName); 46 | 47 | //Allows clients to order by virtual property Age 48 | CanSelect("Age", x => Math.Round((DateTime.Now - x.DateOfBirth).TotalDays / 365, 2)); 49 | CanOrder("Age", x => Math.Round((DateTime.Now - x.DateOfBirth).TotalDays / 365, 2)); 50 | 51 | //Allows clients to filter on virtual property Name 52 | CanFilter("Name", x => x.FirstName + " " + x.LastName); 53 | CanFilter("Id", x => x.Id); 54 | 55 | CanSelect("RoleId", x => x.UserRoles.Select(ur => ur.RoleId)); 56 | 57 | //Allows clients to expand on UserRoles (EF Core Include) 58 | //WARNING: Can lead to bad performance if used incorrectly. 59 | CanExpand(x => x.UserRoles.Select(y => y.Role)); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Source/Delve/Models/Expressions/BaseExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Linq.Expressions; 4 | 5 | using Delve.Models.Validation; 6 | 7 | namespace Delve.Models.Expressions 8 | { 9 | internal abstract class BaseExpression : IExpression 10 | { 11 | public string Key { get; protected set; } 12 | public abstract string Query { get; } 13 | public Type PropertyType { get { return typeof(TResult); } } 14 | public Expression> Property; 15 | 16 | public void ValidateExpression(IQueryValidator validator) 17 | { 18 | var rule = ((IInternalQueryValidator)validator).ValidateExpression(this, IExpressionExtensions.GetValidationType(GetType())); 19 | 20 | try 21 | { 22 | Property = ((ValidationRule)rule).Expression; 23 | } 24 | 25 | catch (InvalidCastException) 26 | { 27 | throw new RuntimeException("Could not cast validationrule."); 28 | } 29 | } 30 | 31 | public virtual IQueryable ApplyFilter(IQueryable source) 32 | { 33 | throw new RuntimeException("This method should not be called. Please report to github.com/bbartels/delve."); 34 | } 35 | 36 | public virtual IQueryable ApplySort(IQueryable source, bool thenBy) 37 | { 38 | throw new RuntimeException("This method should not be called. Please report to github.com/bbartels/delve."); 39 | } 40 | 41 | public virtual IQueryable ApplyExpand(IQueryable source, Func, string, IQueryable> include) 42 | { 43 | throw new RuntimeException("This method should not be called. Please report to github.com/bbartels/delve."); 44 | } 45 | 46 | public virtual string GetSelectProperty() 47 | { 48 | throw new RuntimeException("This method should not be called. Please report to github.com/bbartels/delve."); 49 | } 50 | 51 | public virtual Func GetPropertyMapping() 52 | { 53 | throw new RuntimeException("This method should not be called. Please report to github.com/bbartels/delve."); 54 | } 55 | 56 | protected string GetPropertyName() 57 | { 58 | var propString = Property.ToString(); 59 | return propString.Substring(propString.IndexOf('.') + 1).TrimEnd(')'); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Delve.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27130.2027 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Delve", "Source\Delve\Delve.csproj", "{72D86D9B-B697-4E2B-84C1-B785FDC7C252}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Delve.AspNetCore", "Source\Delve.AspNetCore\Delve.AspNetCore.csproj", "{6276F597-CC58-4C25-94B3-5A69C0C7B93E}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Delve.Demo", "Demo\Delve.Demo\Delve.Demo.csproj", "{3E74CEE9-F8F0-4CF2-BDA4-98577C332AC6}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Delve.Tests", "Tests\Delve.Tests\Delve.Tests.csproj", "{C1A0A28B-1532-46AD-8795-2D83979B32BF}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {72D86D9B-B697-4E2B-84C1-B785FDC7C252}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {72D86D9B-B697-4E2B-84C1-B785FDC7C252}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {72D86D9B-B697-4E2B-84C1-B785FDC7C252}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {72D86D9B-B697-4E2B-84C1-B785FDC7C252}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {6276F597-CC58-4C25-94B3-5A69C0C7B93E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {6276F597-CC58-4C25-94B3-5A69C0C7B93E}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {6276F597-CC58-4C25-94B3-5A69C0C7B93E}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {6276F597-CC58-4C25-94B3-5A69C0C7B93E}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {3E74CEE9-F8F0-4CF2-BDA4-98577C332AC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {3E74CEE9-F8F0-4CF2-BDA4-98577C332AC6}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {3E74CEE9-F8F0-4CF2-BDA4-98577C332AC6}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {3E74CEE9-F8F0-4CF2-BDA4-98577C332AC6}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {C1A0A28B-1532-46AD-8795-2D83979B32BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {C1A0A28B-1532-46AD-8795-2D83979B32BF}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {C1A0A28B-1532-46AD-8795-2D83979B32BF}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {C1A0A28B-1532-46AD-8795-2D83979B32BF}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {70099D84-3797-4CEE-9F71-19979201B240} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /Source/Delve/Extensions/IQueryableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Delve.Models 7 | { 8 | public static class IQueryableExtensions 9 | { 10 | public static async Task> ToPagedResultAsync(this IQueryable source, 11 | Func, Task> CountAsync, 12 | Func, Task>> ToListAsync, 13 | IResourceParameter param) 14 | { 15 | int count = await CountAsync(source); 16 | int pageNum = GetPageNum(count, param.PageSize, param.PageNumber); 17 | var test = source.Skip((pageNum == 0 ? 1 : pageNum - 1) * param.PageSize) 18 | .Take(param.PageSize); 19 | var items = await ToListAsync(test); 20 | return new PagedResult(items, pageNum, param.PageSize, count); 21 | } 22 | 23 | public static IPagedResult ToPagedResult(this IQueryable source, IResourceParameter param) 24 | { 25 | int count = source.Count(); 26 | int pageNum = GetPageNum(count, param.PageSize, param.PageNumber); 27 | 28 | var items = source.Skip((pageNum - 1) * param.PageSize).Take(param.PageSize).ToList(); 29 | return new PagedResult(items, pageNum, param.PageSize, count); 30 | } 31 | 32 | private static int GetPageNum(int count, int pageSize, int pageNum) 33 | { 34 | int totalPages = (count + pageSize - 1) / pageSize; 35 | return pageNum > totalPages ? totalPages : pageNum; 36 | } 37 | 38 | public static IQueryable ApplyFilters(this IQueryable source, IResourceParameter param) 39 | { 40 | if (source == null) { throw new ArgumentException($"{ nameof(source) }"); } 41 | 42 | return ((IInternalResourceParameter)param).ApplyFilters(source); 43 | } 44 | 45 | public static IQueryable ApplyOrderBy(this IQueryable source, IResourceParameter param) 46 | { 47 | if (source == null) { throw new ArgumentException($"{ nameof(source) }"); } 48 | 49 | 50 | source = ((IInternalResourceParameter)param).ApplyOrderBy(source); 51 | return source; 52 | } 53 | 54 | public static IQueryable ApplyIncludes(this IQueryable source, 55 | Func, string, IQueryable> Include, IResourceParameter param) 56 | { 57 | if (source == null) { throw new ArgumentException($"{ nameof(source) }"); } 58 | return Include == null ? source : ((IInternalResourceParameter)param).ApplyExpand(source, Include); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Tests/Delve.Tests/ValidationBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | using Delve.Models.Validation; 6 | 7 | namespace Delve.Tests 8 | { 9 | [TestClass] 10 | public class ValidationBuilderTests 11 | { 12 | private TestValidator _validator; 13 | 14 | [TestInitialize] 15 | public void Initialize() 16 | { 17 | _validator = new TestValidator(); 18 | } 19 | 20 | [TestMethod] 21 | public void CanSelect_CorrectExpression_Succeeds() 22 | { 23 | _validator.CanSelectTest("FirstName", u => u.FirstName); 24 | _validator.CanSelectTest("Date", u => u.DateOfBirth); 25 | _validator.CanSelectTest("Id", u => u.Id); 26 | _validator.CanSelectTest("Age", u => Math.Round((DateTime.Now - u.DateOfBirth).TotalDays, 2)); 27 | _validator.CanSelectTest("Test", u => u.Test); 28 | Assert.IsTrue(true); 29 | } 30 | 31 | [TestMethod] 32 | [ExpectedException(typeof(InvalidValidationBuilderException))] 33 | public void CanSelect_DuplicateKey_ThrowsException() 34 | { 35 | _validator.CanSelectTest("Id", u => u.Id); 36 | _validator.CanSelectTest("Id", u => u.FirstName); 37 | } 38 | 39 | [TestMethod] 40 | [ExpectedException(typeof(InvalidValidationBuilderException))] 41 | public void CanFilter_InvalidType_ThrowsException() 42 | { 43 | _validator.CanFilterTest("Test", u => u.Test); 44 | } 45 | 46 | [TestMethod] 47 | [ExpectedException(typeof(InvalidValidationBuilderException))] 48 | public void AllowAll_InvalidFilterType_ThrowsException() 49 | { 50 | _validator.AllowAllTest("Test", u => u.Test); 51 | } 52 | 53 | [TestMethod] 54 | [ExpectedException(typeof(InvalidValidationBuilderException))] 55 | public void AllowAll_DuplicateKey_ThrowsException() 56 | { 57 | _validator.AllowAllTest("Id", u => u.Id); 58 | _validator.CanSelectTest("Id", u => u.DateOfBirth); 59 | } 60 | 61 | [TestMethod] 62 | public void AllowAll_DuplicateExpression_Succeeds() 63 | { 64 | _validator.AllowAllTest("Id", u => u.Id); 65 | _validator.AllowAllTest("identification", u => u.Id); 66 | } 67 | 68 | [TestMethod] 69 | public void AllowAll_NavigationProperty_Succeeds() 70 | { 71 | _validator.AllowAllTest("RoleId", u => u.UserRoles.Select(x => x.RoleId)); 72 | } 73 | 74 | [TestMethod] 75 | public void CanExpand_ValidExpression_Succeeds() 76 | { 77 | _validator.CanExpandTest(u => u.UserRoles.Select(x => x.Role.UserRoles.Select(y => y.Role))); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Demo/Delve.Demo/Migrations/UserManagerContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | 7 | using Delve.Demo.Persistence; 8 | 9 | namespace Delve.Demo.Migrations 10 | { 11 | [DbContext(typeof(UserManagerContext))] 12 | partial class UserManagerContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasAnnotation("ProductVersion", "2.0.1-rtm-125") 19 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 20 | 21 | modelBuilder.Entity("Delve.Demo.Models.Role", b => 22 | { 23 | b.Property("Id") 24 | .ValueGeneratedOnAdd(); 25 | 26 | b.Property("Description"); 27 | 28 | b.Property("Name"); 29 | 30 | b.HasKey("Id"); 31 | 32 | b.ToTable("Roles"); 33 | }); 34 | 35 | modelBuilder.Entity("Delve.Demo.Models.User", b => 36 | { 37 | b.Property("Id") 38 | .ValueGeneratedOnAdd(); 39 | 40 | b.Property("DateOfBirth"); 41 | 42 | b.Property("FirstName"); 43 | 44 | b.Property("LastName"); 45 | 46 | b.HasKey("Id"); 47 | 48 | b.ToTable("Users"); 49 | }); 50 | 51 | modelBuilder.Entity("Delve.Demo.Models.UserRole", b => 52 | { 53 | b.Property("Id") 54 | .ValueGeneratedOnAdd(); 55 | 56 | b.Property("RoleId"); 57 | 58 | b.Property("UserId"); 59 | 60 | b.HasKey("Id"); 61 | 62 | b.HasIndex("RoleId"); 63 | 64 | b.HasIndex("UserId"); 65 | 66 | b.ToTable("UserRoles"); 67 | }); 68 | 69 | modelBuilder.Entity("Delve.Demo.Models.UserRole", b => 70 | { 71 | b.HasOne("Delve.Demo.Models.Role", "Role") 72 | .WithMany("UserRoles") 73 | .HasForeignKey("RoleId") 74 | .OnDelete(DeleteBehavior.Cascade); 75 | 76 | b.HasOne("Delve.Demo.Models.User", "User") 77 | .WithMany("UserRoles") 78 | .HasForeignKey("UserId") 79 | .OnDelete(DeleteBehavior.Cascade); 80 | }); 81 | #pragma warning restore 612, 618 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Demo/Delve.Demo/Migrations/20180212171542_InitialCreate.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Delve.Demo.Persistence; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage; 8 | using Microsoft.EntityFrameworkCore.Storage.Internal; 9 | using System; 10 | 11 | namespace Delve.Demo.Migrations 12 | { 13 | [DbContext(typeof(UserManagerContext))] 14 | [Migration("20180212171542_InitialCreate")] 15 | partial class InitialCreate 16 | { 17 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 18 | { 19 | #pragma warning disable 612, 618 20 | modelBuilder 21 | .HasAnnotation("ProductVersion", "2.0.1-rtm-125") 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("Delve.Demo.Models.Role", b => 25 | { 26 | b.Property("Id") 27 | .ValueGeneratedOnAdd(); 28 | 29 | b.Property("Description"); 30 | 31 | b.Property("Name"); 32 | 33 | b.HasKey("Id"); 34 | 35 | b.ToTable("Roles"); 36 | }); 37 | 38 | modelBuilder.Entity("Delve.Demo.Models.User", b => 39 | { 40 | b.Property("Id") 41 | .ValueGeneratedOnAdd(); 42 | 43 | b.Property("DateOfBirth"); 44 | 45 | b.Property("FirstName"); 46 | 47 | b.Property("LastName"); 48 | 49 | b.HasKey("Id"); 50 | 51 | b.ToTable("Users"); 52 | }); 53 | 54 | modelBuilder.Entity("Delve.Demo.Models.UserRole", b => 55 | { 56 | b.Property("Id") 57 | .ValueGeneratedOnAdd(); 58 | 59 | b.Property("RoleId"); 60 | 61 | b.Property("UserId"); 62 | 63 | b.HasKey("Id"); 64 | 65 | b.HasIndex("RoleId"); 66 | 67 | b.HasIndex("UserId"); 68 | 69 | b.ToTable("UserRoles"); 70 | }); 71 | 72 | modelBuilder.Entity("Delve.Demo.Models.UserRole", b => 73 | { 74 | b.HasOne("Delve.Demo.Models.Role", "Role") 75 | .WithMany("UserRoles") 76 | .HasForeignKey("RoleId") 77 | .OnDelete(DeleteBehavior.Cascade); 78 | 79 | b.HasOne("Delve.Demo.Models.User", "User") 80 | .WithMany("UserRoles") 81 | .HasForeignKey("UserId") 82 | .OnDelete(DeleteBehavior.Cascade); 83 | }); 84 | #pragma warning restore 612, 618 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Source/Delve.AspNetCore/ResourceUriHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | using Delve.Models; 6 | using Microsoft.AspNetCore.Http; 7 | 8 | namespace Delve.AspNetCore 9 | { 10 | public static class ResourceHelper 11 | { 12 | public static void AddPaginationHeader(this Controller controller, 13 | IResourceParameter parameters, IPagedResult result, IUrlHelper urlHelper) 14 | { 15 | var actionName = controller.ControllerContext.ActionDescriptor.AttributeRouteInfo.Name; 16 | var attributes = new[] 17 | { 18 | nameof(parameters.PageNumber).PascalToCamelCase(), 19 | nameof(parameters.PageSize).PascalToCamelCase() 20 | }; 21 | 22 | var prevPage = result.HasPrevious ? urlHelper.Link(actionName, new Dictionary 23 | { 24 | { attributes[0], (result.PageNumber - 1).ToString() }, 25 | { attributes[1], result.PageSize.ToString() } 26 | }.AddRange(parameters.GetPageHeader())) : null; 27 | 28 | var nextPage = result.HasNext ? urlHelper.Link(actionName, new Dictionary 29 | { 30 | { attributes[0], (result.PageNumber + 1).ToString() }, 31 | { attributes[1], result.PageSize.ToString() } 32 | }.AddRange(parameters.GetPageHeader())) : null; 33 | 34 | var omitHost = ResourceParameterOptions.OmitHostOnPaginationLinks; 35 | 36 | var metaData = new 37 | { 38 | currentPage = result.PageNumber, 39 | pageSize = result.PageSize, 40 | totalPages = result.TotalPages, 41 | totalCount = result.TotalCount, 42 | previousPageLink = PercentEncodeReplace.Replace(omitHost ? OmitHost(prevPage, controller.HttpContext) : prevPage), 43 | nextPageLink = PercentEncodeReplace.Replace(omitHost ? OmitHost(nextPage, controller.HttpContext) : nextPage) 44 | }; 45 | 46 | controller.Response.Headers.Add("X-Pagination", Newtonsoft.Json.JsonConvert.SerializeObject(metaData)); 47 | } 48 | 49 | internal static string PascalToCamelCase(this string str) 50 | { 51 | return char.ToLower(str[0]) + str.Substring(1); 52 | } 53 | 54 | private static string OmitHost(string link, HttpContext context) 55 | { 56 | if (link == null) { return null; } 57 | string host = context.Request.Host.Value; 58 | int lengthHost = link.IndexOf(host, StringComparison.Ordinal) + host.Length; 59 | return link.Substring(lengthHost, link.Length - lengthHost); 60 | } 61 | 62 | public static Dictionary AddRange(this Dictionary d1, Dictionary d2) 63 | { 64 | foreach (var d in d2) 65 | { 66 | d1.Add(d.Key, d.Value); 67 | } 68 | 69 | return d1; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Source/Delve/Models/Expressions/IExpressionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Linq; 5 | 6 | using Delve.Models.Validation; 7 | 8 | namespace Delve.Models.Expressions 9 | { 10 | internal static class IExpressionExtensions 11 | { 12 | private static readonly Dictionary typeMap = new Dictionary 13 | { 14 | { ValidationType.Filter, typeof(FilterExpression<,>) }, 15 | { ValidationType.Expand, typeof(ExpandExpression<,>) }, 16 | { ValidationType.OrderBy, typeof(OrderByExpression<,>) }, 17 | { ValidationType.Select, typeof(SelectExpression<,>) } 18 | }; 19 | 20 | public static ValidationType GetValidationType(Type type) 21 | { 22 | return typeMap.FirstOrDefault(x => x.Value == type.GetGenericTypeDefinition()).Key; 23 | } 24 | 25 | public static string GetQuery(this IEnumerable items) 26 | { 27 | return items.Select(x => x.Query).Aggregate((x, y) => $"{x},{y}"); 28 | } 29 | 30 | public static List> ParseQuery(this string query, 31 | IInternalQueryValidator validator, ValidationType type) 32 | { 33 | var expressions = new List>(); 34 | 35 | if (query == null) { return expressions; } 36 | 37 | foreach (var subQuery in query.Split(',')) 38 | { 39 | var key = QuerySanitizer.GetKey(type, subQuery); 40 | 41 | var trimmedSubQuery = subQuery.Trim(); 42 | 43 | var expressionType = validator.GetResultType(key, type); 44 | expressions.Add((IExpression)MakeGenericType(typeMap[type], expressionType, trimmedSubQuery)); 45 | } 46 | 47 | return expressions; 48 | } 49 | 50 | private static IExpression MakeGenericType(Type expressionType, Type result, string query) 51 | { 52 | var type = expressionType.MakeGenericType(typeof(TEntity), result); 53 | return (IExpression)Activator.CreateInstance(type, query); 54 | } 55 | 56 | public static void ValidatePropertyType(this IExpression exp, string key, IEnumerable values) 57 | { 58 | if (exp.PropertyType == typeof(string)) { return; } 59 | 60 | var currentValue = string.Empty; 61 | try 62 | { 63 | var converter = TypeDescriptor.GetConverter(exp.PropertyType); 64 | foreach (var value in values) 65 | { 66 | currentValue = value; 67 | converter?.ConvertFromString(value); 68 | } 69 | } 70 | catch (NotSupportedException) 71 | { 72 | throw new InvalidQueryException($"Value: '{currentValue}' does not match " + 73 | $"datatype: '{exp.PropertyType}' of registered key: '{key}'"); 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Source/Delve/Models/Config/QueryConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Dynamic; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | 7 | using Delve.Models.Validation; 8 | 9 | namespace Delve.Models 10 | { 11 | internal class QueryConfiguration : IQueryConfiguration 12 | { 13 | private readonly IList>> _defaultFilters 14 | = new List>>(); 15 | private readonly IList> _defaultSorts 16 | = new List>(); 17 | private readonly IList> _defaultSelects 18 | = new List>(); 19 | 20 | private readonly IList _defaultExpands = new List(); 21 | 22 | public void AddDefaultFilter(Expression> exp) 23 | { 24 | _defaultFilters.Add(exp); 25 | } 26 | 27 | public void AddDefaultSelect(string key, Expression> exp) 28 | { 29 | _defaultSelects.Add(new SelectConfiguration(key, exp)); 30 | } 31 | 32 | public void AddDefaultSort(Expression> exp, bool descending) 33 | { 34 | _defaultSorts.Add(new SortConfiguration(exp, descending)); 35 | } 36 | 37 | public void AddDefaultExpand(Expression> exp) 38 | { 39 | _defaultExpands.Add(ValidatorHelpers.GetExpandString(exp.ToString())); 40 | } 41 | 42 | public IQueryable ApplyDefaultFilters(IQueryable source) 43 | { 44 | return source == null 45 | ? null 46 | : _defaultFilters.Aggregate(source, (current, filter) => current.Where(filter)); 47 | } 48 | 49 | public IQueryable ApplyDefaultExpands(IQueryable source, Func, string, IQueryable> include) 50 | { 51 | return source == null ? null : _defaultExpands.Aggregate(source, include); 52 | } 53 | 54 | public IList ApplyDefaultSelects(IEnumerable source) 55 | { 56 | if (_defaultSelects.Count == 0) 57 | { 58 | return source.Select(x => (object)x).ToList(); 59 | } 60 | 61 | var test = _defaultSelects.Select(x => x.GetPropertyMapping()); 62 | 63 | object applyFunc(T user, IEnumerable> func) 64 | { 65 | var t = new ExpandoObject() as IDictionary; 66 | foreach (var f in func) 67 | { 68 | var elements = f(user); 69 | t.Add(elements.Item1, elements.Item2); 70 | } 71 | 72 | return (ExpandoObject)t; 73 | } 74 | 75 | return source.Select(x => applyFunc(x, test)).ToList(); 76 | } 77 | 78 | public IQueryable ApplyDefaultSorts(IQueryable source) 79 | { 80 | if (source == null || _defaultSorts.Count == 0) { return source; } 81 | 82 | var orderedSort = _defaultSorts[0].ApplySort(source); 83 | 84 | for (int i = 1; i < _defaultSorts.Count; i++) 85 | { 86 | orderedSort = _defaultSorts[i].ApplySort(orderedSort); 87 | } 88 | 89 | return orderedSort; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Demo/Delve.Demo/Migrations/20180212171542_InitialCreate.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | using System; 4 | 5 | namespace Delve.Demo.Migrations 6 | { 7 | public partial class InitialCreate : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.CreateTable( 12 | name: "Roles", 13 | columns: table => new 14 | { 15 | Id = table.Column(nullable: false) 16 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), 17 | Description = table.Column(nullable: true), 18 | Name = table.Column(nullable: true) 19 | }, 20 | constraints: table => 21 | { 22 | table.PrimaryKey("PK_Roles", x => x.Id); 23 | }); 24 | 25 | migrationBuilder.CreateTable( 26 | name: "Users", 27 | columns: table => new 28 | { 29 | Id = table.Column(nullable: false) 30 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), 31 | DateOfBirth = table.Column(nullable: false), 32 | FirstName = table.Column(nullable: true), 33 | LastName = table.Column(nullable: true) 34 | }, 35 | constraints: table => 36 | { 37 | table.PrimaryKey("PK_Users", x => x.Id); 38 | }); 39 | 40 | migrationBuilder.CreateTable( 41 | name: "UserRoles", 42 | columns: table => new 43 | { 44 | Id = table.Column(nullable: false) 45 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), 46 | RoleId = table.Column(nullable: false), 47 | UserId = table.Column(nullable: false) 48 | }, 49 | constraints: table => 50 | { 51 | table.PrimaryKey("PK_UserRoles", x => x.Id); 52 | table.ForeignKey( 53 | name: "FK_UserRoles_Roles_RoleId", 54 | column: x => x.RoleId, 55 | principalTable: "Roles", 56 | principalColumn: "Id", 57 | onDelete: ReferentialAction.Cascade); 58 | table.ForeignKey( 59 | name: "FK_UserRoles_Users_UserId", 60 | column: x => x.UserId, 61 | principalTable: "Users", 62 | principalColumn: "Id", 63 | onDelete: ReferentialAction.Cascade); 64 | }); 65 | 66 | migrationBuilder.CreateIndex( 67 | name: "IX_UserRoles_RoleId", 68 | table: "UserRoles", 69 | column: "RoleId"); 70 | 71 | migrationBuilder.CreateIndex( 72 | name: "IX_UserRoles_UserId", 73 | table: "UserRoles", 74 | column: "UserId"); 75 | } 76 | 77 | protected override void Down(MigrationBuilder migrationBuilder) 78 | { 79 | migrationBuilder.DropTable( 80 | name: "UserRoles"); 81 | 82 | migrationBuilder.DropTable( 83 | name: "Roles"); 84 | 85 | migrationBuilder.DropTable( 86 | name: "Users"); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Source/Delve.AspNetCore/ResourceParamModelBinder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Threading.Tasks; 4 | using System.Web; 5 | using Microsoft.AspNetCore.Mvc.ModelBinding; 6 | using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; 7 | 8 | using Delve.Models; 9 | using Delve.Models.Validation; 10 | 11 | namespace Delve.AspNetCore 12 | { 13 | public class ResourceParamBinderProvider : IModelBinderProvider 14 | { 15 | public IModelBinder GetBinder(ModelBinderProviderContext context) 16 | { 17 | if (context == null) 18 | { 19 | throw new ArgumentNullException(nameof(context)); 20 | } 21 | 22 | return typeof(IResourceParameter).IsAssignableFrom(context.Metadata.ModelType) ? 23 | new BinderTypeModelBinder(typeof(ResourceParamModelBinder)) : null; 24 | } 25 | } 26 | 27 | public class ResourceParamModelBinder : IModelBinder 28 | { 29 | public Task BindModelAsync(ModelBindingContext bindingContext) 30 | { 31 | if (bindingContext.ModelType == typeof(IResourceParameter)) 32 | { 33 | throw new ArgumentException($"Must use generic interface: {typeof(IResourceParameter<>)} " + 34 | $"of EntityType instead of: {typeof(IResourceParameter)}."); 35 | } 36 | 37 | var elementTypes = bindingContext.ModelType.GetGenericArguments(); 38 | 39 | if (elementTypes.Length != 1) 40 | { 41 | throw new ArgumentException($"{nameof(IResourceParameter)} must have exactly one type argement."); 42 | } 43 | 44 | var parameterType = typeof(ResourceParameter<>).MakeGenericType(elementTypes[0]); 45 | var param = (IResourceParameter)Activator.CreateInstance(parameterType); 46 | 47 | var coll = HttpUtility.ParseQueryString(bindingContext.HttpContext.Request.QueryString.Value); 48 | var attributes = new[] 49 | { 50 | nameof(param.PageNumber).PascalToCamelCase(), 51 | nameof(param.PageSize).PascalToCamelCase(), 52 | "Filter", 53 | "OrderBy", 54 | "Select", 55 | "Expand" 56 | }; 57 | 58 | if (int.TryParse(coll[attributes[0]], out int num)) { param.PageNumber = num; } 59 | if (int.TryParse(coll[attributes[1]], out int size)) { param.PageSize = size; } 60 | 61 | try 62 | { 63 | var validatorType = typeof(IQueryValidator<>).MakeGenericType(elementTypes[0]); 64 | 65 | if (bindingContext.HttpContext.RequestServices.GetService(validatorType) is IQueryValidator validator) 66 | { 67 | param.ApplyParameters(validator, coll[attributes[2]], coll[attributes[3]], coll[attributes[4]], 68 | coll[attributes[5]]); 69 | } 70 | 71 | else 72 | { 73 | throw new RuntimeException($"No service registered for QueryValidator: {validatorType}."); 74 | } 75 | } 76 | catch (TargetInvocationException e) 77 | { 78 | bindingContext.ModelState.AddModelError("InvalidQueryString", e.InnerException.Message); 79 | bindingContext.Result = ModelBindingResult.Failed(); 80 | return Task.CompletedTask; 81 | } 82 | 83 | catch (InvalidQueryException e) 84 | { 85 | bindingContext.ModelState.AddModelError("InvalidQueryString", e.Message); 86 | bindingContext.Result = ModelBindingResult.Failed(); 87 | return Task.CompletedTask; 88 | } 89 | 90 | bindingContext.Result = ModelBindingResult.Success(param); 91 | return Task.CompletedTask; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Tests/Delve.Tests/Models/Repository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Delve.Tests.Models 5 | { 6 | internal static class Repository 7 | { 8 | private static readonly List _users; 9 | 10 | static Repository() 11 | { 12 | var users = new List 13 | { 14 | new User(1, "Veronica", "Fear", DateTime.Parse("23/01/1946 21:01:40")), 15 | new User(2, "Isabelle", "Bishop", DateTime.Parse("16/04/1968 18:00:00")), 16 | new User(3, "Wilf", "Crawford", DateTime.Parse("14/03/1975 16:56:40")), 17 | new User(4, "Eliza", "Bentley", DateTime.Parse("31/03/1996 19:41:40")), 18 | new User(5, "Steve", "Grenville", DateTime.Parse("19/08/1948 10:15:00")), 19 | new User(6, "Lainey", "Harmon", DateTime.Parse("17/04/1923 11:40:00")), 20 | new User(7, "Rosaline", "Kelsey", DateTime.Parse("25/10/1926 01:31:40")), 21 | new User(8, "Alvena", "Albinson", DateTime.Parse("26/01/1998 22:43:20")), 22 | new User(9, "Ivan", "Keighley", DateTime.Parse("03/06/1945 08:31:40")), 23 | new User(10, "Gordie", "Jack", DateTime.Parse("12/08/1930 22:46:40")), 24 | new User(11, "Delight", "Burnham", DateTime.Parse("15/11/1968 15:18:20")), 25 | new User(12, "Aleta", "Huddleson", DateTime.Parse("20/03/1976 02:18:20")), 26 | new User(13, "Hedley", "Lund", DateTime.Parse("25/12/1940 15:13:20")), 27 | new User(14, "Jeni", "Bristow", DateTime.Parse("18/01/1935 07:33:20")), 28 | new User(15, "Phyllis", "Waters", DateTime.Parse("17/03/1962 22:46:40")), 29 | new User(16, "Ryley", "Tuff", DateTime.Parse("30/03/1989 23:51:40")), 30 | new User(17, "Sharise", "Garrard", DateTime.Parse("18/06/1978 02:31:40")), 31 | new User(18, "Krysten", "Cannon", DateTime.Parse("04/02/1980 12:11:40")), 32 | new User(19, "Fulke", "Bullock", DateTime.Parse("02/03/2006 09:53:20")), 33 | new User(20, "Brand", "Chambers", DateTime.Parse("22/10/1992 18:51:40")) 34 | }; 35 | 36 | var roles = new List 37 | { 38 | new Role { Id = 1, Description = "Admin", Name = "admin" }, 39 | new Role { Id = 2, Description = "Moderator", Name = "Moderator" }, 40 | new Role { Id = 3, Description = "Staff", Name = "Staff" }, 41 | new Role { Id = 4, Description = "Infomation Techonogies", Name = "IT" }, 42 | new Role { Id = 5, Description = "Finance", Name = "finance" }, 43 | new Role { Id = 6, Description = "Human Resources", Name = "HR" } 44 | }; 45 | 46 | var userRoles = new List 47 | { 48 | new UserRole(1, 1, 4), new UserRole(2, 20, 1), new UserRole(3, 12, 5), 49 | new UserRole(4, 17, 1), new UserRole(5, 8, 3), new UserRole(6, 3, 3), 50 | new UserRole(7, 15, 1), new UserRole(8, 12, 6), new UserRole(9, 12, 4), 51 | new UserRole(10, 11, 1), new UserRole(11, 4, 3), new UserRole(12, 16, 2), 52 | new UserRole(13, 10, 4), new UserRole(14, 10, 6), new UserRole(15, 14, 4), 53 | new UserRole(16, 2, 2), new UserRole(17, 18, 6), new UserRole(18, 5, 5), 54 | new UserRole(19, 17, 6), new UserRole(20, 9, 1), new UserRole(21, 9, 4) 55 | }; 56 | 57 | 58 | foreach (var userRole in userRoles) 59 | { 60 | foreach (var role in roles) 61 | { 62 | if (role.Id == userRole.RoleId) 63 | { 64 | userRole.Role = role; 65 | } 66 | } 67 | } 68 | 69 | foreach (var user in users) 70 | { 71 | var list = new List(); 72 | foreach (var userRole in userRoles) 73 | { 74 | if (user.Id == userRole.UserId) 75 | { 76 | list.Add(userRole); 77 | } 78 | } 79 | 80 | user.UserRoles = list; 81 | } 82 | 83 | _users = users; 84 | } 85 | 86 | public static List GetUsers() 87 | { 88 | return new List(_users); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Source/Delve/Models/Expressions/QuerySanitizer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using Delve.Models.Validation; 6 | 7 | namespace Delve.Models.Expressions 8 | { 9 | internal static class QuerySanitizer 10 | { 11 | private static readonly Dictionary _operatorMaps = new Dictionary 12 | { 13 | { "==", QueryOperator.Equal }, 14 | { "==*", QueryOperator.EqualInsensitive }, 15 | { "!=", QueryOperator.NotEqual }, 16 | { "!=*", QueryOperator.NotEqualInsensitive }, 17 | { ">", QueryOperator.GreaterThan }, 18 | { "<", QueryOperator.LessThan}, 19 | { ">=", QueryOperator.GreaterThanOrEqual }, 20 | { "<=", QueryOperator.LessThanOrEqual }, 21 | { "?", QueryOperator.Contains }, 22 | { "?*", QueryOperator.ContainsInsensitive }, 23 | { "^", QueryOperator.StartsWith }, 24 | { "^*", QueryOperator.StartsWithInsensitive }, 25 | { "$", QueryOperator.EndsWith }, 26 | { "$*", QueryOperator.EndsWithInsensitive } 27 | }; 28 | 29 | public static string GetKey(ValidationType type, string query) 30 | { 31 | query = query.Trim(); 32 | 33 | switch (type) 34 | { 35 | case ValidationType.Select: 36 | { 37 | return query; 38 | } 39 | case ValidationType.OrderBy: 40 | { 41 | return query[0] == '-' ? query.Substring(1, query.Length - 1) : query; 42 | } 43 | case ValidationType.Filter: 44 | { 45 | return GetOperands(query).o1; 46 | } 47 | case ValidationType.Expand: 48 | { 49 | return query; 50 | } 51 | default: 52 | { 53 | throw new ArgumentOutOfRangeException(nameof(type), type, null); 54 | } 55 | } 56 | } 57 | 58 | public static string[] GetFilterValues(string query) 59 | { 60 | return GetOperands(query).o2.Split('|').Select(property => property.Trim()).ToArray(); 61 | } 62 | 63 | public static string[] GetExpandValues(string query) 64 | { 65 | return query.Split('>').Select(property => property.Trim()).ToArray(); 66 | } 67 | 68 | public static string GetFilterSymbol(QueryOperator op) 69 | { 70 | return _operatorMaps.FirstOrDefault(k => k.Value == op).Key; 71 | } 72 | 73 | public static QueryOperator GetFilterOperator(string query) 74 | { 75 | if (query == string.Empty) 76 | { 77 | throw new InvalidQueryException("Please specify a key for the filter operation."); 78 | } 79 | 80 | foreach (var op in _operatorMaps.Reverse()) 81 | { 82 | if (query.Contains(op.Key)) 83 | { 84 | return op.Value; 85 | } 86 | } 87 | throw new InvalidQueryException($"'{query}' does not contain a valid {nameof(QueryOperator)}."); 88 | } 89 | 90 | private static (string o1, string o2) GetOperands(string str) 91 | { 92 | bool opFound = false; 93 | var operands = (string.Empty, string.Empty); 94 | 95 | foreach (var op in _operatorMaps) 96 | { 97 | int index = str.IndexOf(op.Key, StringComparison.Ordinal); 98 | if (index == -1) { continue; } 99 | opFound = true; 100 | 101 | operands.Item1 = str.Substring(0, index); 102 | operands.Item2 = str.Substring(index + op.Key.Length, str.Length - index - op.Key.Length); 103 | } 104 | 105 | if (!opFound) 106 | { 107 | if (str.Count(c => c == '=') == 1) 108 | { 109 | throw new InvalidQueryException("Unknown filter operator '='. Did you mean '=='?"); 110 | } 111 | 112 | throw new InvalidQueryException($"'{str}' does not contain a valid {nameof(QueryOperator)}."); 113 | } 114 | 115 | if (operands.Item1 == string.Empty || operands.Item2 == string.Empty) 116 | { 117 | throw new InvalidQueryException($"Filter: '{str}' is missing an operand."); 118 | } 119 | 120 | return operands; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Demo/Delve.Demo/Persistence/Repository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Threading.Tasks; 6 | using Microsoft.EntityFrameworkCore; 7 | 8 | using Delve.Models; 9 | 10 | namespace Delve.Demo.Persistence 11 | { 12 | public class Repository : IRepository where T : class 13 | { 14 | protected DbContext Context { get; } 15 | 16 | public Repository(DbContext context) 17 | { 18 | Context = context; 19 | } 20 | 21 | public T GetById(int id) 22 | { 23 | return Context.Set().Find(id); 24 | } 25 | 26 | public async Task GetByIdAsync(int id) 27 | { 28 | return await Context.Set().FindAsync(id); 29 | } 30 | 31 | public IEnumerable Get(Expression> predicate = null, 32 | Func, IOrderedQueryable> orderBy = null, 33 | string includes = null, 34 | int? skip = null, 35 | int? take = null) 36 | { 37 | return BuildQueryable(predicate, orderBy, includes, skip, take).ToList(); 38 | } 39 | 40 | public async Task> GetAsync(Expression> predicate = null, 41 | Func, IOrderedQueryable> orderBy = null, 42 | string includes = null, 43 | int? skip = null, 44 | int? take = null) 45 | { 46 | return await BuildQueryable(predicate, orderBy, includes, skip, take).ToListAsync(); 47 | } 48 | 49 | public IPagedResult Get(IResourceParameter parameters) 50 | { 51 | var collection = Context.Set().ApplyFilters(parameters); 52 | return collection.ToPagedResult(parameters); 53 | } 54 | 55 | public async Task> GetAsync(IResourceParameter parameters) 56 | { 57 | var collection = Context.Set().ApplyIncludes((q, i) => q.Include(i), parameters).ApplyFilters(parameters).ApplyOrderBy(parameters); 58 | return await collection.ToPagedResultAsync(async q => await q.CountAsync(), async q => await q.ToListAsync(), parameters); 59 | } 60 | 61 | public T SingleOrDefault(Expression> predicate) 62 | { 63 | return Context.Set().SingleOrDefault(predicate); 64 | } 65 | 66 | public async Task SingleOrDefaultAsync(Expression> predicate) 67 | { 68 | return await Context.Set().SingleOrDefaultAsync(predicate); 69 | } 70 | 71 | public int GetCount(Expression> predicate = null) 72 | { 73 | return BuildQueryable(predicate).Count(); 74 | } 75 | 76 | public async Task GetCountAsync(Expression> predicate = null) 77 | { 78 | return await BuildQueryable(predicate).CountAsync(); 79 | } 80 | 81 | public bool GetExists(Expression> predicate = null) 82 | { 83 | return BuildQueryable(predicate).Any(); 84 | } 85 | 86 | public async Task GetExistsAsync(Expression> predicate = null) 87 | { 88 | return await BuildQueryable(predicate).AnyAsync(); 89 | } 90 | 91 | public void Add(T entity) 92 | { 93 | Context.Set().Add(entity); 94 | } 95 | 96 | public void AddRange(IEnumerable entities) 97 | { 98 | Context.Set().AddRange(entities); 99 | } 100 | 101 | public void Remove(T entity) 102 | { 103 | Context.Set().Remove(entity); 104 | } 105 | 106 | public void RemoveRange(IEnumerable entities) 107 | { 108 | Context.Set().RemoveRange(entities); 109 | } 110 | 111 | protected virtual IQueryable BuildQueryable( 112 | Expression> predicate = null, 113 | Func, IOrderedQueryable> orderBy = null, 114 | string includes = null, 115 | int? skip = null, 116 | int? take = null) 117 | { 118 | includes = includes ?? string.Empty; 119 | IQueryable query = Context.Set(); 120 | 121 | if (predicate != null) { query = query.Where(predicate); } 122 | 123 | query = includes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) 124 | .Aggregate(query, (current, include) => current.Include(include)); 125 | 126 | if (orderBy != null) { query = orderBy(query); } 127 | 128 | if (skip.HasValue) { query = query.Skip(skip.Value); } 129 | if (take.HasValue) { query = query.Take(take.Value); } 130 | 131 | return query; 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Demo/Delve.Demo/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using AutoMapper; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.EntityFrameworkCore; 9 | 10 | using Delve.Demo.Persistence; 11 | using Delve.Demo.Models; 12 | using Delve.Models.Validation; 13 | using Delve.AspNetCore; 14 | using Microsoft.EntityFrameworkCore.Extensions.Internal; 15 | 16 | namespace Delve.Demo 17 | { 18 | public class Startup 19 | { 20 | public void ConfigureServices(IServiceCollection services) 21 | { 22 | //Add Delve to this Mvc Project 23 | services.AddMvc().AddDelve(options => 24 | { 25 | options.IgnoreNullOnSerilazation = false; 26 | options.OmitHostOnPaginationLinks = true; 27 | }); 28 | 29 | var provider = new ServiceCollection() 30 | .AddEntityFrameworkInMemoryDatabase().BuildServiceProvider(); 31 | 32 | services.AddDbContext(options => 33 | { 34 | options.UseInMemoryDatabase("DelveDemo"); 35 | options.UseInternalServiceProvider(provider); 36 | }); 37 | 38 | services.AddTransient(); 39 | 40 | //Add the QueryValidator for User 41 | services.AddTransient, UserQueryValidator>(); 42 | 43 | using (var scope = services.BuildServiceProvider().CreateScope()) 44 | { 45 | var context = scope.ServiceProvider.GetService(); 46 | 47 | if (context.Database.EnsureCreated()) 48 | { 49 | context.EnsureSeeded(); 50 | } 51 | } 52 | 53 | services.AddAutoMapper(typeof(Startup)); 54 | } 55 | 56 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 57 | { 58 | if (env.IsDevelopment()) 59 | { 60 | app.UseDeveloperExceptionPage(); 61 | } 62 | 63 | app.UseMvc(); 64 | } 65 | } 66 | 67 | public static class ContextExtensions 68 | { 69 | public static void EnsureSeeded(this UserManagerContext context) 70 | { 71 | var users = new List 72 | { 73 | new User("Veronica", "Fear", DateTime.Parse("23/01/1946 21:01:40")), 74 | new User("Isabelle", "Bishop", DateTime.Parse("16/04/1968 18:00:00")), 75 | new User("Wilf", "Crawford", DateTime.Parse("14/03/1975 16:56:40")), 76 | new User("Eliza", "Bentley", DateTime.Parse("31/03/1996 19:41:40")), 77 | new User("Steve", "Grenville", DateTime.Parse("19/08/1948 10:15:00")), 78 | new User("Lainey", "Harmon", DateTime.Parse("17/04/1923 11:40:00")), 79 | new User("Rosaline", "Kelsey", DateTime.Parse("25/10/1926 01:31:40")), 80 | new User("Alvena", "Albinson", DateTime.Parse("26/01/1998 22:43:20")), 81 | new User("Ivan", "Keighley", DateTime.Parse("03/06/1945 08:31:40")), 82 | new User("Gordie", "Jack", DateTime.Parse("12/08/1930 22:46:40")), 83 | new User("Delight", "Burnham", DateTime.Parse("15/11/1968 15:18:20")), 84 | new User("Aleta", "Huddleson", DateTime.Parse("20/03/1976 02:18:20")), 85 | new User("Hedley", "Lund", DateTime.Parse("25/12/1940 15:13:20")), 86 | new User("Jeni", "Bristow", DateTime.Parse("18/01/1935 07:33:20")), 87 | new User("Phyllis", "Waters", DateTime.Parse("17/03/1962 22:46:40")), 88 | new User("Ryley", "Tuff", DateTime.Parse("30/03/1989 23:51:40")), 89 | new User("Sharise", "Garrard", DateTime.Parse("18/06/1978 02:31:40")), 90 | new User("Krysten", "Cannon", DateTime.Parse("04/02/1980 12:11:40")), 91 | new User("Fulke", "Bullock", DateTime.Parse("02/03/2006 09:53:20")), 92 | new User("Brand", "Chambers", DateTime.Parse("22/10/1992 18:51:40")) 93 | }; 94 | context.Users.AddRange(users); 95 | 96 | var roles = new List 97 | { 98 | new Role {Description = "Admin", Name = "admin"}, 99 | new Role {Description = "Moderator", Name = "Moderator"}, 100 | new Role {Description = "Staff", Name = "Staff"}, 101 | new Role {Description = "Information Technologies", Name = "IT"}, 102 | new Role {Description = "Finance", Name = "finance"}, 103 | new Role {Description = "Human Resources", Name = "HR"} 104 | }; 105 | context.Roles.AddRange(roles); 106 | 107 | var userRoles = new List 108 | { 109 | new UserRole(1, 4), new UserRole(20, 1), new UserRole(12, 5), 110 | new UserRole(17, 1), new UserRole(8, 3), new UserRole(3, 3), 111 | new UserRole(15, 1), new UserRole(12, 6), new UserRole(12, 4), 112 | new UserRole(11, 1), new UserRole(4, 3), new UserRole(16, 2), 113 | new UserRole(10, 4), new UserRole(10, 6), new UserRole(14, 4), 114 | new UserRole(2, 2), new UserRole(18, 6), new UserRole(5, 5), 115 | new UserRole(17, 6), new UserRole(9, 1), new UserRole(9, 4) 116 | }; 117 | context.UserRoles.AddRange(userRoles); 118 | context.SaveChanges(); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Source/Delve/Models/ResourceParameter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Dynamic; 4 | using System.Linq; 5 | 6 | using Delve.Models.Expressions; 7 | using Delve.Models.Validation; 8 | 9 | namespace Delve.Models 10 | { 11 | internal class ResourceParameter : IResourceParameter, IInternalResourceParameter 12 | { 13 | private int _pageNumber = 1; 14 | public int PageNumber 15 | { 16 | get { return _pageNumber; } 17 | set 18 | { 19 | if (value >= 1) { _pageNumber = value; } 20 | } 21 | } 22 | 23 | private int _pageSize = ResourceParameterOptions.DefaultPageSize; 24 | public int PageSize 25 | { 26 | get { return _pageSize; } 27 | set 28 | { 29 | if (value <= ResourceParameterOptions.MaxPageSize && 30 | value > 0) 31 | { 32 | _pageSize = value; 33 | } 34 | } 35 | } 36 | 37 | private IQueryConfiguration _configuration; 38 | internal List> Filter { get; private set; } 39 | internal List> OrderBy { get; private set; } 40 | internal List> Select { get; private set; } 41 | internal List> Expand { get; private set; } 42 | 43 | public void ApplyParameters(IQueryValidator validator, string filter, string orderBy, string select, string expand) 44 | { 45 | var internalValidator = (IInternalQueryValidator)validator; 46 | Filter = filter.ParseQuery(internalValidator, ValidationType.Filter); 47 | OrderBy = orderBy.ParseQuery(internalValidator, ValidationType.OrderBy); 48 | Select = select.ParseQuery(internalValidator, ValidationType.Select); 49 | Expand = expand.ParseQuery(internalValidator, ValidationType.Expand); 50 | _configuration = internalValidator.GetConfiguration(); 51 | 52 | foreach (var expression in Filter.Concat(OrderBy.Concat(Select.Concat(Expand)))) 53 | { 54 | expression.ValidateExpression(validator); 55 | } 56 | } 57 | 58 | public Dictionary GetPageHeader() 59 | { 60 | var dict = new Dictionary(); 61 | if (Filter.Any()) { dict.Add("filter", Filter.GetQuery()); } 62 | if (OrderBy.Any()) { dict.Add("orderBy", OrderBy.GetQuery()); } 63 | if (Select.Any()) { dict.Add("select", Select.GetQuery()); } 64 | if (Expand.Any()) { dict.Add("expand", Expand.GetQuery());} 65 | 66 | return dict; 67 | } 68 | 69 | IQueryable IInternalResourceParameter.ApplyOrderBy(IQueryable source) 70 | { 71 | if (OrderBy.Count == 0) 72 | { 73 | return _configuration.ApplyDefaultSorts(source); 74 | } 75 | 76 | bool thenBy = false; 77 | 78 | foreach (var sort in OrderBy) 79 | { 80 | source = sort.ApplySort(source, thenBy); 81 | thenBy = true; 82 | } 83 | 84 | return source; 85 | } 86 | 87 | IQueryable IInternalResourceParameter.ApplyExpand(IQueryable source, Func, string, IQueryable> include) 88 | { 89 | if (Expand.Count == 0) 90 | { 91 | return _configuration.ApplyDefaultExpands(source, include); 92 | } 93 | 94 | Expand.ForEach(e => source = e.ApplyExpand(source, include)); 95 | return source; 96 | } 97 | 98 | IList IInternalResourceParameter.ApplySelect(IEnumerable source) 99 | { 100 | if (Select.Count == 0) 101 | { 102 | return _configuration.ApplyDefaultSelects(source); 103 | } 104 | 105 | var test = Select.Select(x => x.GetPropertyMapping()); 106 | 107 | object applyFunc(T user, IEnumerable> func) 108 | { 109 | var t = new ExpandoObject() as IDictionary; 110 | foreach (var f in func) 111 | { 112 | var elements = f(user); 113 | t.Add(elements.Item1, elements.Item2); 114 | } 115 | 116 | return (ExpandoObject)t; 117 | } 118 | 119 | return source.Select(x => applyFunc(x, test)).ToList(); 120 | } 121 | 122 | IQueryable IInternalResourceParameter.ApplyFilters(IQueryable source) 123 | { 124 | if (Filter.Count == 0) 125 | { 126 | return _configuration.ApplyDefaultFilters(source); 127 | } 128 | 129 | Filter.ForEach(f => source = f.ApplyFilter(source)); 130 | return source; 131 | } 132 | } 133 | 134 | public static class ResourceParameterOptions 135 | { 136 | public static int MaxPageSize { get; set; } = 25; 137 | public static int DefaultPageSize { get; set; } = 10; 138 | public static bool IgnoreNullOnSerialization { get; set; } = true; 139 | public static bool OmitHostOnPaginationLinks { get; set; } = false; 140 | 141 | public static void ApplyConfig(DelveOptions options) 142 | { 143 | MaxPageSize = options.MaxPageSize; 144 | DefaultPageSize = options.DefaultPageSize; 145 | IgnoreNullOnSerialization = options.IgnoreNullOnSerilazation; 146 | OmitHostOnPaginationLinks = options.OmitHostOnPaginationLinks; 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.pch 68 | *.pdb 69 | *.pgc 70 | *.pgd 71 | *.rsp 72 | *.sbr 73 | *.tlb 74 | *.tli 75 | *.tlh 76 | *.tmp 77 | *.tmp_proj 78 | *.log 79 | *.vspscc 80 | *.vssscc 81 | .builds 82 | *.pidb 83 | *.svclog 84 | *.scc 85 | 86 | # Chutzpah Test files 87 | _Chutzpah* 88 | 89 | # Visual C++ cache files 90 | ipch/ 91 | *.aps 92 | *.ncb 93 | *.opendb 94 | *.opensdf 95 | *.sdf 96 | *.cachefile 97 | *.VC.db 98 | *.VC.VC.opendb 99 | 100 | # Visual Studio profiler 101 | *.psess 102 | *.vsp 103 | *.vspx 104 | *.sap 105 | 106 | # Visual Studio Trace Files 107 | *.e2e 108 | 109 | # TFS 2012 Local Workspace 110 | $tf/ 111 | 112 | # Guidance Automation Toolkit 113 | *.gpState 114 | 115 | # ReSharper is a .NET coding add-in 116 | _ReSharper*/ 117 | *.[Rr]e[Ss]harper 118 | *.DotSettings.user 119 | 120 | # JustCode is a .NET coding add-in 121 | .JustCode 122 | 123 | # TeamCity is a build add-in 124 | _TeamCity* 125 | 126 | # DotCover is a Code Coverage Tool 127 | *.dotCover 128 | 129 | # AxoCover is a Code Coverage Tool 130 | .axoCover/* 131 | !.axoCover/settings.json 132 | 133 | # Visual Studio code coverage results 134 | *.coverage 135 | *.coveragexml 136 | 137 | # NCrunch 138 | _NCrunch_* 139 | .*crunch*.local.xml 140 | nCrunchTemp_* 141 | 142 | # MightyMoose 143 | *.mm.* 144 | AutoTest.Net/ 145 | 146 | # Web workbench (sass) 147 | .sass-cache/ 148 | 149 | # Installshield output folder 150 | [Ee]xpress/ 151 | 152 | # DocProject is a documentation generator add-in 153 | DocProject/buildhelp/ 154 | DocProject/Help/*.HxT 155 | DocProject/Help/*.HxC 156 | DocProject/Help/*.hhc 157 | DocProject/Help/*.hhk 158 | DocProject/Help/*.hhp 159 | DocProject/Help/Html2 160 | DocProject/Help/html 161 | 162 | # Click-Once directory 163 | publish/ 164 | 165 | # Publish Web Output 166 | *.[Pp]ublish.xml 167 | *.azurePubxml 168 | # Note: Comment the next line if you want to checkin your web deploy settings, 169 | # but database connection strings (with potential passwords) will be unencrypted 170 | *.pubxml 171 | *.publishproj 172 | 173 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 174 | # checkin your Azure Web App publish settings, but sensitive information contained 175 | # in these scripts will be unencrypted 176 | PublishScripts/ 177 | 178 | # NuGet Packages 179 | *.nupkg 180 | # The packages folder can be ignored because of Package Restore 181 | **/[Pp]ackages/* 182 | # except build/, which is used as an MSBuild target. 183 | !**/[Pp]ackages/build/ 184 | # Uncomment if necessary however generally it will be regenerated when needed 185 | #!**/[Pp]ackages/repositories.config 186 | # NuGet v3's project.json files produces more ignorable files 187 | *.nuget.props 188 | *.nuget.targets 189 | 190 | # Microsoft Azure Build Output 191 | csx/ 192 | *.build.csdef 193 | 194 | # Microsoft Azure Emulator 195 | ecf/ 196 | rcf/ 197 | 198 | # Windows Store app package directories and files 199 | AppPackages/ 200 | BundleArtifacts/ 201 | Package.StoreAssociation.xml 202 | _pkginfo.txt 203 | *.appx 204 | 205 | # Visual Studio cache files 206 | # files ending in .cache can be ignored 207 | *.[Cc]ache 208 | # but keep track of directories ending in .cache 209 | !*.[Cc]ache/ 210 | 211 | # Others 212 | ClientBin/ 213 | ~$* 214 | *~ 215 | *.dbmdl 216 | *.dbproj.schemaview 217 | *.jfm 218 | *.pfx 219 | *.publishsettings 220 | orleans.codegen.cs 221 | 222 | # Including strong name files can present a security risk 223 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 224 | #*.snk 225 | 226 | # Since there are multiple workflows, uncomment next line to ignore bower_components 227 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 228 | #bower_components/ 229 | 230 | # RIA/Silverlight projects 231 | Generated_Code/ 232 | 233 | # Backup & report files from converting an old project file 234 | # to a newer Visual Studio version. Backup files are not needed, 235 | # because we have git ;-) 236 | _UpgradeReport_Files/ 237 | Backup*/ 238 | UpgradeLog*.XML 239 | UpgradeLog*.htm 240 | ServiceFabricBackup/ 241 | 242 | # SQL Server files 243 | *.mdf 244 | *.ldf 245 | *.ndf 246 | 247 | # Business Intelligence projects 248 | *.rdl.data 249 | *.bim.layout 250 | *.bim_*.settings 251 | 252 | # Microsoft Fakes 253 | FakesAssemblies/ 254 | 255 | # GhostDoc plugin setting file 256 | *.GhostDoc.xml 257 | 258 | # Node.js Tools for Visual Studio 259 | .ntvs_analysis.dat 260 | node_modules/ 261 | 262 | # TypeScript v1 declaration files 263 | typings/ 264 | 265 | # Visual Studio 6 build log 266 | *.plg 267 | 268 | # Visual Studio 6 workspace options file 269 | *.opt 270 | 271 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 272 | *.vbw 273 | 274 | # Visual Studio LightSwitch build output 275 | **/*.HTMLClient/GeneratedArtifacts 276 | **/*.DesktopClient/GeneratedArtifacts 277 | **/*.DesktopClient/ModelManifest.xml 278 | **/*.Server/GeneratedArtifacts 279 | **/*.Server/ModelManifest.xml 280 | _Pvt_Extensions 281 | 282 | # Paket dependency manager 283 | .paket/paket.exe 284 | paket-files/ 285 | 286 | # FAKE - F# Make 287 | .fake/ 288 | 289 | # JetBrains Rider 290 | .idea/ 291 | *.sln.iml 292 | 293 | # CodeRush 294 | .cr/ 295 | 296 | # Python Tools for Visual Studio (PTVS) 297 | __pycache__/ 298 | *.pyc 299 | 300 | # Cake - Uncomment if you are using it 301 | # tools/** 302 | # !tools/packages.config 303 | 304 | # Tabs Studio 305 | *.tss 306 | 307 | # Telerik's JustMock configuration file 308 | *.jmconfig 309 | 310 | # BizTalk build output 311 | *.btp.cs 312 | *.btm.cs 313 | *.odx.cs 314 | *.xsd.cs 315 | 316 | # OpenCover UI analysis results 317 | OpenCover/ 318 | 319 | # Azure Stream Analytics local run output 320 | ASALocalRun/ 321 | 322 | # MSBuild Binary and Structured Log 323 | *.binlog 324 | -------------------------------------------------------------------------------- /Source/Delve/Models/Validation/AbstractQueryValidator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | using System.Text.RegularExpressions; 7 | 8 | using Delve.Models.Expressions; 9 | 10 | namespace Delve.Models.Validation 11 | { 12 | public class AbstractQueryValidator : IQueryValidator, IInternalQueryValidator 13 | { 14 | private readonly IDictionary _validationRules = 15 | new Dictionary(StringComparer.CurrentCultureIgnoreCase); 16 | 17 | private readonly IQueryConfiguration _defaultConfig = new QueryConfiguration(); 18 | 19 | private void AddRule(string key, IValidationRule rule) 20 | { 21 | ValidatorHelpers.CheckTypeValid(typeof(TResult), rule.ValidationType, key); 22 | 23 | if (!Regex.IsMatch(key, @"^[a-zA-Z0-9_.]+$")) 24 | { 25 | throw new InvalidValidationBuilderException($"Key: '{key}' must only contain numbers, " + 26 | $"letters, dots and underscores."); 27 | } 28 | 29 | if (!_validationRules.ContainsKey(key)) 30 | { 31 | _validationRules.Add(key, new ValidationRules(rule, key)); 32 | } 33 | 34 | else 35 | { 36 | _validationRules[key].AddRule(rule, key); 37 | } 38 | } 39 | 40 | protected void AddDefaultFilter(Expression> expression) 41 | { 42 | _defaultConfig.AddDefaultFilter(expression); 43 | } 44 | 45 | protected void AddDefaultSort(Expression> exp, bool descending) 46 | { 47 | _defaultConfig.AddDefaultSort(exp, descending); 48 | } 49 | 50 | protected void AddDefaultSelect(string key, Expression> exp) 51 | { 52 | _defaultConfig.AddDefaultSelect(key, exp); 53 | } 54 | 55 | protected void AddDefaultExpand(Expression> exp) 56 | { 57 | _defaultConfig.AddDefaultExpand(exp); 58 | } 59 | 60 | protected void CanFilter(string key, Expression> exp) 61 | { 62 | AddRule(key, new ValidationRule(exp, ValidationType.Filter)); 63 | } 64 | 65 | protected void CanSelect(string key, Expression> exp) 66 | { 67 | AddRule(key, new ValidationRule(exp, ValidationType.Select)); 68 | } 69 | 70 | protected void CanOrder(string key, Expression> exp) 71 | { 72 | AddRule(key, new ValidationRule(exp, ValidationType.OrderBy)); 73 | } 74 | 75 | protected void CanExpand(Expression> exp) where TResult : class 76 | { 77 | AddRule(ValidatorHelpers.GetExpandString(exp.ToString()), 78 | new ValidationRule(exp, ValidationType.Expand)); 79 | } 80 | 81 | protected void AllowAll(string key, Expression> exp) 82 | { 83 | CanFilter(key, exp); 84 | CanSelect(key, exp); 85 | CanOrder(key, exp); 86 | } 87 | 88 | private void ValidateKey(string key, ValidationType type) 89 | { 90 | if (type != ValidationType.Expand) 91 | { 92 | if (!_validationRules.ContainsKey(key)) 93 | { 94 | throw new InvalidQueryException($"Invalid '{type}' " + 95 | $"propertykey: '{key}' in query."); 96 | } 97 | } 98 | 99 | else 100 | { 101 | if (GetExpandKey(key) == null) { throw new InvalidQueryException($"Key '{key}' is not defined."); } 102 | } 103 | } 104 | 105 | IValidationRule IInternalQueryValidator.ValidateExpression(IExpression expression, ValidationType type) 106 | { 107 | ValidateKey(expression.Key, type); 108 | return _validationRules[type == ValidationType.Expand ? GetExpandKey(expression.Key) : expression.Key] 109 | .ValidateExpression(type, expression); 110 | } 111 | 112 | Type IInternalQueryValidator.GetResultType(string key, ValidationType type) 113 | { 114 | ValidateKey(key, type); 115 | return _validationRules[type == ValidationType.Expand ? GetExpandKey(key) : key].GetResultType(type, key); 116 | } 117 | 118 | IQueryConfiguration IInternalQueryValidator.GetConfiguration() 119 | { 120 | return _defaultConfig; 121 | } 122 | 123 | private string GetExpandKey(string key) 124 | { 125 | if (key.EndsWith(".")) { return null; } 126 | return _validationRules 127 | .Where(pair => pair.Key.IndexOf(key, StringComparison.CurrentCultureIgnoreCase) == 0).Select(x => x.Key) 128 | .FirstOrDefault(); 129 | } 130 | } 131 | 132 | internal static class ValidatorHelpers 133 | { 134 | private static readonly Type[] _validNonPrimitive = 135 | { 136 | typeof(string), typeof(DateTime), typeof(TimeSpan) 137 | }; 138 | 139 | public static void CheckTypeValid(Type type, ValidationType valType, string key) 140 | { 141 | switch (valType) 142 | { 143 | case ValidationType.Select: { } break; 144 | case ValidationType.OrderBy: { } break; 145 | 146 | case ValidationType.Filter: 147 | { 148 | if (!type.IsPrimitive && !_validNonPrimitive.Contains(type) && !typeof(IEnumerable).IsAssignableFrom(type)) 149 | { 150 | throw new InvalidValidationBuilderException 151 | ($"Registered filter property: '{key}' can not be of type '{type}'."); 152 | } 153 | } break; 154 | 155 | case ValidationType.Expand: 156 | { 157 | if (type.IsValueType) 158 | { 159 | throw new InvalidValidationBuilderException 160 | ($"Registered property: '{key}' can not be of type: '{type}'. Expand is limited to classes."); 161 | } 162 | } break; 163 | 164 | default: 165 | { 166 | throw new ArgumentOutOfRangeException(nameof(valType), valType, null); 167 | } 168 | } 169 | } 170 | 171 | public static string GetExpandString(string expression) 172 | { 173 | var split = expression.Split(new[] { "=>" }, StringSplitOptions.None).ToList(); 174 | 175 | var properties = new List(); 176 | 177 | foreach(var str in split) 178 | { 179 | if(!str.Contains('.')) { continue; } 180 | var propSplit = str.Split('.'); 181 | for(int i = 1; i < propSplit.Length; i++) 182 | { 183 | if (!propSplit[i].StartsWith("Select")) 184 | { 185 | properties.Add(propSplit[i].Trim(')').Trim('(')); 186 | } 187 | } 188 | } 189 | 190 | return properties.Aggregate((x, y) => x + '.' + y); 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Delve 2 | 3 | Delve is a simple framework for ASP.NET Core that adds easy pagination, filtering, sorting, selecting and expanding to an MVC project without being tightly coupled to an ORM. 4 | 5 | Core: 6 | [![Delve](https://img.shields.io/nuget/vpre/Delve.svg)](https://www.nuget.org/packages/Delve/0.9.6-alpha) 7 | AspNetCore Integration: 8 | [![Delve.AspNetCore](https://img.shields.io/nuget/vpre/Delve.AspNetCore.svg)](https://www.nuget.org/packages/Delve.AspNetCore/0.9.6-alpha) 9 | 10 | ## Features 11 | * Pagination, Sorting, Filtering, Selecting and Expanding (i.e. EFCore Include) capabilities. 12 | * Easy ASP.Net Core Mvc integration 13 | * Auto-Generation of X-Pagination header 14 | * Optional default definitions for filtering, sorting and selecting 15 | * Automatically returns a 400 Bad Request on malformed query 16 | * Virtual Properties allow for extending API functionality without cluttering domain classes 17 | * Not ORM dependant (Any ORM that uses IQueryable should work) 18 | * Async pagination is supported 19 | 20 | ## Demo 21 | There is a Demo Project in the Demo/ directory, which demonstrates all of Delve's functionality. 22 | 23 | ## Usage 24 | 25 | ### 1. Add Delve to the MVC Project 26 | Just append **.AddDelve()** to your **.AddMvc()** call in your **Startup.cs** file. 27 | ```csharp 28 | public void ConfigureServices(IServiceCollection services) 29 | { 30 | services.AddMvc().AddDelve(); 31 | } 32 | ``` 33 | 34 | If you wish to change the default configuration of **Delve** you can add **DelveOptions** as a parameter to **.AddDelve()**. 35 | 36 | ```csharp 37 | services.AddMvc().AddDelve(options => options.MaxPageSize = 15); 38 | ``` 39 | 40 | ### 2. Add a QueryValidator to your Domain class 41 | By deriving from **AbstractQueryValidator\** you can precisely define what is allowed to by queried by the user. 42 | By adding virtual properties you do not have to create a new property for something you only want to expose to the API (See examples below). 43 | 44 | ```csharp 45 | using Delve.Validation; 46 | 47 | public class User 48 | { 49 | public int Id { get; set; } 50 | public string FirstName { get; set; } 51 | public string LastName { get; set; } 52 | public DateTime DateOfBirth { get; set; } 53 | //"UserRoles" not shown here for brevity, check the demo project. 54 | public IEnumerable { get; set; } 55 | } 56 | 57 | public class UserQueryValidator : AbstractQueryValidator 58 | { 59 | public UserQueryValidator() 60 | { 61 | //Adds Id and virtual property "Name" as default selects 62 | AddDefaultSelect("Id", u => u.Id); 63 | AddDefaultSelect("Name", u => u.LastName + " " + u.FirstName); 64 | 65 | //Adds virtual property as default sort 66 | AddDefaultSort(u => u.LastName + " " + u.LastName, true); 67 | //Adds default filter 68 | AddDefaultFilter(u => u.Id > 5); 69 | 70 | //Adds selecting/filtering/sorting with key="Id" for Id property 71 | CanSelect("Id", x => x.Id); 72 | CanFilter("Id", x => x.Id); 73 | CanOrder("Id", x => x.Id); 74 | 75 | //Adds a virtual property for the Age of the user calculated using the DateOfBirth 76 | //By using AllowAll() you can automatically add selecting/filtering/sorting for a property 77 | AllowAll("Age", x => Math.Round((DateTime.Now - x.DateOfBirth).TotalDays / 365, 2)); 78 | 79 | //Adds a virtual property with key="Name" for the combination of LastName and FirstName 80 | AllowAll("Name", x => x.LastName + "" + x.FirstName); 81 | 82 | //Allows you to use "Include" in ORM's like EFCore 83 | //Expanding syntax is EF6 like. Branch into an IEnumerable navigation-property with a .Select(). 84 | //All previous navigation-properties are automatically included. (in this case: both UserRoles and UserRoles.Role) 85 | //No need to do another CanExpand for the standalone UserRoles 86 | CanExpand(x => x.UserRoles.Select(ur => ur.Role); 87 | } 88 | } 89 | ``` 90 | 91 | Now you just have to register the validator in your services: 92 | ```csharp 93 | services.AddTransient, UserQueryValidator>(); 94 | ``` 95 | 96 | ### 3. Configure your Controller 97 | By simply adding a **IResourceParameter\** to the method signature Delve will automatically parse and validate the client request and upon an invalid request return a 400 BadRequest with a matching error message. 98 | 99 | ```csharp 100 | using Delve.Models; 101 | using Delve.AspNetCore; 102 | 103 | public class UserController : Controller 104 | { 105 | private readonly IUrlHelper _urlHelper; 106 | public UserController(IUrlHelper urlHelper) 107 | { 108 | _urlHelper = urlHelper; 109 | } 110 | 111 | public async Task GetUsers(IResourceParameter parameter) 112 | { 113 | var collection = Context.Set() 114 | //Applies expands to the IQueryable. Since delve isn't directly coupled to EFCore 115 | //you need to pass in the include method as a delegate. 116 | .ApplyIncludes((q, i) => q.Include(i), parameters) 117 | //Applies filters to the IQueryable 118 | .ApplyFilters(parameters) 119 | //Applies sorts to the IQueryable 120 | .ApplyOrderBy(parameters); 121 | 122 | //ToPagedResultAsync() will pull the matching IQueryable data from the database and applies pagination. 123 | //Since System.Linq doesn't provide a way to asynchronously Count() and ToList() 124 | //you will need to pass in the matching methods of your ORM for the pagination to work. 125 | //If you dont need async capabilities you can use ToPagedResult(). It will work without any delegates. 126 | var users = await collection.ToPagedResultAsync( 127 | 128 | async q => await q.CountAsync(), 129 | async q => await q.ToListAsync(), parameters); 130 | 131 | //Adds paginationheader to the response 132 | this.AddPaginationHelper(parameter, users, _urlHelper); 133 | 134 | return Ok(users.ShapeData(parameter)); 135 | } 136 | ``` 137 | 138 | ### 4. Send a request 139 | 140 | ### Filters: 141 | There are a couple of ways you can work with Delve's filters. 142 | By adding **filter=** to the query string you can filter on any in the QueryValidator defined virtual properties. 143 | You can add mulitple filters by separating these with commas (i.e. **filter=Id== 5, Name==John**). 144 | Furthermore you can define logical OR behaviour by separating the values of one filter with a '|' operator. 145 | This way you can check for a user called "John" or "Mary" (i.e. **filter=Name==John|Mary**). 146 | 147 | ### FilterOperators 148 | 149 | | Operator | Interpretation | Allowed Type 150 | |----------|-----------------------------|-------------- 151 | | `==` | Equal | object 152 | | `==*` | CaseInsensitive Equal | string 153 | | `!=` | NotEqual | object 154 | | `!=*` | CaseInsensitive NotEqual | string 155 | | `>` | GreaterThan | IComparable 156 | | `<` | LessThan | IComparable 157 | | `>=` | GreaterThanOrEqual | IComparable 158 | | `<=` | LessThanOrEqual | IComparable 159 | | `?` | Contains | string 160 | | `?*` | CaseInsensitive Contains | string 161 | | `^` | StartsWith | string 162 | | `^*` | CaseInsensitive StartsWith | string 163 | | `$` | EndsWith | string 164 | | `$*` | CaseInsensitive EndsWith | string 165 | 166 | ### Sorting 167 | 168 | Just as Filtering, Sorting is delimited by commas, though unlike with Filtering order of the sorts matter. 169 | Meaning, if you have **"(orderby=Name, -Age)"** in your query, it will first order by Name ascending and then by Age descending **(Linq equivalent: .OrderBy(x => x.Name).ThenByDescending(x => x.Age);)**. 170 | 171 | ### Selecting 172 | 173 | If your entity has numerous columns and as a consumer of the API you are only really interested in a couple of things you can save bandwith by using Select. 174 | Just like before selects are delimited by commas and allow you to select on any virtual property defined in the **QueryValidator**. 175 | 176 | ### Expanding 177 | 178 | If your Entity references another relation you can include that relation using the **expand** keyword. 179 | This will allow you to perform further operations on navigation properties. 180 | 181 | Example for the query validation definition: 182 | ```csharp 183 | CanExpand(x => x.UserRoles.Select(ur => ur.Role); 184 | ``` 185 | This will add a way to allow to include both **UserRoles** and **UserRoles.Role** in your query. 186 | Note: CanExpand does not require a key, it will automatically take the name of the properties as the individual keys and separate each layer of depth by a '.'. 187 | 188 | ##### Request Example: 189 | ``` 190 | GET /api/Users 191 | ?filter= Age>=20, Name$*Smith|Bullock 192 | &orderby= -Age, Name 193 | &select= Id, Name, Age 194 | &expand= UserRoles 195 | &pageNumber= 1 196 | &pageSize= 5 197 | ``` 198 | 199 | ##### X-Pagination Response Header: 200 | ``` 201 | { 202 | "currentPage":1, 203 | "pageSize":5, 204 | "totalPages":4, 205 | "totalCount":20, 206 | "previousPageLink":null, 207 | "nextPageLink": "{address}/api/Users?pageNumber=1 208 | &pageSize=10 209 | &filter=Age>=20,Name$*Smith|Bullock 210 | &orderby=-Age,Name 211 | &select=Id,Name,Age 212 | &expand=UserRoles" 213 | } 214 | ``` 215 | 216 | ## Licensing 217 | Delve is licensed under MIT. 218 | 219 | ## Contribution 220 | This project is still at an early stage in development so any contributions are welcomed! 221 | Even if it is just a suggestions/discussions about how to improve upon Delve! 222 | --------------------------------------------------------------------------------