From 428acd7558d77862f99709f9e79f1eef35f490fa Mon Sep 17 00:00:00 2001 From: ITDancer139 Date: Thu, 13 May 2021 14:04:18 +0200 Subject: [PATCH] * Migrate tests to xunit * Update sample project to dotnetcore3.1 * Use Sqlite in sample project to run it everywhere * Fix: Filter with escaped comma * Fix: Filter "null" does not work with Contains or StartsWith * Code cleanup: Adjust namespaces, adjust usings --- .gitignore | 8 +- .../Controllers/PostsController.cs | 4 +- .../Entities/ApplicationDbContext.cs | 2 +- {SieveTests => Sieve.Sample}/Entities/Post.cs | 4 +- .../20210513114647_Initial.Designer.cs | 52 +++ .../Migrations/20210513114647_Initial.cs | 17 +- .../ApplicationDbContextModelSnapshot.cs | 50 +++ {SieveTests => Sieve.Sample}/Program.cs | 2 +- .../Properties/launchSettings.json | 0 .../Services/ApplicationSieveProcessor.cs | 4 +- .../Services/SieveCustomFilterMethods.cs | 4 +- .../Services/SieveCustomSortMethods.cs | 4 +- Sieve.Sample/Sieve.Sample.csproj | 20 + Sieve.Sample/Startup.cs | 60 +++ .../appsettings.Development.json | 0 {SieveTests => Sieve.Sample}/appsettings.json | 0 {SieveTests => Sieve.Sample}/pyprofile.py | 0 Sieve.sln | 2 +- Sieve/Models/SieveModel.cs | 63 ++-- Sieve/Services/SieveProcessor.cs | 354 +++++++++--------- Sieve/Sieve.csproj | 31 +- .../20180127005347_Init.Designer.cs | 44 --- ...013323_AddDateLastViewedColumn.Designer.cs | 47 --- .../20180522013323_AddDateLastViewedColumn.cs | 25 -- .../ApplicationDbContextModelSnapshot.cs | 48 --- SieveTests/SieveTests.csproj | 29 -- SieveTests/Startup.cs | 67 ---- SieveUnitTests/Entities/Comment.cs | 3 + SieveUnitTests/General.cs | 326 ++++++++-------- SieveUnitTests/GeneralWithInterfaces.cs | 307 +++++++-------- SieveUnitTests/Mapper.cs | 36 +- SieveUnitTests/SieveUnitTests.csproj | 17 +- SieveUnitTests/StringFilterNullTests.cs | 148 ++++++++ 33 files changed, 966 insertions(+), 812 deletions(-) rename {SieveTests => Sieve.Sample}/Controllers/PostsController.cs (95%) rename {SieveTests => Sieve.Sample}/Entities/ApplicationDbContext.cs (91%) rename {SieveTests => Sieve.Sample}/Entities/Post.cs (92%) create mode 100644 Sieve.Sample/Migrations/20210513114647_Initial.Designer.cs rename SieveTests/Migrations/20180127005347_Init.cs => Sieve.Sample/Migrations/20210513114647_Initial.cs (70%) create mode 100644 Sieve.Sample/Migrations/ApplicationDbContextModelSnapshot.cs rename {SieveTests => Sieve.Sample}/Program.cs (94%) rename {SieveTests => Sieve.Sample}/Properties/launchSettings.json (100%) rename {SieveTests => Sieve.Sample}/Services/ApplicationSieveProcessor.cs (91%) rename {SieveTests => Sieve.Sample}/Services/SieveCustomFilterMethods.cs (82%) rename {SieveTests => Sieve.Sample}/Services/SieveCustomSortMethods.cs (87%) create mode 100644 Sieve.Sample/Sieve.Sample.csproj create mode 100644 Sieve.Sample/Startup.cs rename {SieveTests => Sieve.Sample}/appsettings.Development.json (100%) rename {SieveTests => Sieve.Sample}/appsettings.json (100%) rename {SieveTests => Sieve.Sample}/pyprofile.py (100%) delete mode 100644 SieveTests/Migrations/20180127005347_Init.Designer.cs delete mode 100644 SieveTests/Migrations/20180522013323_AddDateLastViewedColumn.Designer.cs delete mode 100644 SieveTests/Migrations/20180522013323_AddDateLastViewedColumn.cs delete mode 100644 SieveTests/Migrations/ApplicationDbContextModelSnapshot.cs delete mode 100644 SieveTests/SieveTests.csproj delete mode 100644 SieveTests/Startup.cs create mode 100644 SieveUnitTests/StringFilterNullTests.cs diff --git a/.gitignore b/.gitignore index da4d8f3..c365f8d 100644 --- a/.gitignore +++ b/.gitignore @@ -260,4 +260,10 @@ paket-files/ # Python Tools for Visual Studio (PTVS) __pycache__/ -*.pyc \ No newline at end of file +*.pyc + +# Nuke output +/output + +# Sample database +Sieve.Sample/Sieve.db diff --git a/SieveTests/Controllers/PostsController.cs b/Sieve.Sample/Controllers/PostsController.cs similarity index 95% rename from SieveTests/Controllers/PostsController.cs rename to Sieve.Sample/Controllers/PostsController.cs index 6ab09fa..3f142de 100644 --- a/SieveTests/Controllers/PostsController.cs +++ b/Sieve.Sample/Controllers/PostsController.cs @@ -2,10 +2,10 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Sieve.Models; +using Sieve.Sample.Entities; using Sieve.Services; -using SieveTests.Entities; -namespace SieveTests.Controllers +namespace Sieve.Sample.Controllers { [Route("api/[controller]/[action]")] public class PostsController : Controller diff --git a/SieveTests/Entities/ApplicationDbContext.cs b/Sieve.Sample/Entities/ApplicationDbContext.cs similarity index 91% rename from SieveTests/Entities/ApplicationDbContext.cs rename to Sieve.Sample/Entities/ApplicationDbContext.cs index 6c745e7..aa053e6 100644 --- a/SieveTests/Entities/ApplicationDbContext.cs +++ b/Sieve.Sample/Entities/ApplicationDbContext.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore; -namespace SieveTests.Entities +namespace Sieve.Sample.Entities { public class ApplicationDbContext : DbContext { diff --git a/SieveTests/Entities/Post.cs b/Sieve.Sample/Entities/Post.cs similarity index 92% rename from SieveTests/Entities/Post.cs rename to Sieve.Sample/Entities/Post.cs index e275c3c..25f6201 100644 --- a/SieveTests/Entities/Post.cs +++ b/Sieve.Sample/Entities/Post.cs @@ -2,14 +2,14 @@ using System.ComponentModel.DataAnnotations.Schema; using Sieve.Attributes; -namespace SieveTests.Entities +namespace Sieve.Sample.Entities { public class Post { public int Id { get; set; } [Sieve(CanFilter = true, CanSort = true)] - public string Title { get; set; } = Guid.NewGuid().ToString().Replace("-", string.Empty).Substring(0, 8); + public string Title { get; set; } = Guid.NewGuid().ToString().Replace("-", string.Empty)[..8]; [Sieve(CanFilter = true, CanSort = true)] public int LikeCount { get; set; } = new Random().Next(0, 1000); diff --git a/Sieve.Sample/Migrations/20210513114647_Initial.Designer.cs b/Sieve.Sample/Migrations/20210513114647_Initial.Designer.cs new file mode 100644 index 0000000..7a26181 --- /dev/null +++ b/Sieve.Sample/Migrations/20210513114647_Initial.Designer.cs @@ -0,0 +1,52 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Sieve.Sample.Entities; + +namespace Sieve.Sample.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20210513114647_Initial")] + partial class Initial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.14"); + + modelBuilder.Entity("Sieve.Sample.Entities.Post", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("CommentCount") + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastViewed") + .HasColumnType("datetime"); + + b.Property("LikeCount") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Posts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SieveTests/Migrations/20180127005347_Init.cs b/Sieve.Sample/Migrations/20210513114647_Initial.cs similarity index 70% rename from SieveTests/Migrations/20180127005347_Init.cs rename to Sieve.Sample/Migrations/20210513114647_Initial.cs index c69e330..276c0ad 100644 --- a/SieveTests/Migrations/20180127005347_Init.cs +++ b/Sieve.Sample/Migrations/20210513114647_Initial.cs @@ -1,10 +1,9 @@ using System; -using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; -namespace SieveTests.Migrations +namespace Sieve.Sample.Migrations { - public partial class Init : Migration + public partial class Initial : Migration { protected override void Up(MigrationBuilder migrationBuilder) { @@ -13,14 +12,18 @@ namespace SieveTests.Migrations columns: table => new { Id = table.Column(nullable: false) - .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + .Annotation("Sqlite:Autoincrement", true), + Title = table.Column(nullable: true), + LikeCount = table.Column(nullable: false), CommentCount = table.Column(nullable: false), DateCreated = table.Column(nullable: false), - LikeCount = table.Column(nullable: false), - Title = table.Column(nullable: true), + DateLastViewed = table.Column(type: "datetime", nullable: false), CategoryId = table.Column(nullable: true) }, - constraints: table => table.PrimaryKey("PK_Posts", x => x.Id)); + constraints: table => + { + table.PrimaryKey("PK_Posts", x => x.Id); + }); } protected override void Down(MigrationBuilder migrationBuilder) diff --git a/Sieve.Sample/Migrations/ApplicationDbContextModelSnapshot.cs b/Sieve.Sample/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..b33a790 --- /dev/null +++ b/Sieve.Sample/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,50 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Sieve.Sample.Entities; + +namespace Sieve.Sample.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.14"); + + modelBuilder.Entity("Sieve.Sample.Entities.Post", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("CommentCount") + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastViewed") + .HasColumnType("datetime"); + + b.Property("LikeCount") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Posts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SieveTests/Program.cs b/Sieve.Sample/Program.cs similarity index 94% rename from SieveTests/Program.cs rename to Sieve.Sample/Program.cs index 255e0c6..7b5dc68 100644 --- a/SieveTests/Program.cs +++ b/Sieve.Sample/Program.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; -namespace SieveTests +namespace Sieve.Sample { public static class Program { diff --git a/SieveTests/Properties/launchSettings.json b/Sieve.Sample/Properties/launchSettings.json similarity index 100% rename from SieveTests/Properties/launchSettings.json rename to Sieve.Sample/Properties/launchSettings.json diff --git a/SieveTests/Services/ApplicationSieveProcessor.cs b/Sieve.Sample/Services/ApplicationSieveProcessor.cs similarity index 91% rename from SieveTests/Services/ApplicationSieveProcessor.cs rename to Sieve.Sample/Services/ApplicationSieveProcessor.cs index 039352a..29157f5 100644 --- a/SieveTests/Services/ApplicationSieveProcessor.cs +++ b/Sieve.Sample/Services/ApplicationSieveProcessor.cs @@ -1,9 +1,9 @@ using Microsoft.Extensions.Options; using Sieve.Models; +using Sieve.Sample.Entities; using Sieve.Services; -using SieveTests.Entities; -namespace SieveTests.Services +namespace Sieve.Sample.Services { public class ApplicationSieveProcessor : SieveProcessor { diff --git a/SieveTests/Services/SieveCustomFilterMethods.cs b/Sieve.Sample/Services/SieveCustomFilterMethods.cs similarity index 82% rename from SieveTests/Services/SieveCustomFilterMethods.cs rename to Sieve.Sample/Services/SieveCustomFilterMethods.cs index 2a366ea..a2fc673 100644 --- a/SieveTests/Services/SieveCustomFilterMethods.cs +++ b/Sieve.Sample/Services/SieveCustomFilterMethods.cs @@ -1,8 +1,8 @@ using System.Linq; +using Sieve.Sample.Entities; using Sieve.Services; -using SieveTests.Entities; -namespace SieveTests.Services +namespace Sieve.Sample.Services { public class SieveCustomFilterMethods : ISieveCustomFilterMethods { diff --git a/SieveTests/Services/SieveCustomSortMethods.cs b/Sieve.Sample/Services/SieveCustomSortMethods.cs similarity index 87% rename from SieveTests/Services/SieveCustomSortMethods.cs rename to Sieve.Sample/Services/SieveCustomSortMethods.cs index 98f1244..5b89745 100644 --- a/SieveTests/Services/SieveCustomSortMethods.cs +++ b/Sieve.Sample/Services/SieveCustomSortMethods.cs @@ -1,8 +1,8 @@ using System.Linq; +using Sieve.Sample.Entities; using Sieve.Services; -using SieveTests.Entities; -namespace SieveTests.Services +namespace Sieve.Sample.Services { public class SieveCustomSortMethods : ISieveCustomSortMethods { diff --git a/Sieve.Sample/Sieve.Sample.csproj b/Sieve.Sample/Sieve.Sample.csproj new file mode 100644 index 0000000..8aca6b9 --- /dev/null +++ b/Sieve.Sample/Sieve.Sample.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp3.1 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/Sieve.Sample/Startup.cs b/Sieve.Sample/Startup.cs new file mode 100644 index 0000000..06c36b3 --- /dev/null +++ b/Sieve.Sample/Startup.cs @@ -0,0 +1,60 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Sieve.Models; +using Sieve.Sample.Entities; +using Sieve.Sample.Services; +using Sieve.Services; + +namespace Sieve.Sample +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddMvc(opts => + { + opts.EnableEndpointRouting = false; + }); + + services.AddDbContext(options => + options.UseSqlite("Data Source=.\\sieve.db")); + + services.Configure(Configuration.GetSection("Sieve")); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + PrepareDatabase(app); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseMvc(); + } + + private static void PrepareDatabase(IApplicationBuilder app) + { + using var scope = app.ApplicationServices.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + dbContext.Database.EnsureDeleted(); + dbContext.Database.Migrate(); + } + } +} diff --git a/SieveTests/appsettings.Development.json b/Sieve.Sample/appsettings.Development.json similarity index 100% rename from SieveTests/appsettings.Development.json rename to Sieve.Sample/appsettings.Development.json diff --git a/SieveTests/appsettings.json b/Sieve.Sample/appsettings.json similarity index 100% rename from SieveTests/appsettings.json rename to Sieve.Sample/appsettings.json diff --git a/SieveTests/pyprofile.py b/Sieve.Sample/pyprofile.py similarity index 100% rename from SieveTests/pyprofile.py rename to Sieve.Sample/pyprofile.py diff --git a/Sieve.sln b/Sieve.sln index acf6862..31a5359 100644 --- a/Sieve.sln +++ b/Sieve.sln @@ -5,7 +5,7 @@ VisualStudioVersion = 15.0.27130.2027 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sieve", "Sieve\Sieve.csproj", "{B32B8B33-94B0-40E3-8FE5-D54602222717}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SieveTests", "SieveTests\SieveTests.csproj", "{8043D264-42A0-4275-97A1-46400C02E37E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sieve.Sample", "Sieve.Sample\Sieve.Sample.csproj", "{8043D264-42A0-4275-97A1-46400C02E37E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SieveUnitTests", "SieveUnitTests\SieveUnitTests.csproj", "{21C3082D-F40E-457F-BE2E-AA099E19E199}" EndProject diff --git a/Sieve/Models/SieveModel.cs b/Sieve/Models/SieveModel.cs index a773392..a3f9818 100644 --- a/Sieve/Models/SieveModel.cs +++ b/Sieve/Models/SieveModel.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Runtime.Serialization; @@ -13,7 +14,15 @@ namespace Sieve.Models where TFilterTerm : IFilterTerm, new() where TSortTerm : ISortTerm, new() { + /// + /// Pattern used to split filters and sorts by comma. + /// private const string EscapedCommaPattern = @"(? + /// Escaped comma e.g. used in filter filter string. + /// + private const string EscapedComma = @"\,"; [DataMember] public string Filters { get; set; } @@ -34,15 +43,20 @@ namespace Sieve.Models var value = new List(); foreach (var filter in Regex.Split(Filters, EscapedCommaPattern)) { - if (string.IsNullOrWhiteSpace(filter)) continue; + if (string.IsNullOrWhiteSpace(filter)) + { + continue; + } + + var filterValue = filter.Replace(EscapedComma, ","); if (filter.StartsWith("(")) { - var filterOpAndVal = filter.Substring(filter.LastIndexOf(")") + 1); - var subfilters = filter.Replace(filterOpAndVal, "").Replace("(", "").Replace(")", ""); + var filterOpAndVal = filterValue[(filterValue.LastIndexOf(")", StringComparison.Ordinal) + 1)..]; + var subFilters = filterValue.Replace(filterOpAndVal, "").Replace("(", "").Replace(")", ""); var filterTerm = new TFilterTerm { - Filter = subfilters + filterOpAndVal + Filter = subFilters + filterOpAndVal }; value.Add(filterTerm); } @@ -50,7 +64,7 @@ namespace Sieve.Models { var filterTerm = new TFilterTerm { - Filter = filter + Filter = filterValue }; value.Add(filterTerm); } @@ -65,29 +79,28 @@ namespace Sieve.Models public List GetSortsParsed() { - if (Sorts != null) - { - var value = new List(); - foreach (var sort in Regex.Split(Sorts, EscapedCommaPattern)) - { - if (string.IsNullOrWhiteSpace(sort)) continue; - - var sortTerm = new TSortTerm() - { - Sort = sort - }; - if (!value.Any(s => s.Name == sortTerm.Name)) - { - value.Add(sortTerm); - } - } - return value; - } - else + if (Sorts == null) { return null; } + var value = new List(); + foreach (var sort in Regex.Split(Sorts, EscapedCommaPattern)) + { + if (string.IsNullOrWhiteSpace(sort)) continue; + + var sortTerm = new TSortTerm + { + Sort = sort + }; + + if (value.All(s => s.Name != sortTerm.Name)) + { + value.Add(sortTerm); + } + } + + return value; } } } diff --git a/Sieve/Services/SieveProcessor.cs b/Sieve/Services/SieveProcessor.cs index ab363fc..6496c7a 100644 --- a/Sieve/Services/SieveProcessor.cs +++ b/Sieve/Services/SieveProcessor.cs @@ -14,40 +14,50 @@ namespace Sieve.Services { public class SieveProcessor : SieveProcessor, ISieveProcessor { - public SieveProcessor(IOptions options) : base(options) + public SieveProcessor(IOptions options) + : base(options) { } - public SieveProcessor(IOptions options, ISieveCustomSortMethods customSortMethods) : base(options, customSortMethods) + public SieveProcessor(IOptions options, ISieveCustomSortMethods customSortMethods) + : base(options, customSortMethods) { } - public SieveProcessor(IOptions options, ISieveCustomFilterMethods customFilterMethods) : base(options, customFilterMethods) + public SieveProcessor(IOptions options, ISieveCustomFilterMethods customFilterMethods) + : base(options, customFilterMethods) { } - public SieveProcessor(IOptions options, ISieveCustomSortMethods customSortMethods, ISieveCustomFilterMethods customFilterMethods) : base(options, customSortMethods, customFilterMethods) + public SieveProcessor(IOptions options, ISieveCustomSortMethods customSortMethods, + ISieveCustomFilterMethods customFilterMethods) : base(options, customSortMethods, customFilterMethods) { } } - public class SieveProcessor : SieveProcessor, TFilterTerm, TSortTerm>, ISieveProcessor + public class SieveProcessor : + SieveProcessor, TFilterTerm, TSortTerm>, ISieveProcessor where TFilterTerm : IFilterTerm, new() where TSortTerm : ISortTerm, new() { - public SieveProcessor(IOptions options) : base(options) + public SieveProcessor(IOptions options) + : base(options) { } - public SieveProcessor(IOptions options, ISieveCustomSortMethods customSortMethods) : base(options, customSortMethods) + public SieveProcessor(IOptions options, ISieveCustomSortMethods customSortMethods) + : base(options, customSortMethods) { } - public SieveProcessor(IOptions options, ISieveCustomFilterMethods customFilterMethods) : base(options, customFilterMethods) + public SieveProcessor(IOptions options, ISieveCustomFilterMethods customFilterMethods) + : base(options, customFilterMethods) { } - public SieveProcessor(IOptions options, ISieveCustomSortMethods customSortMethods, ISieveCustomFilterMethods customFilterMethods) : base(options, customSortMethods, customFilterMethods) + public SieveProcessor(IOptions options, ISieveCustomSortMethods customSortMethods, + ISieveCustomFilterMethods customFilterMethods) + : base(options, customSortMethods, customFilterMethods) { } } @@ -57,17 +67,17 @@ namespace Sieve.Services where TFilterTerm : IFilterTerm, new() where TSortTerm : ISortTerm, new() { - private const string nullFilterValue = "null"; + private const string NullFilterValue = "null"; private readonly IOptions _options; private readonly ISieveCustomSortMethods _customSortMethods; private readonly ISieveCustomFilterMethods _customFilterMethods; - private readonly SievePropertyMapper mapper = new SievePropertyMapper(); + private readonly SievePropertyMapper _mapper = new SievePropertyMapper(); public SieveProcessor(IOptions options, ISieveCustomSortMethods customSortMethods, ISieveCustomFilterMethods customFilterMethods) { - mapper = MapProperties(mapper); + _mapper = MapProperties(_mapper); _options = options; _customSortMethods = customSortMethods; _customFilterMethods = customFilterMethods; @@ -76,7 +86,7 @@ namespace Sieve.Services public SieveProcessor(IOptions options, ISieveCustomSortMethods customSortMethods) { - mapper = MapProperties(mapper); + _mapper = MapProperties(_mapper); _options = options; _customSortMethods = customSortMethods; } @@ -84,14 +94,14 @@ namespace Sieve.Services public SieveProcessor(IOptions options, ISieveCustomFilterMethods customFilterMethods) { - mapper = MapProperties(mapper); + _mapper = MapProperties(_mapper); _options = options; _customFilterMethods = customFilterMethods; } public SieveProcessor(IOptions options) { - mapper = MapProperties(mapper); + _mapper = MapProperties(_mapper); _options = options; } @@ -106,12 +116,8 @@ namespace Sieve.Services /// Should the data be sorted? Defaults to true. /// Should the data be paginated? Defaults to true. /// Returns a transformed version of `source` - public IQueryable Apply( - TSieveModel model, - IQueryable source, - object[] dataForCustomMethods = null, - bool applyFiltering = true, - bool applySorting = true, + public IQueryable Apply(TSieveModel model, IQueryable source, + object[] dataForCustomMethods = null, bool applyFiltering = true, bool applySorting = true, bool applyPagination = true) { var result = source; @@ -123,19 +129,16 @@ namespace Sieve.Services try { - // Filter if (applyFiltering) { result = ApplyFiltering(model, result, dataForCustomMethods); } - // Sort if (applySorting) { result = ApplySorting(model, result, dataForCustomMethods); } - // Paginate if (applyPagination) { result = ApplyPagination(model, result); @@ -145,25 +148,21 @@ namespace Sieve.Services } catch (Exception ex) { - if (_options.Value.ThrowExceptions) - { - if (ex is SieveException) - { - throw; - } - - throw new SieveException(ex.Message, ex); - } - else + if (!_options.Value.ThrowExceptions) { return result; } + + if (ex is SieveException) + { + throw; + } + + throw new SieveException(ex.Message, ex); } } - private IQueryable ApplyFiltering( - TSieveModel model, - IQueryable result, + private IQueryable ApplyFiltering(TSieveModel model, IQueryable result, object[] dataForCustomMethods = null) { if (model?.GetFiltersParsed() == null) @@ -181,25 +180,20 @@ namespace Sieve.Services var (fullPropertyName, property) = GetSieveProperty(false, true, filterTermName); if (property != null) { - Expression propertyValue = parameter; - Expression nullCheck = null; - var names = fullPropertyName.Split('.'); - for (var i = 0; i < names.Length; i++) + if (filterTerm.Values == null) { - propertyValue = Expression.PropertyOrField(propertyValue, names[i]); - - if (i != names.Length - 1 && propertyValue.Type.IsNullable()) - { - nullCheck = GenerateFilterNullCheckExpression(propertyValue, nullCheck); - } + continue; } - if (filterTerm.Values == null) continue; - var converter = TypeDescriptor.GetConverter(property.PropertyType); foreach (var filterTermValue in filterTerm.Values) { - var isFilterTermValueNull = filterTermValue.ToLower() == nullFilterValue; + var (propertyValue, nullCheck) = + GetPropertyValueAndNullCheckExpression(parameter, fullPropertyName); + + var isFilterTermValueNull = + IsFilterTermValueNull(propertyValue, filterTerm, filterTermValue); + var filterValue = isFilterTermValueNull ? Expression.Constant(null, property.PropertyType) : ConvertStringValueToConstantExpression(filterTermValue, property, converter); @@ -208,11 +202,11 @@ namespace Sieve.Services { propertyValue = Expression.Call(propertyValue, typeof(string).GetMethods() - .First(m => m.Name == "ToUpper" && m.GetParameters().Length == 0)); + .First(m => m.Name == "ToUpper" && m.GetParameters().Length == 0)); filterValue = Expression.Call(filterValue, typeof(string).GetMethods() - .First(m => m.Name == "ToUpper" && m.GetParameters().Length == 0)); + .First(m => m.Name == "ToUpper" && m.GetParameters().Length == 0)); } var expression = GetExpression(filterTerm, filterValue, propertyValue); @@ -222,60 +216,97 @@ namespace Sieve.Services expression = Expression.Not(expression); } - var filterValueNullCheck = !isFilterTermValueNull && propertyValue.Type.IsNullable() - ? GenerateFilterNullCheckExpression(propertyValue, nullCheck) - : nullCheck; - + var filterValueNullCheck = GetFilterValueNullCheck(parameter, fullPropertyName, isFilterTermValueNull); if (filterValueNullCheck != null) { expression = Expression.AndAlso(filterValueNullCheck, expression); } - if (innerExpression == null) - { - innerExpression = expression; - } - else - { - innerExpression = Expression.OrElse(innerExpression, expression); - } + innerExpression = innerExpression == null + ? expression + : Expression.OrElse(innerExpression, expression); } } else { result = ApplyCustomMethod(result, filterTermName, _customFilterMethods, - new object[] { - result, - filterTerm.Operator, - filterTerm.Values - }, dataForCustomMethods); - + new object[] {result, filterTerm.Operator, filterTerm.Values}, dataForCustomMethods); } } + if (outerExpression == null) { outerExpression = innerExpression; continue; } + if (innerExpression == null) { continue; } + outerExpression = Expression.AndAlso(outerExpression, innerExpression); } + return outerExpression == null ? result : result.Where(Expression.Lambda>(outerExpression, parameter)); } - private static Expression GenerateFilterNullCheckExpression(Expression propertyValue, Expression nullCheckExpression) + private static Expression GetFilterValueNullCheck(Expression parameter, string fullPropertyName, + bool isFilterTermValueNull) + { + var (propertyValue, nullCheck) = GetPropertyValueAndNullCheckExpression(parameter, fullPropertyName); + + if (!isFilterTermValueNull && propertyValue.Type.IsNullable()) + { + return GenerateFilterNullCheckExpression(propertyValue, nullCheck); + } + + return nullCheck; + } + + private static bool IsFilterTermValueNull(Expression propertyValue, TFilterTerm filterTerm, + string filterTermValue) + { + var isNotString = propertyValue.Type != typeof(string); + + var isValidStringNullOperation = filterTerm.OperatorParsed == FilterOperator.Equals || + filterTerm.OperatorParsed == FilterOperator.NotEquals; + + return filterTermValue.ToLower() == NullFilterValue && (isNotString || isValidStringNullOperation); + } + + private static (Expression propertyValue, Expression nullCheck) GetPropertyValueAndNullCheckExpression( + Expression parameter, string fullPropertyName) + { + var propertyValue = parameter; + Expression nullCheck = null; + var names = fullPropertyName.Split('.'); + for (var i = 0; i < names.Length; i++) + { + propertyValue = Expression.PropertyOrField(propertyValue, names[i]); + + if (i != names.Length - 1 && propertyValue.Type.IsNullable()) + { + nullCheck = GenerateFilterNullCheckExpression(propertyValue, nullCheck); + } + } + + return (propertyValue, nullCheck); + } + + private static Expression GenerateFilterNullCheckExpression(Expression propertyValue, + Expression nullCheckExpression) { return nullCheckExpression == null ? Expression.NotEqual(propertyValue, Expression.Default(propertyValue.Type)) - : Expression.AndAlso(nullCheckExpression, Expression.NotEqual(propertyValue, Expression.Default(propertyValue.Type))); + : Expression.AndAlso(nullCheckExpression, + Expression.NotEqual(propertyValue, Expression.Default(propertyValue.Type))); } - private Expression ConvertStringValueToConstantExpression(string value, PropertyInfo property, TypeConverter converter) + private static Expression ConvertStringValueToConstantExpression(string value, PropertyInfo property, + TypeConverter converter) { dynamic constantVal = converter.CanConvertFrom(typeof(string)) ? converter.ConvertFrom(value) @@ -286,47 +317,34 @@ namespace Sieve.Services private static Expression GetExpression(TFilterTerm filterTerm, dynamic filterValue, dynamic propertyValue) { - switch (filterTerm.OperatorParsed) + return filterTerm.OperatorParsed switch { - case FilterOperator.Equals: - return Expression.Equal(propertyValue, filterValue); - case FilterOperator.NotEquals: - return Expression.NotEqual(propertyValue, filterValue); - case FilterOperator.GreaterThan: - return Expression.GreaterThan(propertyValue, filterValue); - case FilterOperator.LessThan: - return Expression.LessThan(propertyValue, filterValue); - case FilterOperator.GreaterThanOrEqualTo: - return Expression.GreaterThanOrEqual(propertyValue, filterValue); - case FilterOperator.LessThanOrEqualTo: - return Expression.LessThanOrEqual(propertyValue, filterValue); - case FilterOperator.Contains: - return Expression.Call(propertyValue, - typeof(string).GetMethods() - .First(m => m.Name == "Contains" && m.GetParameters().Length == 1), - filterValue); - case FilterOperator.StartsWith: - return Expression.Call(propertyValue, - typeof(string).GetMethods() - .First(m => m.Name == "StartsWith" && m.GetParameters().Length == 1), - filterValue); - default: - return Expression.Equal(propertyValue, filterValue); - } + FilterOperator.Equals => Expression.Equal(propertyValue, filterValue), + FilterOperator.NotEquals => Expression.NotEqual(propertyValue, filterValue), + FilterOperator.GreaterThan => Expression.GreaterThan(propertyValue, filterValue), + FilterOperator.LessThan => Expression.LessThan(propertyValue, filterValue), + FilterOperator.GreaterThanOrEqualTo => Expression.GreaterThanOrEqual(propertyValue, filterValue), + FilterOperator.LessThanOrEqualTo => Expression.LessThanOrEqual(propertyValue, filterValue), + FilterOperator.Contains => Expression.Call(propertyValue, + typeof(string).GetMethods().First(m => m.Name == "Contains" && m.GetParameters().Length == 1), + filterValue), + FilterOperator.StartsWith => Expression.Call(propertyValue, + typeof(string).GetMethods().First(m => m.Name == "StartsWith" && m.GetParameters().Length == 1), + filterValue), + _ => Expression.Equal(propertyValue, filterValue) + }; } // Workaround to ensure that the filter value gets passed as a parameter in generated SQL from EF Core // See https://github.com/aspnet/EntityFrameworkCore/issues/3361 // Expression.Constant passed the target type to allow Nullable comparison // See http://bradwilson.typepad.com/blog/2008/07/creating-nullab.html - private Expression GetClosureOverConstant(T constant, Type targetType) + private static Expression GetClosureOverConstant(T constant, Type targetType) { return Expression.Constant(constant, targetType); } - private IQueryable ApplySorting( - TSieveModel model, - IQueryable result, + private IQueryable ApplySorting(TSieveModel model, IQueryable result, object[] dataForCustomMethods = null) { if (model?.GetSortsParsed() == null) @@ -346,33 +364,29 @@ namespace Sieve.Services else { result = ApplyCustomMethod(result, sortTerm.Name, _customSortMethods, - new object[] - { - result, - useThenBy, - sortTerm.Descending - }, dataForCustomMethods); + new object[] {result, useThenBy, sortTerm.Descending}, dataForCustomMethods); } + useThenBy = true; } return result; } - private IQueryable ApplyPagination( - TSieveModel model, - IQueryable result) + private IQueryable ApplyPagination(TSieveModel model, IQueryable result) { var page = model?.Page ?? 1; var pageSize = model?.PageSize ?? _options.Value.DefaultPageSize; var maxPageSize = _options.Value.MaxPageSize > 0 ? _options.Value.MaxPageSize : pageSize; - if (pageSize > 0) + if (pageSize <= 0) { - result = result.Skip((page - 1) * pageSize); - result = result.Take(Math.Min(pageSize, maxPageSize)); + return result; } + result = result.Skip((page - 1) * pageSize); + result = result.Take(Math.Min(pageSize, maxPageSize)); + return result; } @@ -381,51 +395,52 @@ namespace Sieve.Services return mapper; } - private (string, PropertyInfo) GetSieveProperty( - bool canSortRequired, - bool canFilterRequired, + private (string, PropertyInfo) GetSieveProperty(bool canSortRequired, bool canFilterRequired, string name) { - var property = mapper.FindProperty(canSortRequired, canFilterRequired, name, _options.Value.CaseSensitive); - if (property.Item1 == null) + var property = _mapper.FindProperty(canSortRequired, canFilterRequired, name, + _options.Value.CaseSensitive); + if (property.Item1 != null) { - var prop = FindPropertyBySieveAttribute(canSortRequired, canFilterRequired, name, _options.Value.CaseSensitive); - return (prop?.Name, prop); + return property; } - return property; + var prop = FindPropertyBySieveAttribute(canSortRequired, canFilterRequired, name, + _options.Value.CaseSensitive); + return (prop?.Name, prop); } - private PropertyInfo FindPropertyBySieveAttribute( - bool canSortRequired, - bool canFilterRequired, - string name, - bool isCaseSensitive) + private static PropertyInfo FindPropertyBySieveAttribute(bool canSortRequired, bool canFilterRequired, + string name, bool isCaseSensitive) { - return Array.Find(typeof(TEntity).GetProperties(), p => - { - return p.GetCustomAttribute(typeof(SieveAttribute)) is SieveAttribute sieveAttribute - && (!canSortRequired || sieveAttribute.CanSort) - && (!canFilterRequired || sieveAttribute.CanFilter) - && (sieveAttribute.Name ?? p.Name).Equals(name, isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); - }); + return Array.Find(typeof(TEntity).GetProperties(), + p => p.GetCustomAttribute(typeof(SieveAttribute)) is SieveAttribute SieveAttribute + && (!canSortRequired || SieveAttribute.CanSort) + && (!canFilterRequired || SieveAttribute.CanFilter) + && (SieveAttribute.Name ?? p.Name).Equals(name, + isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)); } - private IQueryable ApplyCustomMethod(IQueryable result, string name, object parent, object[] parameters, object[] optionalParameters = null) + private IQueryable ApplyCustomMethod(IQueryable result, string name, object parent, + object[] parameters, object[] optionalParameters = null) { var customMethod = parent?.GetType() .GetMethodExt(name, - _options.Value.CaseSensitive ? BindingFlags.Default : BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance, - typeof(IQueryable)); + _options.Value.CaseSensitive + ? BindingFlags.Default + : BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance, + typeof(IQueryable)); if (customMethod == null) { // Find generic methods `public IQueryable Filter(IQueryable source, ...)` var genericCustomMethod = parent?.GetType() - .GetMethodExt(name, - _options.Value.CaseSensitive ? BindingFlags.Default : BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance, - typeof(IQueryable<>)); + .GetMethodExt(name, + _options.Value.CaseSensitive + ? BindingFlags.Default + : BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance, + typeof(IQueryable<>)); if (genericCustomMethod != null && genericCustomMethod.ReturnType.IsGenericType && @@ -433,7 +448,8 @@ namespace Sieve.Services { var genericBaseType = genericCustomMethod.ReturnType.GenericTypeArguments[0]; var constraints = genericBaseType.GetGenericParameterConstraints(); - if (constraints == null || constraints.Length == 0 || constraints.All((t) => t.IsAssignableFrom(typeof(TEntity)))) + if (constraints == null || constraints.Length == 0 || + constraints.All((t) => t.IsAssignableFrom(typeof(TEntity)))) { customMethod = genericCustomMethod.MakeGenericMethod(typeof(TEntity)); } @@ -462,40 +478,34 @@ namespace Sieve.Services } else { - var incompatibleCustomMethods = parent? - .GetType() - .GetMethods - ( - _options.Value.CaseSensitive - ? BindingFlags.Default - : BindingFlags.IgnoreCase | BindingFlags.Public | - BindingFlags.Instance - ) - .Where(method => string.Equals(method.Name, name, - _options.Value.CaseSensitive - ? StringComparison.InvariantCulture - : StringComparison.InvariantCultureIgnoreCase)) - .ToList() - ?? - new List(); + var incompatibleCustomMethods = + parent? + .GetType() + .GetMethods(_options.Value.CaseSensitive + ? BindingFlags.Default + : BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) + .Where(method => string.Equals(method.Name, name, + _options.Value.CaseSensitive + ? StringComparison.InvariantCulture + : StringComparison.InvariantCultureIgnoreCase)) + .ToList() + ?? new List(); - if (incompatibleCustomMethods.Any()) - { - var incompatibles = - from incompatibleCustomMethod in incompatibleCustomMethods - let expected = typeof(IQueryable) - let actual = incompatibleCustomMethod.ReturnType - select new SieveIncompatibleMethodException(name, expected, actual, - $"{name} failed. Expected a custom method for type {expected} but only found for type {actual}"); - - var aggregate = new AggregateException(incompatibles); - - throw new SieveIncompatibleMethodException(aggregate.Message, aggregate); - } - else + if (!incompatibleCustomMethods.Any()) { throw new SieveMethodNotFoundException(name, $"{name} not found."); } + + var incompatibles = + from incompatibleCustomMethod in incompatibleCustomMethods + let expected = typeof(IQueryable) + let actual = incompatibleCustomMethod.ReturnType + select new SieveIncompatibleMethodException(name, expected, actual, + $"{name} failed. Expected a custom method for type {expected} but only found for type {actual}"); + + var aggregate = new AggregateException(incompatibles); + + throw new SieveIncompatibleMethodException(aggregate.Message, aggregate); } return result; diff --git a/Sieve/Sieve.csproj b/Sieve/Sieve.csproj index 344c5af..4f86b7a 100644 --- a/Sieve/Sieve.csproj +++ b/Sieve/Sieve.csproj @@ -1,22 +1,25 @@  - netstandard2.0 - 2.3.3 - Sieve is a simple, clean, and extensible framework for .NET Core that adds sorting, filtering, and pagination functionality out of the box. Most common use case would be for serving ASP.NET Core GET queries. Documentation available on GitHub: https://github.com/Biarity/Sieve/ - - Copyright 2018 - https://github.com/Biarity/Sieve/blob/master/LICENSE + netstandard2.1 + Sieve is a simple, clean, and extensible framework for .NET Core that adds sorting, filtering, and pagination functionality out of the box. Most common use case would be for serving ASP.NET Core GET queries. Documentation available on GitHub: https://github.com/Biarity/Sieve/ + 2018 Biarity, 2021 Kevin Sommer + + Filter;Sort;Page;Paging; + icon.png https://github.com/Biarity/Sieve - https://emojipedia-us.s3.amazonaws.com/thumbs/240/twitter/120/alembic_2697.png - - Only Skip when pageSize > 0 (#63) -Added support for generic filter and sort methods (#60) -Don't process when filterTerm.Values is null (#59) - - true + Apache-2.0 true - Biarity + + https://github.com/Biarity/Sieve + git + + + true + + true + + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb diff --git a/SieveTests/Migrations/20180127005347_Init.Designer.cs b/SieveTests/Migrations/20180127005347_Init.Designer.cs deleted file mode 100644 index 741e3af..0000000 --- a/SieveTests/Migrations/20180127005347_Init.Designer.cs +++ /dev/null @@ -1,44 +0,0 @@ -// -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.EntityFrameworkCore.Storage.Internal; -using SieveTests.Entities; -using System; - -namespace SieveTests.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20180127005347_Init")] - partial class Init - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.0.1-rtm-125") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("SieveTests.Entities.Post", b => - { - b.Property("Id") - .ValueGeneratedOnAdd(); - - b.Property("CommentCount"); - - b.Property("DateCreated"); - - b.Property("LikeCount"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.ToTable("Posts"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/SieveTests/Migrations/20180522013323_AddDateLastViewedColumn.Designer.cs b/SieveTests/Migrations/20180522013323_AddDateLastViewedColumn.Designer.cs deleted file mode 100644 index 052009c..0000000 --- a/SieveTests/Migrations/20180522013323_AddDateLastViewedColumn.Designer.cs +++ /dev/null @@ -1,47 +0,0 @@ -// -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.EntityFrameworkCore.Storage.Internal; -using SieveTests.Entities; -using System; - -namespace SieveTests.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20180522013323_AddDateLastViewedColumn")] - partial class AddDateLastViewedColumn - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.0.1-rtm-125") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("SieveTests.Entities.Post", b => - { - b.Property("Id") - .ValueGeneratedOnAdd(); - - b.Property("CommentCount"); - - b.Property("DateCreated"); - - b.Property("DateLastViewed") - .HasColumnType("datetime"); - - b.Property("LikeCount"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.ToTable("Posts"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/SieveTests/Migrations/20180522013323_AddDateLastViewedColumn.cs b/SieveTests/Migrations/20180522013323_AddDateLastViewedColumn.cs deleted file mode 100644 index b42ab3e..0000000 --- a/SieveTests/Migrations/20180522013323_AddDateLastViewedColumn.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace SieveTests.Migrations -{ - public partial class AddDateLastViewedColumn : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "DateLastViewed", - table: "Posts", - type: "datetime", - nullable: false, - defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "DateLastViewed", - table: "Posts"); - } - } -} diff --git a/SieveTests/Migrations/ApplicationDbContextModelSnapshot.cs b/SieveTests/Migrations/ApplicationDbContextModelSnapshot.cs deleted file mode 100644 index f2a7904..0000000 --- a/SieveTests/Migrations/ApplicationDbContextModelSnapshot.cs +++ /dev/null @@ -1,48 +0,0 @@ -// -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.EntityFrameworkCore.Storage.Internal; -using SieveTests.Entities; -using System; - -namespace SieveTests.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - partial class ApplicationDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.0.1-rtm-125") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("SieveTests.Entities.Post", b => - { - b.Property("Id") - .ValueGeneratedOnAdd(); - - b.Property("CommentCount"); - - b.Property("DateCreated"); - - b.Property("DateLastViewed") - .HasColumnType("datetime"); - - b.Property("LikeCount"); - - b.Property("CategoryId"); - - b.Property("Title"); - - b.HasKey("Id"); - - b.ToTable("Posts"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/SieveTests/SieveTests.csproj b/SieveTests/SieveTests.csproj deleted file mode 100644 index 1374e21..0000000 --- a/SieveTests/SieveTests.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - netcoreapp2.0 - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/SieveTests/Startup.cs b/SieveTests/Startup.cs deleted file mode 100644 index 415abd9..0000000 --- a/SieveTests/Startup.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Sieve.Models; -using Sieve.Services; -using SieveTests.Entities; -using SieveTests.Services; - -namespace SieveTests -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddMvc(); - - services.AddDbContext(options => - options.UseSqlServer(Configuration.GetConnectionString("TestSqlServer"))); - - services.Configure(Configuration.GetSection("Sieve")); - - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env) - { - // TIME MEASUREMENT - var times = new List(); - app.Use(async (context, next) => - { - var sw = new Stopwatch(); - sw.Start(); - await next.Invoke(); - sw.Stop(); - times.Add(sw.ElapsedMilliseconds); - var text = $"AVG: {(int)times.Average()}ms; AT {sw.ElapsedMilliseconds}; COUNT: {times.Count()}"; - Console.WriteLine(text); - await context.Response.WriteAsync($""); - }); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseMvc(); - } - } -} diff --git a/SieveUnitTests/Entities/Comment.cs b/SieveUnitTests/Entities/Comment.cs index b892796..4a04ff7 100644 --- a/SieveUnitTests/Entities/Comment.cs +++ b/SieveUnitTests/Entities/Comment.cs @@ -7,5 +7,8 @@ namespace SieveUnitTests.Entities { [Sieve(CanFilter = true)] public string Text { get; set; } + + [Sieve(CanFilter = true)] + public string Author { get; set; } } } diff --git a/SieveUnitTests/General.cs b/SieveUnitTests/General.cs index 5379825..f12e5a3 100644 --- a/SieveUnitTests/General.cs +++ b/SieveUnitTests/General.cs @@ -1,32 +1,34 @@ using System; using System.Collections.Generic; using System.Linq; -using Microsoft.VisualStudio.TestTools.UnitTesting; using Sieve.Exceptions; using Sieve.Models; using Sieve.Services; -using SieveUnitTests.Abstractions.Entity; using SieveUnitTests.Entities; using SieveUnitTests.Services; +using Xunit; +using Xunit.Abstractions; namespace SieveUnitTests { - [TestClass] public class General { + private readonly ITestOutputHelper _testOutputHelper; private readonly SieveProcessor _processor; private readonly IQueryable _posts; private readonly IQueryable _comments; - public General() + public General(ITestOutputHelper testOutputHelper) { + _testOutputHelper = testOutputHelper; _processor = new ApplicationSieveProcessor(new SieveOptionsAccessor(), new SieveCustomSortMethods(), new SieveCustomFilterMethods()); _posts = new List { - new Post() { + new Post + { Id = 0, Title = "A", LikeCount = 100, @@ -35,7 +37,8 @@ namespace SieveUnitTests TopComment = new Comment { Id = 0, Text = "A1" }, FeaturedComment = new Comment { Id = 4, Text = "A2" } }, - new Post() { + new Post + { Id = 1, Title = "B", LikeCount = 50, @@ -44,7 +47,8 @@ namespace SieveUnitTests TopComment = new Comment { Id = 3, Text = "B1" }, FeaturedComment = new Comment { Id = 5, Text = "B2" } }, - new Post() { + new Post + { Id = 2, Title = "C", LikeCount = 0, @@ -52,7 +56,8 @@ namespace SieveUnitTests TopComment = new Comment { Id = 2, Text = "C1" }, FeaturedComment = new Comment { Id = 6, Text = "C2" } }, - new Post() { + new Post + { Id = 3, Title = "D", LikeCount = 3, @@ -65,281 +70,301 @@ namespace SieveUnitTests _comments = new List { - new Comment() { + new Comment + { Id = 0, DateCreated = DateTimeOffset.UtcNow.AddDays(-20), Text = "This is an old comment." }, - new Comment() { + new Comment + { Id = 1, DateCreated = DateTimeOffset.UtcNow.AddDays(-1), Text = "This is a fairly new comment." }, - new Comment() { + new Comment + { Id = 2, DateCreated = DateTimeOffset.UtcNow, - Text = "This is a brand new comment. (Text in braces)" + Text = "This is a brand new comment. (Text in braces, comma separated)" }, }.AsQueryable(); } - [TestMethod] + [Fact] public void ContainsCanBeCaseInsensitive() { - var model = new SieveModel() + var model = new SieveModel { Filters = "Title@=*a" }; var result = _processor.Apply(model, _posts); - Assert.AreEqual(result.First().Id, 0); - Assert.IsTrue(result.Count() == 1); + Assert.Equal(0, result.First().Id); + Assert.True(result.Count() == 1); } - [TestMethod] + [Fact] public void NotEqualsCanBeCaseInsensitive() { - var model = new SieveModel() + var model = new SieveModel { Filters = "Title!=*a" }; var result = _processor.Apply(model, _posts); - Assert.AreEqual(result.First().Id, 1); - Assert.IsTrue(result.Count() == 3); + Assert.Equal(1, result.First().Id); + Assert.True(result.Count() == 3); } - [TestMethod] + [Fact] public void ContainsIsCaseSensitive() { - var model = new SieveModel() + var model = new SieveModel { Filters = "Title@=a", }; var result = _processor.Apply(model, _posts); - Assert.IsTrue(result.Count() == 0); + Assert.True(!result.Any()); } - [TestMethod] + [Fact] public void NotContainsWorks() { - var model = new SieveModel() + var model = new SieveModel { Filters = "Title!@=D", }; var result = _processor.Apply(model, _posts); - Assert.IsTrue(result.Count() == 3); + Assert.True(result.Count() == 3); } - [TestMethod] + [Fact] public void CanFilterBools() { - var model = new SieveModel() + var model = new SieveModel { Filters = "IsDraft==false" }; var result = _processor.Apply(model, _posts); - Assert.IsTrue(result.Count() == 2); + Assert.True(result.Count() == 2); } - [TestMethod] + [Fact] public void CanSortBools() { - var model = new SieveModel() + var model = new SieveModel { Sorts = "-IsDraft" }; var result = _processor.Apply(model, _posts); - Assert.AreEqual(result.First().Id, 0); + Assert.Equal(0, result.First().Id); } - [TestMethod] + [Fact] public void CanFilterNullableInts() { - var model = new SieveModel() + var model = new SieveModel { Filters = "CategoryId==1" }; var result = _processor.Apply(model, _posts); - Assert.IsTrue(result.Count() == 2); + Assert.True(result.Count() == 2); + } + + [Theory] + [InlineData(@"Text@=*\,")] + [InlineData(@"Text@=*\, ")] + [InlineData(@"Text@=*braces\,")] + [InlineData(@"Text@=*braces\, comma")] + public void CanFilterWithEscapedComma(string filter) + { + var model = new SieveModel + { + Filters = filter + }; + + var result = _processor.Apply(model, _comments); + + Assert.True(result.Count() == 1); } - [TestMethod] + [Fact] public void EqualsDoesntFailWithNonStringTypes() { - var model = new SieveModel() + var model = new SieveModel { Filters = "LikeCount==50", }; - Console.WriteLine(model.GetFiltersParsed()[0].Values); - Console.WriteLine(model.GetFiltersParsed()[0].Operator); - Console.WriteLine(model.GetFiltersParsed()[0].OperatorParsed); + _testOutputHelper.WriteLine(model.GetFiltersParsed()[0].Values.ToString()); + _testOutputHelper.WriteLine(model.GetFiltersParsed()[0].Operator); + _testOutputHelper.WriteLine(model.GetFiltersParsed()[0].OperatorParsed.ToString()); var result = _processor.Apply(model, _posts); - Assert.AreEqual(result.First().Id, 1); - Assert.IsTrue(result.Count() == 1); + Assert.Equal(1, result.First().Id); + Assert.True(result.Count() == 1); } - [TestMethod] + [Fact] public void CustomFiltersWork() { - var model = new SieveModel() + var model = new SieveModel { Filters = "Isnew", }; var result = _processor.Apply(model, _posts); - Assert.IsFalse(result.Any(p => p.Id == 0)); - Assert.IsTrue(result.Count() == 3); + Assert.False(result.Any(p => p.Id == 0)); + Assert.True(result.Count() == 3); } - [TestMethod] + [Fact] public void CustomGenericFiltersWork() { - var model = new SieveModel() + var model = new SieveModel { Filters = "Latest", }; var result = _processor.Apply(model, _comments); - Assert.IsFalse(result.Any(p => p.Id == 0)); - Assert.IsTrue(result.Count() == 2); + Assert.False(result.Any(p => p.Id == 0)); + Assert.True(result.Count() == 2); } - [TestMethod] + [Fact] public void CustomFiltersWithOperatorsWork() { - var model = new SieveModel() + var model = new SieveModel { Filters = "HasInTitle==A", }; var result = _processor.Apply(model, _posts); - Assert.IsTrue(result.Any(p => p.Id == 0)); - Assert.IsTrue(result.Count() == 1); + Assert.True(result.Any(p => p.Id == 0)); + Assert.True(result.Count() == 1); } - [TestMethod] + [Fact] public void CustomFiltersMixedWithUsualWork1() { - var model = new SieveModel() + var model = new SieveModel { Filters = "Isnew,CategoryId==2", }; var result = _processor.Apply(model, _posts); - Assert.IsTrue(result.Any(p => p.Id == 3)); - Assert.IsTrue(result.Count() == 1); + Assert.True(result.Any(p => p.Id == 3)); + Assert.True(result.Count() == 1); } - [TestMethod] + [Fact] public void CustomFiltersMixedWithUsualWork2() { - var model = new SieveModel() + var model = new SieveModel { Filters = "CategoryId==2,Isnew", }; var result = _processor.Apply(model, _posts); - Assert.IsTrue(result.Any(p => p.Id == 3)); - Assert.IsTrue(result.Count() == 1); + Assert.True(result.Any(p => p.Id == 3)); + Assert.True(result.Count() == 1); } - [TestMethod] + [Fact] public void CustomFiltersOnDifferentSourcesCanShareName() { - var postModel = new SieveModel() + var postModel = new SieveModel { Filters = "CategoryId==2,Isnew", }; var postResult = _processor.Apply(postModel, _posts); - Assert.IsTrue(postResult.Any(p => p.Id == 3)); - Assert.AreEqual(1, postResult.Count()); + Assert.True(postResult.Any(p => p.Id == 3)); + Assert.Equal(1, postResult.Count()); - var commentModel = new SieveModel() + var commentModel = new SieveModel { Filters = "Isnew", }; var commentResult = _processor.Apply(commentModel, _comments); - Assert.IsTrue(commentResult.Any(c => c.Id == 2)); - Assert.AreEqual(2, commentResult.Count()); + Assert.True(commentResult.Any(c => c.Id == 2)); + Assert.Equal(2, commentResult.Count()); } - [TestMethod] + [Fact] public void CustomSortsWork() { - var model = new SieveModel() + var model = new SieveModel { Sorts = "Popularity", }; var result = _processor.Apply(model, _posts); - Assert.IsFalse(result.First().Id == 0); + Assert.False(result.First().Id == 0); } - [TestMethod] + [Fact] public void CustomGenericSortsWork() { - var model = new SieveModel() + var model = new SieveModel { Sorts = "Oldest", }; var result = _processor.Apply(model, _posts); - Assert.IsTrue(result.Last().Id == 0); + Assert.True(result.Last().Id == 0); } - [TestMethod] + [Fact] public void MethodNotFoundExceptionWork() { - var model = new SieveModel() + var model = new SieveModel { Filters = "does not exist", }; - Assert.ThrowsException(() => _processor.Apply(model, _posts)); + Assert.Throws(() => _processor.Apply(model, _posts)); } - [TestMethod] + [Fact] public void IncompatibleMethodExceptionsWork() { - var model = new SieveModel() + var model = new SieveModel { Filters = "TestComment", }; - Assert.ThrowsException(() => _processor.Apply(model, _posts)); + Assert.Throws(() => _processor.Apply(model, _posts)); } - [TestMethod] + [Fact] public void OrNameFilteringWorks() { - var model = new SieveModel() + var model = new SieveModel { Filters = "(Title|LikeCount)==3", }; @@ -348,17 +373,17 @@ namespace SieveUnitTests var entry = result.FirstOrDefault(); var resultCount = result.Count(); - Assert.IsNotNull(entry); - Assert.AreEqual(1, resultCount); - Assert.AreEqual(3, entry.Id); + Assert.NotNull(entry); + Assert.Equal(1, resultCount); + Assert.Equal(3, entry.Id); } - [DataTestMethod] - [DataRow("CategoryId==1,(CategoryId|LikeCount)==50")] - [DataRow("(CategoryId|LikeCount)==50,CategoryId==1")] + [Theory] + [InlineData("CategoryId==1,(CategoryId|LikeCount)==50")] + [InlineData("(CategoryId|LikeCount)==50,CategoryId==1")] public void CombinedAndOrFilterIndependentOfOrder(string filter) { - var model = new SieveModel() + var model = new SieveModel { Filters = filter, }; @@ -367,14 +392,14 @@ namespace SieveUnitTests var entry = result.FirstOrDefault(); var resultCount = result.Count(); - Assert.IsNotNull(entry); - Assert.AreEqual(1, resultCount); + Assert.NotNull(entry); + Assert.Equal(1, resultCount); } - [TestMethod] + [Fact] public void CombinedAndOrWithSpaceFilteringWorks() { - var model = new SieveModel() + var model = new SieveModel { Filters = "Title==D, (Title|LikeCount)==3", }; @@ -383,97 +408,98 @@ namespace SieveUnitTests var entry = result.FirstOrDefault(); var resultCount = result.Count(); - Assert.IsNotNull(entry); - Assert.AreEqual(1, resultCount); - Assert.AreEqual(3, entry.Id); + Assert.NotNull(entry); + Assert.Equal(1, resultCount); + Assert.Equal(3, entry.Id); } - [TestMethod] + [Fact] public void OrValueFilteringWorks() { - var model = new SieveModel() + var model = new SieveModel { Filters = "Title==C|D", }; var result = _processor.Apply(model, _posts); - Assert.AreEqual(2, result.Count()); - Assert.IsTrue(result.Any(p => p.Id == 2)); - Assert.IsTrue(result.Any(p => p.Id == 3)); + Assert.Equal(2, result.Count()); + Assert.True(result.Any(p => p.Id == 2)); + Assert.True(result.Any(p => p.Id == 3)); } - [TestMethod] + [Fact] public void OrValueFilteringWorks2() { - var model = new SieveModel() + var model = new SieveModel { Filters = "Text@=(|)", }; var result = _processor.Apply(model, _comments); - Assert.AreEqual(1, result.Count()); - Assert.AreEqual(2, result.FirstOrDefault().Id); + Assert.Equal(1, result.Count()); + Assert.Equal(2, result.FirstOrDefault()?.Id); } - [TestMethod] + [Fact] public void NestedFilteringWorks() { - var model = new SieveModel() + var model = new SieveModel { Filters = "TopComment.Text!@=A", }; var result = _processor.Apply(model, _posts); - Assert.AreEqual(3, result.Count()); + Assert.Equal(3, result.Count()); var posts = result.ToList(); - Assert.IsTrue(posts[0].TopComment.Text.Contains("B")); - Assert.IsTrue(posts[1].TopComment.Text.Contains("C")); - Assert.IsTrue(posts[2].TopComment.Text.Contains("D")); + Assert.Contains("B", posts[0].TopComment.Text); + Assert.Contains("C", posts[1].TopComment.Text); + Assert.Contains("D", posts[2].TopComment.Text); } - [TestMethod] + [Fact] public void NestedSortingWorks() { - var model = new SieveModel() + var model = new SieveModel { Sorts = "TopComment.Id", }; var result = _processor.Apply(model, _posts); - Assert.AreEqual(4, result.Count()); + Assert.Equal(4, result.Count()); var posts = result.ToList(); - Assert.AreEqual(posts[0].Id, 0); - Assert.AreEqual(posts[1].Id, 3); - Assert.AreEqual(posts[2].Id, 2); - Assert.AreEqual(posts[3].Id, 1); + Assert.Equal(0, posts[0].Id); + Assert.Equal(3, posts[1].Id); + Assert.Equal(2, posts[2].Id); + Assert.Equal(1, posts[3].Id); } - [TestMethod] + [Fact] public void NestedFilteringWithIdenticTypesWorks() { - var model = new SieveModel() + var model = new SieveModel { Filters = "(topc|featc)@=*2", }; var result = _processor.Apply(model, _posts); - Assert.AreEqual(4, result.Count()); + Assert.Equal(4, result.Count()); - model = new SieveModel() + model = new SieveModel { Filters = "(topc|featc)@=*B", }; result = _processor.Apply(model, _posts); - Assert.AreEqual(1, result.Count()); + Assert.Equal(1, result.Count()); } - [TestMethod] + [Fact] public void FilteringNullsWorks() { var posts = new List { - new Post() { + new Post + { Id = 1, Title = null, LikeCount = 0, @@ -484,21 +510,22 @@ namespace SieveUnitTests }, }.AsQueryable(); - var model = new SieveModel() + var model = new SieveModel { Filters = "FeaturedComment.Text!@=Some value", }; var result = _processor.Apply(model, posts); - Assert.AreEqual(0, result.Count()); + Assert.Equal(0, result.Count()); } - [TestMethod] + [Fact] public void SortingNullsWorks() { var posts = new List { - new Post() { + new Post + { Id = 1, Title = null, LikeCount = 0, @@ -507,7 +534,8 @@ namespace SieveUnitTests TopComment = new Comment { Id = 1 }, FeaturedComment = null }, - new Post() { + new Post + { Id = 2, Title = null, LikeCount = 0, @@ -518,24 +546,25 @@ namespace SieveUnitTests }, }.AsQueryable(); - var model = new SieveModel() + var model = new SieveModel { Sorts = "TopComment.Id", }; var result = _processor.Apply(model, posts); - Assert.AreEqual(2, result.Count()); + Assert.Equal(2, result.Count()); var sortedPosts = result.ToList(); - Assert.AreEqual(sortedPosts[0].Id, 2); - Assert.AreEqual(sortedPosts[1].Id, 1); + Assert.Equal(2, sortedPosts[0].Id); + Assert.Equal(1, sortedPosts[1].Id); } - [TestMethod] + [Fact] public void FilteringOnNullWorks() { var posts = new List { - new Post() { + new Post + { Id = 1, Title = null, LikeCount = 0, @@ -544,7 +573,8 @@ namespace SieveUnitTests TopComment = null, FeaturedComment = null }, - new Post() { + new Post + { Id = 2, Title = null, LikeCount = 0, @@ -555,33 +585,33 @@ namespace SieveUnitTests }, }.AsQueryable(); - var model = new SieveModel() + var model = new SieveModel { Filters = "FeaturedComment.Text==null", }; var result = _processor.Apply(model, posts); - Assert.AreEqual(1, result.Count()); + Assert.Equal(1, result.Count()); var filteredPosts = result.ToList(); - Assert.AreEqual(filteredPosts[0].Id, 2); + Assert.Equal(2, filteredPosts[0].Id); } - [TestMethod] + [Fact] public void BaseDefinedPropertyMappingSortingWorks_WithCustomName() { - var model = new SieveModel() + var model = new SieveModel { Sorts = "-CreateDate" }; var result = _processor.Apply(model, _posts); - Assert.AreEqual(4, result.Count()); + Assert.Equal(4, result.Count()); var posts = result.ToList(); - Assert.AreEqual(posts[0].Id, 3); - Assert.AreEqual(posts[1].Id, 2); - Assert.AreEqual(posts[2].Id, 1); - Assert.AreEqual(posts[3].Id, 0); + Assert.Equal(3,posts[0].Id); + Assert.Equal(2,posts[1].Id); + Assert.Equal(1,posts[2].Id); + Assert.Equal(0,posts[3].Id); } } } diff --git a/SieveUnitTests/GeneralWithInterfaces.cs b/SieveUnitTests/GeneralWithInterfaces.cs index 45f6c9f..abcbaa3 100644 --- a/SieveUnitTests/GeneralWithInterfaces.cs +++ b/SieveUnitTests/GeneralWithInterfaces.cs @@ -1,33 +1,35 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using Microsoft.VisualStudio.TestTools.UnitTesting; using Sieve.Exceptions; using Sieve.Models; using Sieve.Services; using SieveUnitTests.Abstractions.Entity; using SieveUnitTests.Entities; using SieveUnitTests.Services; +using Xunit; +using Xunit.Abstractions; namespace SieveUnitTests { - [TestClass] public class GeneralWithInterfaces { + private readonly ITestOutputHelper _testOutputHelper; private readonly SieveProcessor _processor; private readonly IQueryable _posts; private readonly IQueryable _comments; - public GeneralWithInterfaces() + public GeneralWithInterfaces(ITestOutputHelper testOutputHelper) { + _testOutputHelper = testOutputHelper; _processor = new ApplicationSieveProcessor(new SieveOptionsAccessor(), new SieveCustomSortMethods(), new SieveCustomFilterMethods()); _posts = new List { - new Post() { + new Post + { Id = 0, Title = "A", LikeCount = 100, @@ -36,7 +38,8 @@ namespace SieveUnitTests TopComment = new Comment { Id = 0, Text = "A1" }, FeaturedComment = new Comment { Id = 4, Text = "A2" } }, - new Post() { + new Post + { Id = 1, Title = "B", LikeCount = 50, @@ -45,7 +48,8 @@ namespace SieveUnitTests TopComment = new Comment { Id = 3, Text = "B1" }, FeaturedComment = new Comment { Id = 5, Text = "B2" } }, - new Post() { + new Post + { Id = 2, Title = "C", LikeCount = 0, @@ -53,7 +57,8 @@ namespace SieveUnitTests TopComment = new Comment { Id = 2, Text = "C1" }, FeaturedComment = new Comment { Id = 6, Text = "C2" } }, - new Post() { + new Post + { Id = 3, Title = "D", LikeCount = 3, @@ -66,17 +71,20 @@ namespace SieveUnitTests _comments = new List { - new Comment() { + new Comment + { Id = 0, DateCreated = DateTimeOffset.UtcNow.AddDays(-20), Text = "This is an old comment." }, - new Comment() { + new Comment + { Id = 1, DateCreated = DateTimeOffset.UtcNow.AddDays(-1), Text = "This is a fairly new comment." }, - new Comment() { + new Comment + { Id = 2, DateCreated = DateTimeOffset.UtcNow, Text = "This is a brand new comment. (Text in braces)" @@ -84,263 +92,263 @@ namespace SieveUnitTests }.AsQueryable(); } - [TestMethod] + [Fact] public void ContainsCanBeCaseInsensitive() { - var model = new SieveModel() + var model = new SieveModel { Filters = "Title@=*a" }; var result = _processor.Apply(model, _posts); - Assert.AreEqual(result.First().Id, 0); - Assert.IsTrue(result.Count() == 1); + Assert.Equal(0, result.First().Id); + Assert.True(result.Count() == 1); } - [TestMethod] + [Fact] public void NotEqualsCanBeCaseInsensitive() { - var model = new SieveModel() + var model = new SieveModel { Filters = "Title!=*a" }; var result = _processor.Apply(model, _posts); - Assert.AreEqual(result.First().Id, 1); - Assert.IsTrue(result.Count() == 3); + Assert.Equal(1, result.First().Id); + Assert.True(result.Count() == 3); } - [TestMethod] + [Fact] public void ContainsIsCaseSensitive() { - var model = new SieveModel() + var model = new SieveModel { Filters = "Title@=a", }; var result = _processor.Apply(model, _posts); - Assert.IsTrue(result.Count() == 0); + Assert.True(!result.Any()); } - [TestMethod] + [Fact] public void NotContainsWorks() { - var model = new SieveModel() + var model = new SieveModel { Filters = "Title!@=D", }; var result = _processor.Apply(model, _posts); - Assert.IsTrue(result.Count() == 3); + Assert.True(result.Count() == 3); } - [TestMethod] + [Fact] public void CanFilterBools() { - var model = new SieveModel() + var model = new SieveModel { Filters = "IsDraft==false" }; var result = _processor.Apply(model, _posts); - Assert.IsTrue(result.Count() == 2); + Assert.True(result.Count() == 2); } - [TestMethod] + [Fact] public void CanSortBools() { - var model = new SieveModel() + var model = new SieveModel { Sorts = "-IsDraft" }; var result = _processor.Apply(model, _posts); - Assert.AreEqual(result.First().Id, 0); + Assert.Equal(0, result.First().Id); } - [TestMethod] + [Fact] public void CanFilterNullableInts() { - var model = new SieveModel() + var model = new SieveModel { Filters = "CategoryId==1" }; var result = _processor.Apply(model, _posts); - Assert.IsTrue(result.Count() == 2); + Assert.True(result.Count() == 2); } - [TestMethod] + [Fact] public void EqualsDoesntFailWithNonStringTypes() { - var model = new SieveModel() + var model = new SieveModel { Filters = "LikeCount==50", }; - Console.WriteLine(model.GetFiltersParsed()[0].Values); - Console.WriteLine(model.GetFiltersParsed()[0].Operator); - Console.WriteLine(model.GetFiltersParsed()[0].OperatorParsed); + _testOutputHelper.WriteLine(model.GetFiltersParsed()[0].Values.ToString()); + _testOutputHelper.WriteLine(model.GetFiltersParsed()[0].Operator); + _testOutputHelper.WriteLine(model.GetFiltersParsed()[0].OperatorParsed.ToString()); var result = _processor.Apply(model, _posts); - Assert.AreEqual(result.First().Id, 1); - Assert.IsTrue(result.Count() == 1); + Assert.Equal(1, result.First().Id); + Assert.True(result.Count() == 1); } - [TestMethod] + [Fact] public void CustomFiltersWork() { - var model = new SieveModel() + var model = new SieveModel { Filters = "Isnew", }; var result = _processor.Apply(model, _posts); - Assert.IsFalse(result.Any(p => p.Id == 0)); - Assert.IsTrue(result.Count() == 3); + Assert.False(result.Any(p => p.Id == 0)); + Assert.True(result.Count() == 3); } - [TestMethod] + [Fact] public void CustomGenericFiltersWork() { - var model = new SieveModel() + var model = new SieveModel { Filters = "Latest", }; var result = _processor.Apply(model, _comments); - Assert.IsFalse(result.Any(p => p.Id == 0)); - Assert.IsTrue(result.Count() == 2); + Assert.False(result.Any(p => p.Id == 0)); + Assert.True(result.Count() == 2); } - [TestMethod] + [Fact] public void CustomFiltersWithOperatorsWork() { - var model = new SieveModel() + var model = new SieveModel { Filters = "HasInTitle==A", }; var result = _processor.Apply(model, _posts); - Assert.IsTrue(result.Any(p => p.Id == 0)); - Assert.IsTrue(result.Count() == 1); + Assert.True(result.Any(p => p.Id == 0)); + Assert.True(result.Count() == 1); } - [TestMethod] + [Fact] public void CustomFiltersMixedWithUsualWork1() { - var model = new SieveModel() + var model = new SieveModel { Filters = "Isnew,CategoryId==2", }; var result = _processor.Apply(model, _posts); - Assert.IsTrue(result.Any(p => p.Id == 3)); - Assert.IsTrue(result.Count() == 1); + Assert.True(result.Any(p => p.Id == 3)); + Assert.True(result.Count() == 1); } - [TestMethod] + [Fact] public void CustomFiltersMixedWithUsualWork2() { - var model = new SieveModel() + var model = new SieveModel { Filters = "CategoryId==2,Isnew", }; var result = _processor.Apply(model, _posts); - Assert.IsTrue(result.Any(p => p.Id == 3)); - Assert.IsTrue(result.Count() == 1); + Assert.True(result.Any(p => p.Id == 3)); + Assert.True(result.Count() == 1); } - [TestMethod] + [Fact] public void CustomFiltersOnDifferentSourcesCanShareName() { - var postModel = new SieveModel() + var postModel = new SieveModel { Filters = "CategoryId==2,Isnew", }; var postResult = _processor.Apply(postModel, _posts); - Assert.IsTrue(postResult.Any(p => p.Id == 3)); - Assert.AreEqual(1, postResult.Count()); + Assert.True(postResult.Any(p => p.Id == 3)); + Assert.Equal(1, postResult.Count()); - var commentModel = new SieveModel() + var commentModel = new SieveModel { Filters = "Isnew", }; var commentResult = _processor.Apply(commentModel, _comments); - Assert.IsTrue(commentResult.Any(c => c.Id == 2)); - Assert.AreEqual(2, commentResult.Count()); + Assert.True(commentResult.Any(c => c.Id == 2)); + Assert.Equal(2, commentResult.Count()); } - [TestMethod] + [Fact] public void CustomSortsWork() { - var model = new SieveModel() + var model = new SieveModel { Sorts = "Popularity", }; var result = _processor.Apply(model, _posts); - Assert.IsFalse(result.First().Id == 0); + Assert.False(result.First().Id == 0); } - [TestMethod] + [Fact] public void CustomGenericSortsWork() { - var model = new SieveModel() + var model = new SieveModel { Sorts = "Oldest", }; var result = _processor.Apply(model, _posts); - Assert.IsTrue(result.Last().Id == 0); + Assert.True(result.Last().Id == 0); } - [TestMethod] + [Fact] public void MethodNotFoundExceptionWork() { - var model = new SieveModel() + var model = new SieveModel { Filters = "does not exist", }; - Assert.ThrowsException(() => _processor.Apply(model, _posts)); + Assert.Throws(() => _processor.Apply(model, _posts)); } - [TestMethod] + [Fact] public void IncompatibleMethodExceptionsWork() { - var model = new SieveModel() + var model = new SieveModel { Filters = "TestComment", }; - Assert.ThrowsException(() => _processor.Apply(model, _posts)); + Assert.Throws(() => _processor.Apply(model, _posts)); } - [TestMethod] + [Fact] public void OrNameFilteringWorks() { - var model = new SieveModel() + var model = new SieveModel { Filters = "(Title|LikeCount)==3", }; @@ -349,17 +357,17 @@ namespace SieveUnitTests var entry = result.FirstOrDefault(); var resultCount = result.Count(); - Assert.IsNotNull(entry); - Assert.AreEqual(1, resultCount); - Assert.AreEqual(3, entry.Id); + Assert.NotNull(entry); + Assert.Equal(1, resultCount); + Assert.Equal(3, entry.Id); } - [DataTestMethod] - [DataRow("CategoryId==1,(CategoryId|LikeCount)==50")] - [DataRow("(CategoryId|LikeCount)==50,CategoryId==1")] + [Theory] + [InlineData("CategoryId==1,(CategoryId|LikeCount)==50")] + [InlineData("(CategoryId|LikeCount)==50,CategoryId==1")] public void CombinedAndOrFilterIndependentOfOrder(string filter) { - var model = new SieveModel() + var model = new SieveModel { Filters = filter, }; @@ -368,14 +376,14 @@ namespace SieveUnitTests var entry = result.FirstOrDefault(); var resultCount = result.Count(); - Assert.IsNotNull(entry); - Assert.AreEqual(1, resultCount); + Assert.NotNull(entry); + Assert.Equal(1, resultCount); } - [TestMethod] + [Fact] public void CombinedAndOrWithSpaceFilteringWorks() { - var model = new SieveModel() + var model = new SieveModel { Filters = "Title==D, (Title|LikeCount)==3", }; @@ -384,97 +392,98 @@ namespace SieveUnitTests var entry = result.FirstOrDefault(); var resultCount = result.Count(); - Assert.IsNotNull(entry); - Assert.AreEqual(1, resultCount); - Assert.AreEqual(3, entry.Id); + Assert.NotNull(entry); + Assert.Equal(1, resultCount); + Assert.Equal(3, entry.Id); } - [TestMethod] + [Fact] public void OrValueFilteringWorks() { - var model = new SieveModel() + var model = new SieveModel { Filters = "Title==C|D", }; var result = _processor.Apply(model, _posts); - Assert.AreEqual(2, result.Count()); - Assert.IsTrue(result.Any(p => p.Id == 2)); - Assert.IsTrue(result.Any(p => p.Id == 3)); + Assert.Equal(2, result.Count()); + Assert.True(result.Any(p => p.Id == 2)); + Assert.True(result.Any(p => p.Id == 3)); } - [TestMethod] + [Fact] public void OrValueFilteringWorks2() { - var model = new SieveModel() + var model = new SieveModel { Filters = "Text@=(|)", }; var result = _processor.Apply(model, _comments); - Assert.AreEqual(1, result.Count()); - Assert.AreEqual(2, result.FirstOrDefault().Id); + Assert.Equal(1, result.Count()); + Assert.Equal(2, result.FirstOrDefault()?.Id); } - [TestMethod] + [Fact] public void NestedFilteringWorks() { - var model = new SieveModel() + var model = new SieveModel { Filters = "TopComment.Text!@=A", }; var result = _processor.Apply(model, _posts); - Assert.AreEqual(3, result.Count()); + Assert.Equal(3, result.Count()); var posts = result.ToList(); - Assert.IsTrue(posts[0].TopComment.Text.Contains("B")); - Assert.IsTrue(posts[1].TopComment.Text.Contains("C")); - Assert.IsTrue(posts[2].TopComment.Text.Contains("D")); + Assert.Contains("B", posts[0].TopComment.Text); + Assert.Contains("C", posts[1].TopComment.Text); + Assert.Contains("D", posts[2].TopComment.Text); } - [TestMethod] + [Fact] public void NestedSortingWorks() { - var model = new SieveModel() + var model = new SieveModel { Sorts = "TopComment.Id", }; var result = _processor.Apply(model, _posts); - Assert.AreEqual(4, result.Count()); + Assert.Equal(4, result.Count()); var posts = result.ToList(); - Assert.AreEqual(posts[0].Id, 0); - Assert.AreEqual(posts[1].Id, 3); - Assert.AreEqual(posts[2].Id, 2); - Assert.AreEqual(posts[3].Id, 1); + Assert.Equal(0, posts[0].Id); + Assert.Equal(3, posts[1].Id); + Assert.Equal(2, posts[2].Id); + Assert.Equal(1, posts[3].Id); } - [TestMethod] + [Fact] public void NestedFilteringWithIdenticTypesWorks() { - var model = new SieveModel() + var model = new SieveModel { Filters = "(topc|featc)@=*2", }; var result = _processor.Apply(model, _posts); - Assert.AreEqual(4, result.Count()); + Assert.Equal(4, result.Count()); - model = new SieveModel() + model = new SieveModel { Filters = "(topc|featc)@=*B", }; result = _processor.Apply(model, _posts); - Assert.AreEqual(1, result.Count()); + Assert.Equal(1, result.Count()); } - [TestMethod] + [Fact] public void FilteringNullsWorks() { var posts = new List { - new Post() { + new Post + { Id = 1, Title = null, LikeCount = 0, @@ -485,21 +494,22 @@ namespace SieveUnitTests }, }.AsQueryable(); - var model = new SieveModel() + var model = new SieveModel { Filters = "FeaturedComment.Text!@=Some value", }; var result = _processor.Apply(model, posts); - Assert.AreEqual(0, result.Count()); + Assert.Equal(0, result.Count()); } - [TestMethod] + [Fact] public void SortingNullsWorks() { var posts = new List { - new Post() { + new Post + { Id = 1, Title = null, LikeCount = 0, @@ -508,7 +518,8 @@ namespace SieveUnitTests TopComment = new Comment { Id = 1 }, FeaturedComment = null }, - new Post() { + new Post + { Id = 2, Title = null, LikeCount = 0, @@ -519,24 +530,25 @@ namespace SieveUnitTests }, }.AsQueryable(); - var model = new SieveModel() + var model = new SieveModel { Sorts = "TopComment.Id", }; var result = _processor.Apply(model, posts); - Assert.AreEqual(2, result.Count()); + Assert.Equal(2, result.Count()); var sortedPosts = result.ToList(); - Assert.AreEqual(sortedPosts[0].Id, 2); - Assert.AreEqual(sortedPosts[1].Id, 1); + Assert.Equal(2, sortedPosts[0].Id); + Assert.Equal(1, sortedPosts[1].Id); } - [TestMethod] + [Fact] public void FilteringOnNullWorks() { var posts = new List { - new Post() { + new Post + { Id = 1, Title = null, LikeCount = 0, @@ -545,7 +557,8 @@ namespace SieveUnitTests TopComment = null, FeaturedComment = null }, - new Post() { + new Post + { Id = 2, Title = null, LikeCount = 0, @@ -556,33 +569,33 @@ namespace SieveUnitTests }, }.AsQueryable(); - var model = new SieveModel() + var model = new SieveModel { Filters = "FeaturedComment.Text==null", }; var result = _processor.Apply(model, posts); - Assert.AreEqual(1, result.Count()); + Assert.Equal(1, result.Count()); var filteredPosts = result.ToList(); - Assert.AreEqual(filteredPosts[0].Id, 2); + Assert.Equal(2, filteredPosts[0].Id); } - [TestMethod] + [Fact] public void BaseDefinedPropertyMappingSortingWorks_WithCustomName() { - var model = new SieveModel() + var model = new SieveModel { Sorts = "-CreateDate" }; var result = _processor.Apply(model, _posts); - Assert.AreEqual(4, result.Count()); + Assert.Equal(4, result.Count()); var posts = result.ToList(); - Assert.AreEqual(posts[0].Id, 3); - Assert.AreEqual(posts[1].Id, 2); - Assert.AreEqual(posts[2].Id, 1); - Assert.AreEqual(posts[3].Id, 0); + Assert.Equal(3,posts[0].Id); + Assert.Equal(2,posts[1].Id); + Assert.Equal(1,posts[2].Id); + Assert.Equal(0,posts[3].Id); } } } diff --git a/SieveUnitTests/Mapper.cs b/SieveUnitTests/Mapper.cs index 7da20a4..f8a126b 100644 --- a/SieveUnitTests/Mapper.cs +++ b/SieveUnitTests/Mapper.cs @@ -1,14 +1,13 @@ using System.Collections.Generic; using System.Linq; -using Microsoft.VisualStudio.TestTools.UnitTesting; using Sieve.Exceptions; using Sieve.Models; using SieveUnitTests.Entities; using SieveUnitTests.Services; +using Xunit; namespace SieveUnitTests { - [TestClass] public class Mapper { private readonly ApplicationSieveProcessor _processor; @@ -22,19 +21,22 @@ namespace SieveUnitTests _posts = new List { - new Post() { + new Post + { Id = 1, ThisHasNoAttributeButIsAccessible = "A", ThisHasNoAttribute = "A", OnlySortableViaFluentApi = 100 }, - new Post() { + new Post + { Id = 2, ThisHasNoAttributeButIsAccessible = "B", ThisHasNoAttribute = "B", OnlySortableViaFluentApi = 50 }, - new Post() { + new Post + { Id = 3, ThisHasNoAttributeButIsAccessible = "C", ThisHasNoAttribute = "C", @@ -43,25 +45,25 @@ namespace SieveUnitTests }.AsQueryable(); } - [TestMethod] + [Fact] public void MapperWorks() { - var model = new SieveModel() + var model = new SieveModel { Filters = "shortname@=A", }; var result = _processor.Apply(model, _posts); - Assert.AreEqual(result.First().ThisHasNoAttributeButIsAccessible, "A"); + Assert.Equal("A", result.First().ThisHasNoAttributeButIsAccessible); - Assert.IsTrue(result.Count() == 1); + Assert.True(result.Count() == 1); } - [TestMethod] + [Fact] public void MapperSortOnlyWorks() { - var model = new SieveModel() + var model = new SieveModel { Filters = "OnlySortableViaFluentApi@=50", Sorts = "OnlySortableViaFluentApi" @@ -69,17 +71,11 @@ namespace SieveUnitTests var result = _processor.Apply(model, _posts, applyFiltering: false, applyPagination: false); - Assert.ThrowsException(() => _processor.Apply(model, _posts)); + Assert.Throws(() => _processor.Apply(model, _posts)); - Assert.AreEqual(result.First().Id, 3); + Assert.Equal(3, result.First().Id); - Assert.IsTrue(result.Count() == 3); + Assert.True(result.Count() == 3); } } } - -// -//Sorts = "LikeCount", -//Page = 1, -//PageSize = 10 -// \ No newline at end of file diff --git a/SieveUnitTests/SieveUnitTests.csproj b/SieveUnitTests/SieveUnitTests.csproj index 772896a..1c26e54 100644 --- a/SieveUnitTests/SieveUnitTests.csproj +++ b/SieveUnitTests/SieveUnitTests.csproj @@ -1,15 +1,22 @@ - netcoreapp2.0 - + netcoreapp3.1 false - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/SieveUnitTests/StringFilterNullTests.cs b/SieveUnitTests/StringFilterNullTests.cs new file mode 100644 index 0000000..f4ccedd --- /dev/null +++ b/SieveUnitTests/StringFilterNullTests.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Sieve.Models; +using Sieve.Services; +using SieveUnitTests.Entities; +using SieveUnitTests.Services; +using Xunit; + +namespace SieveUnitTests +{ + public class StringFilterNullTests + { + private readonly IQueryable _comments; + private readonly SieveProcessor _processor; + + public StringFilterNullTests() + { + _processor = new SieveProcessor(new SieveOptionsAccessor()); + + _comments = new List + { + new Comment + { + Id = 0, + DateCreated = DateTimeOffset.UtcNow, + Text = "This text contains null somewhere in the middle of a string", + Author = "Dog", + }, + new Comment + { + Id = 1, + DateCreated = DateTimeOffset.UtcNow, + Text = "null is here in the text", + Author = "Cat", + }, + new Comment + { + Id = 2, + DateCreated = DateTimeOffset.UtcNow, + Text = "Regular comment without n*ll.", + Author = "Mouse", + }, + new Comment + { + Id = 100, + DateCreated = DateTimeOffset.UtcNow, + Text = null, + Author = "null", + }, + }.AsQueryable(); + } + + [Fact] + public void Filter_Equals_Null() + { + var model = new SieveModel {Filters = "Text==null"}; + + var result = _processor.Apply(model, _comments); + + Assert.Equal(100, result.Single().Id); + } + + [Fact] + public void Filter_NotEquals_Null() + { + var model = new SieveModel {Filters = "Text!=null"}; + + var result = _processor.Apply(model, _comments); + + Assert.Equal(new[] {0, 1, 2}, result.Select(p => p.Id)); + } + + [Theory] + [InlineData("Text@=null")] + [InlineData("Text@=*null")] + [InlineData("Text@=*NULL")] + [InlineData("Text@=*NulL")] + [InlineData("Text@=*null|text")] + public void Filter_Contains_NullString(string filter) + { + var model = new SieveModel {Filters = filter}; + + var result = _processor.Apply(model, _comments); + + Assert.Equal(new[] {0, 1}, result.Select(p => p.Id)); + } + + [Theory] + [InlineData("Text|Author==null", 100)] + [InlineData("Text|Author@=null", 0, 1, 100)] + [InlineData("Text|Author@=*null", 0, 1, 100)] + [InlineData("Text|Author_=null", 1, 100)] + [InlineData("Text|Author_=*null", 1, 100)] + public void MultiFilter_Contains_NullString(string filter, params int[] expectedIds) + { + var model = new SieveModel {Filters = filter}; + + var result = _processor.Apply(model, _comments); + + Assert.Equal(expectedIds, result.Select(p => p.Id)); + } + + [Theory] + [InlineData("Text_=null")] + [InlineData("Text_=*null")] + [InlineData("Text_=*NULL")] + [InlineData("Text_=*NulL")] + [InlineData("Text_=*null|text")] + public void Filter_StartsWith_NullString(string filter) + { + var model = new SieveModel {Filters = filter}; + + var result = _processor.Apply(model, _comments); + + Assert.Equal(new[] {1}, result.Select(p => p.Id)); + } + + [Theory] + [InlineData("Text!@=null")] + [InlineData("Text!@=*null")] + [InlineData("Text!@=*NULL")] + [InlineData("Text!@=*NulL")] + [InlineData("Text!@=*null|text")] + public void Filter_DoesNotContain_NullString(string filter) + { + var model = new SieveModel {Filters = filter}; + + var result = _processor.Apply(model, _comments); + + Assert.Equal(new[] {2}, result.Select(p => p.Id)); + } + + [Theory] + [InlineData("Text!_=null")] + [InlineData("Text!_=*null")] + [InlineData("Text!_=*NULL")] + [InlineData("Text!_=*NulL")] + public void Filter_DoesNotStartsWith_NullString(string filter) + { + var model = new SieveModel {Filters = filter}; + + var result = _processor.Apply(model, _comments); + + Assert.Equal(new[] {0, 2}, result.Select(p => p.Id)); + } + } +}