Merge pull request #126 from Biarity/sieve-3-based-on-pagr-fork

Merge changes from Pagr fork and prepare CI / CD
This commit is contained in:
Biarity 2021-05-14 09:09:37 +00:00 committed by GitHub
commit d513b108ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 1435 additions and 1001 deletions

31
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,31 @@
# ------------------------------------------------------------------------------
# <auto-generated>
#
# This code was generated.
#
# - To turn off auto-generation set:
#
# [GitHubActions (AutoGenerate = false)]
#
# - To trigger manual generation invoke:
#
# nuke --generate-configuration GitHubActions_ci --host GitHubActions
#
# </auto-generated>
# ------------------------------------------------------------------------------
name: ci
on:
push:
branches:
- master
jobs:
ubuntu-latest:
name: ubuntu-latest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Run './build.cmd Ci'
run: ./build.cmd Ci

8
.gitignore vendored
View File

@ -260,4 +260,10 @@ paket-files/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
*.pyc
# Nuke output
/output
# Sample database
Sieve.Sample/Sieve.db

108
.nuke/build.schema.json Normal file
View File

@ -0,0 +1,108 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Build Schema",
"$ref": "#/definitions/build",
"definitions": {
"build": {
"type": "object",
"properties": {
"Configuration": {
"type": "string",
"description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)",
"enum": [
"Debug",
"Release"
]
},
"Continue": {
"type": "boolean",
"description": "Indicates to continue a previously failed build attempt"
},
"Help": {
"type": "boolean",
"description": "Shows the help text for this build assembly"
},
"Host": {
"type": "string",
"description": "Host for execution. Default is 'automatic'",
"enum": [
"AppVeyor",
"AzurePipelines",
"Bamboo",
"Bitrise",
"GitHubActions",
"GitLab",
"Jenkins",
"SpaceAutomation",
"TeamCity",
"Terminal",
"TravisCI"
]
},
"NoLogo": {
"type": "boolean",
"description": "Disables displaying the NUKE logo"
},
"Plan": {
"type": "boolean",
"description": "Shows the execution plan (HTML)"
},
"Profile": {
"type": "array",
"description": "Defines the profiles to load",
"items": {
"type": "string"
}
},
"Root": {
"type": "string",
"description": "Root directory during build execution"
},
"Skip": {
"type": "array",
"description": "List of targets to be skipped. Empty list skips all dependencies",
"items": {
"type": "string",
"enum": [
"Ci",
"Clean",
"Compile",
"Package",
"Restore",
"Test"
]
}
},
"Solution": {
"type": "string",
"description": "Path to a solution file that is automatically loaded"
},
"Target": {
"type": "array",
"description": "List of targets to be invoked. Default is '{default_target}'",
"items": {
"type": "string",
"enum": [
"Ci",
"Clean",
"Compile",
"Package",
"Restore",
"Test"
]
}
},
"Verbosity": {
"type": "string",
"description": "Logging verbosity during build execution. Default is 'Normal'",
"enum": [
"Minimal",
"Normal",
"Quiet",
"Verbose"
]
}
}
}
}
}

4
.nuke/parameters.json Normal file
View File

@ -0,0 +1,4 @@
{
"$schema": "./build.schema.json",
"Solution": "Sieve.sln"
}

1
GitVersion.yml Normal file
View File

@ -0,0 +1 @@
next-version: 3.0

190
LICENSE
View File

@ -1,192 +1,4 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Copyright 2018 Biarity, 2021 a-patel and Kevin Sommer
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -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

View File

@ -1,6 +1,6 @@
using Microsoft.EntityFrameworkCore;
namespace SieveTests.Entities
namespace Sieve.Sample.Entities
{
public class ApplicationDbContext : DbContext
{

View File

@ -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);

View File

@ -0,0 +1,52 @@
// <auto-generated />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int?>("CategoryId")
.HasColumnType("INTEGER");
b.Property<int>("CommentCount")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("DateCreated")
.HasColumnType("TEXT");
b.Property<DateTime>("DateLastViewed")
.HasColumnType("datetime");
b.Property<int>("LikeCount")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Posts");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -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<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
.Annotation("Sqlite:Autoincrement", true),
Title = table.Column<string>(nullable: true),
LikeCount = table.Column<int>(nullable: false),
CommentCount = table.Column<int>(nullable: false),
DateCreated = table.Column<DateTimeOffset>(nullable: false),
LikeCount = table.Column<int>(nullable: false),
Title = table.Column<string>(nullable: true),
DateLastViewed = table.Column<DateTime>(type: "datetime", nullable: false),
CategoryId = table.Column<int>(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)

View File

@ -0,0 +1,50 @@
// <auto-generated />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int?>("CategoryId")
.HasColumnType("INTEGER");
b.Property<int>("CommentCount")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("DateCreated")
.HasColumnType("TEXT");
b.Property<DateTime>("DateLastViewed")
.HasColumnType("datetime");
b.Property<int>("LikeCount")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Posts");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,7 +1,7 @@
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
namespace SieveTests
namespace Sieve.Sample
{
public static class Program
{

View File

@ -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
{

View File

@ -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
{

View File

@ -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
{

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.14">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.14" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SieveUnitTests\SieveUnitTests.csproj" />
<ProjectReference Include="..\Sieve\Sieve.csproj" />
</ItemGroup>
</Project>

60
Sieve.Sample/Startup.cs Normal file
View File

@ -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<ApplicationDbContext>(options =>
options.UseSqlite("Data Source=.\\sieve.db"));
services.Configure<SieveOptions>(Configuration.GetSection("Sieve"));
services.AddScoped<ISieveCustomSortMethods, SieveCustomSortMethods>();
services.AddScoped<ISieveCustomFilterMethods, SieveCustomFilterMethods>();
services.AddScoped<ISieveProcessor, ApplicationSieveProcessor>();
}
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<ApplicationDbContext>();
dbContext.Database.EnsureDeleted();
dbContext.Database.Migrate();
}
}
}

View File

@ -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
@ -15,12 +15,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
README.md = README.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{C8829BE9-BC1E-4C67-ACEA-EC5DD3633EE2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{C8829BE9-BC1E-4C67-ACEA-EC5DD3633EE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C8829BE9-BC1E-4C67-ACEA-EC5DD3633EE2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B32B8B33-94B0-40E3-8FE5-D54602222717}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B32B8B33-94B0-40E3-8FE5-D54602222717}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B32B8B33-94B0-40E3-8FE5-D54602222717}.Release|Any CPU.ActiveCfg = Release|Any CPU

View File

@ -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()
{
/// <summary>
/// Pattern used to split filters and sorts by comma.
/// </summary>
private const string EscapedCommaPattern = @"(?<!($|[^\\])(\\\\)*?\\),\s*";
/// <summary>
/// Escaped comma e.g. used in filter filter string.
/// </summary>
private const string EscapedComma = @"\,";
[DataMember]
public string Filters { get; set; }
@ -34,15 +43,20 @@ namespace Sieve.Models
var value = new List<TFilterTerm>();
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<TSortTerm> GetSortsParsed()
{
if (Sorts != null)
{
var value = new List<TSortTerm>();
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<TSortTerm>();
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;
}
}
}

View File

@ -14,40 +14,50 @@ namespace Sieve.Services
{
public class SieveProcessor : SieveProcessor<SieveModel, FilterTerm, SortTerm>, ISieveProcessor
{
public SieveProcessor(IOptions<SieveOptions> options) : base(options)
public SieveProcessor(IOptions<SieveOptions> options)
: base(options)
{
}
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods) : base(options, customSortMethods)
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods)
: base(options, customSortMethods)
{
}
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomFilterMethods customFilterMethods) : base(options, customFilterMethods)
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomFilterMethods customFilterMethods)
: base(options, customFilterMethods)
{
}
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods, ISieveCustomFilterMethods customFilterMethods) : base(options, customSortMethods, customFilterMethods)
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods,
ISieveCustomFilterMethods customFilterMethods) : base(options, customSortMethods, customFilterMethods)
{
}
}
public class SieveProcessor<TFilterTerm, TSortTerm> : SieveProcessor<SieveModel<TFilterTerm, TSortTerm>, TFilterTerm, TSortTerm>, ISieveProcessor<TFilterTerm, TSortTerm>
public class SieveProcessor<TFilterTerm, TSortTerm> :
SieveProcessor<SieveModel<TFilterTerm, TSortTerm>, TFilterTerm, TSortTerm>, ISieveProcessor<TFilterTerm, TSortTerm>
where TFilterTerm : IFilterTerm, new()
where TSortTerm : ISortTerm, new()
{
public SieveProcessor(IOptions<SieveOptions> options) : base(options)
public SieveProcessor(IOptions<SieveOptions> options)
: base(options)
{
}
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods) : base(options, customSortMethods)
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods)
: base(options, customSortMethods)
{
}
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomFilterMethods customFilterMethods) : base(options, customFilterMethods)
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomFilterMethods customFilterMethods)
: base(options, customFilterMethods)
{
}
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods, ISieveCustomFilterMethods customFilterMethods) : base(options, customSortMethods, customFilterMethods)
public SieveProcessor(IOptions<SieveOptions> 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<SieveOptions> _options;
private readonly ISieveCustomSortMethods _customSortMethods;
private readonly ISieveCustomFilterMethods _customFilterMethods;
private readonly SievePropertyMapper mapper = new SievePropertyMapper();
private readonly SievePropertyMapper _mapper = new SievePropertyMapper();
public SieveProcessor(IOptions<SieveOptions> 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<SieveOptions> options,
ISieveCustomSortMethods customSortMethods)
{
mapper = MapProperties(mapper);
_mapper = MapProperties(_mapper);
_options = options;
_customSortMethods = customSortMethods;
}
@ -84,14 +94,14 @@ namespace Sieve.Services
public SieveProcessor(IOptions<SieveOptions> options,
ISieveCustomFilterMethods customFilterMethods)
{
mapper = MapProperties(mapper);
_mapper = MapProperties(_mapper);
_options = options;
_customFilterMethods = customFilterMethods;
}
public SieveProcessor(IOptions<SieveOptions> options)
{
mapper = MapProperties(mapper);
_mapper = MapProperties(_mapper);
_options = options;
}
@ -106,12 +116,8 @@ namespace Sieve.Services
/// <param name="applySorting">Should the data be sorted? Defaults to true.</param>
/// <param name="applyPagination">Should the data be paginated? Defaults to true.</param>
/// <returns>Returns a transformed version of `source`</returns>
public IQueryable<TEntity> Apply<TEntity>(
TSieveModel model,
IQueryable<TEntity> source,
object[] dataForCustomMethods = null,
bool applyFiltering = true,
bool applySorting = true,
public IQueryable<TEntity> Apply<TEntity>(TSieveModel model, IQueryable<TEntity> 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<TEntity> ApplyFiltering<TEntity>(
TSieveModel model,
IQueryable<TEntity> result,
private IQueryable<TEntity> ApplyFiltering<TEntity>(TSieveModel model, IQueryable<TEntity> result,
object[] dataForCustomMethods = null)
{
if (model?.GetFiltersParsed() == null)
@ -181,25 +180,20 @@ namespace Sieve.Services
var (fullPropertyName, property) = GetSieveProperty<TEntity>(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<Func<TEntity, bool>>(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>(T constant, Type targetType)
private static Expression GetClosureOverConstant<T>(T constant, Type targetType)
{
return Expression.Constant(constant, targetType);
}
private IQueryable<TEntity> ApplySorting<TEntity>(
TSieveModel model,
IQueryable<TEntity> result,
private IQueryable<TEntity> ApplySorting<TEntity>(TSieveModel model, IQueryable<TEntity> 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<TEntity> ApplyPagination<TEntity>(
TSieveModel model,
IQueryable<TEntity> result)
private IQueryable<TEntity> ApplyPagination<TEntity>(TSieveModel model, IQueryable<TEntity> 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<TEntity>(
bool canSortRequired,
bool canFilterRequired,
private (string, PropertyInfo) GetSieveProperty<TEntity>(bool canSortRequired, bool canFilterRequired,
string name)
{
var property = mapper.FindProperty<TEntity>(canSortRequired, canFilterRequired, name, _options.Value.CaseSensitive);
if (property.Item1 == null)
var property = _mapper.FindProperty<TEntity>(canSortRequired, canFilterRequired, name,
_options.Value.CaseSensitive);
if (property.Item1 != null)
{
var prop = FindPropertyBySieveAttribute<TEntity>(canSortRequired, canFilterRequired, name, _options.Value.CaseSensitive);
return (prop?.Name, prop);
return property;
}
return property;
var prop = FindPropertyBySieveAttribute<TEntity>(canSortRequired, canFilterRequired, name,
_options.Value.CaseSensitive);
return (prop?.Name, prop);
}
private PropertyInfo FindPropertyBySieveAttribute<TEntity>(
bool canSortRequired,
bool canFilterRequired,
string name,
bool isCaseSensitive)
private static PropertyInfo FindPropertyBySieveAttribute<TEntity>(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<TEntity> ApplyCustomMethod<TEntity>(IQueryable<TEntity> result, string name, object parent, object[] parameters, object[] optionalParameters = null)
private IQueryable<TEntity> ApplyCustomMethod<TEntity>(IQueryable<TEntity> 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<TEntity>));
_options.Value.CaseSensitive
? BindingFlags.Default
: BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance,
typeof(IQueryable<TEntity>));
if (customMethod == null)
{
// Find generic methods `public IQueryable<T> Filter<T>(IQueryable<T> 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<MethodInfo>();
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<MethodInfo>();
if (incompatibleCustomMethods.Any())
{
var incompatibles =
from incompatibleCustomMethod in incompatibleCustomMethods
let expected = typeof(IQueryable<TEntity>)
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<TEntity>)
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;

View File

@ -1,22 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Version>2.3.3</Version>
<Description>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/
</Description>
<Copyright>Copyright 2018</Copyright>
<PackageLicenseUrl>https://github.com/Biarity/Sieve/blob/master/LICENSE</PackageLicenseUrl>
<TargetFramework>netstandard2.1</TargetFramework>
<Description>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/</Description>
<Authors>Biarity;Ashish Patel;Kevin Sommer</Authors>
<PackageTags>Filter;Sort;Page;Paging;</PackageTags>
<PackageProjectUrl>https://github.com/Biarity/Sieve</PackageProjectUrl>
<PackageIconUrl>https://emojipedia-us.s3.amazonaws.com/thumbs/240/twitter/120/alembic_2697.png</PackageIconUrl>
<RepositoryUrl></RepositoryUrl>
<PackageReleaseNotes>Only Skip when pageSize &gt; 0 (#63)
Added support for generic filter and sort methods (#60)
Don't process when filterTerm.Values is null (#59)
</PackageReleaseNotes>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<Authors>Biarity</Authors>
<RepositoryUrl>https://github.com/Biarity/Sieve</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<!-- Declare that the Repository URL can be published to NuSpec -->
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<!-- Embed source files that are not tracked by the source control manager to the PDB -->
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<!-- Include PDB in the built .nupkg -->
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
</PropertyGroup>
<ItemGroup>

Binary file not shown.

View File

@ -1,44 +0,0 @@
// <auto-generated />
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<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("CommentCount");
b.Property<DateTimeOffset>("DateCreated");
b.Property<int>("LikeCount");
b.Property<string>("Title");
b.HasKey("Id");
b.ToTable("Posts");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,47 +0,0 @@
// <auto-generated />
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<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("CommentCount");
b.Property<DateTimeOffset>("DateCreated");
b.Property<DateTime>("DateLastViewed")
.HasColumnType("datetime");
b.Property<int>("LikeCount");
b.Property<string>("Title");
b.HasKey("Id");
b.ToTable("Posts");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -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<DateTime>(
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");
}
}
}

View File

@ -1,48 +0,0 @@
// <auto-generated />
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<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("CommentCount");
b.Property<DateTimeOffset>("DateCreated");
b.Property<DateTime>("DateLastViewed")
.HasColumnType("datetime");
b.Property<int>("LikeCount");
b.Property<int?>("CategoryId");
b.Property<string>("Title");
b.HasKey("Id");
b.ToTable("Posts");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,29 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Migrations\20180127005211_Itit.cs" />
<Compile Remove="Migrations\20180127005211_Itit.Designer.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.9" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SieveUnitTests\SieveUnitTests.csproj" />
<ProjectReference Include="..\Sieve\Sieve.csproj" />
</ItemGroup>
</Project>

View File

@ -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<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("TestSqlServer")));
services.Configure<SieveOptions>(Configuration.GetSection("Sieve"));
services.AddScoped<ISieveCustomSortMethods, SieveCustomSortMethods>();
services.AddScoped<ISieveCustomFilterMethods, SieveCustomFilterMethods>();
services.AddScoped<ISieveProcessor, ApplicationSieveProcessor>();
}
// 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<long>();
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($"<!-- {text} -->");
});
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvc();
}
}
}

View File

@ -7,5 +7,8 @@ namespace SieveUnitTests.Entities
{
[Sieve(CanFilter = true)]
public string Text { get; set; }
[Sieve(CanFilter = true)]
public string Author { get; set; }
}
}

View File

@ -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<Post> _posts;
private readonly IQueryable<Comment> _comments;
public General()
public General(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
_processor = new ApplicationSieveProcessor(new SieveOptionsAccessor(),
new SieveCustomSortMethods(),
new SieveCustomFilterMethods());
_posts = new List<Post>
{
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<Comment>
{
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<SieveMethodNotFoundException>(() => _processor.Apply(model, _posts));
Assert.Throws<SieveMethodNotFoundException>(() => _processor.Apply(model, _posts));
}
[TestMethod]
[Fact]
public void IncompatibleMethodExceptionsWork()
{
var model = new SieveModel()
var model = new SieveModel
{
Filters = "TestComment",
};
Assert.ThrowsException<SieveIncompatibleMethodException>(() => _processor.Apply(model, _posts));
Assert.Throws<SieveIncompatibleMethodException>(() => _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<Post>
{
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<Post>
{
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<Post>
{
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);
}
}
}

View File

@ -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<IPost> _posts;
private readonly IQueryable<Comment> _comments;
public GeneralWithInterfaces()
public GeneralWithInterfaces(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
_processor = new ApplicationSieveProcessor(new SieveOptionsAccessor(),
new SieveCustomSortMethods(),
new SieveCustomFilterMethods());
_posts = new List<IPost>
{
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<Comment>
{
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<SieveMethodNotFoundException>(() => _processor.Apply(model, _posts));
Assert.Throws<SieveMethodNotFoundException>(() => _processor.Apply(model, _posts));
}
[TestMethod]
[Fact]
public void IncompatibleMethodExceptionsWork()
{
var model = new SieveModel()
var model = new SieveModel
{
Filters = "TestComment",
};
Assert.ThrowsException<SieveIncompatibleMethodException>(() => _processor.Apply(model, _posts));
Assert.Throws<SieveIncompatibleMethodException>(() => _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<Post>
{
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<Post>
{
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<Post>
{
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);
}
}
}

View File

@ -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<Post>
{
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<SieveMethodNotFoundException>(() => _processor.Apply(model, _posts));
Assert.Throws<SieveMethodNotFoundException>(() => _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
//

View File

@ -1,15 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
<PackageReference Include="MSTest.TestAdapter" Version="1.2.0" />
<PackageReference Include="MSTest.TestFramework" Version="1.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="Microsoft.Extensions.Options" Version="2.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="1.3.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>

View File

@ -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<Comment> _comments;
private readonly SieveProcessor _processor;
public StringFilterNullTests()
{
_processor = new SieveProcessor(new SieveOptionsAccessor());
_comments = new List<Comment>
{
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));
}
}
}

7
build.cmd Executable file
View File

@ -0,0 +1,7 @@
:; set -eo pipefail
:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
:; ${SCRIPT_DIR}/build.sh "$@"
:; exit $?
@ECHO OFF
powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %*

69
build.ps1 Normal file
View File

@ -0,0 +1,69 @@
[CmdletBinding()]
Param(
[Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
[string[]]$BuildArguments
)
Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)"
Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 }
$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
###########################################################################
# CONFIGURATION
###########################################################################
$BuildProjectFile = "$PSScriptRoot\build\_build.csproj"
$TempDirectory = "$PSScriptRoot\\.nuke\temp"
$DotNetGlobalFile = "$PSScriptRoot\\global.json"
$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1"
$DotNetChannel = "Current"
$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1
$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1
$env:DOTNET_MULTILEVEL_LOOKUP = 0
###########################################################################
# EXECUTION
###########################################################################
function ExecSafe([scriptblock] $cmd) {
& $cmd
if ($LASTEXITCODE) { exit $LASTEXITCODE }
}
# If dotnet CLI is installed globally and it matches requested version, use for execution
if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and `
$(dotnet --version) -and $LASTEXITCODE -eq 0) {
$env:DOTNET_EXE = (Get-Command "dotnet").Path
}
else {
# Download install script
$DotNetInstallFile = "$TempDirectory\dotnet-install.ps1"
New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
(New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile)
# If global.json exists, load expected version
if (Test-Path $DotNetGlobalFile) {
$DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json)
if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) {
$DotNetVersion = $DotNetGlobal.sdk.version
}
}
# Install by channel or version
$DotNetDirectory = "$TempDirectory\dotnet-win"
if (!(Test-Path variable:DotNetVersion)) {
ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath }
} else {
ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath }
}
$env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe"
}
Write-Output "Microsoft (R) .NET Core SDK version $(& $env:DOTNET_EXE --version)"
ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet }
ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments }

62
build.sh Executable file
View File

@ -0,0 +1,62 @@
#!/usr/bin/env bash
bash --version 2>&1 | head -n 1
set -eo pipefail
SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
###########################################################################
# CONFIGURATION
###########################################################################
BUILD_PROJECT_FILE="$SCRIPT_DIR/build/_build.csproj"
TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp"
DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json"
DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh"
DOTNET_CHANNEL="Current"
export DOTNET_CLI_TELEMETRY_OPTOUT=1
export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
export DOTNET_MULTILEVEL_LOOKUP=0
###########################################################################
# EXECUTION
###########################################################################
function FirstJsonValue {
perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}"
}
# If dotnet CLI is installed globally and it matches requested version, use for execution
if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then
export DOTNET_EXE="$(command -v dotnet)"
else
# Download install script
DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh"
mkdir -p "$TEMP_DIRECTORY"
curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL"
chmod +x "$DOTNET_INSTALL_FILE"
# If global.json exists, load expected version
if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then
DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")")
if [[ "$DOTNET_VERSION" == "" ]]; then
unset DOTNET_VERSION
fi
fi
# Install by channel or version
DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix"
if [[ -z ${DOTNET_VERSION+x} ]]; then
"$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path
else
"$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path
fi
export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet"
fi
echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)"
"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet
"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@"

11
build/.editorconfig Normal file
View File

@ -0,0 +1,11 @@
[*.cs]
dotnet_style_qualification_for_field = false:warning
dotnet_style_qualification_for_property = false:warning
dotnet_style_qualification_for_method = false:warning
dotnet_style_qualification_for_event = false:warning
dotnet_style_require_accessibility_modifiers = never:warning
csharp_style_expression_bodied_methods = true:silent
csharp_style_expression_bodied_properties = true:warning
csharp_style_expression_bodied_indexers = true:warning
csharp_style_expression_bodied_accessors = true:warning

96
build/Build.cs Normal file
View File

@ -0,0 +1,96 @@
using System.Linq;
using Nuke.Common;
using Nuke.Common.CI;
using Nuke.Common.CI.GitHubActions;
using Nuke.Common.Execution;
using Nuke.Common.Git;
using Nuke.Common.IO;
using Nuke.Common.ProjectModel;
using Nuke.Common.Tools.DotNet;
using Nuke.Common.Tools.GitVersion;
using static Nuke.Common.IO.FileSystemTasks;
using static Nuke.Common.Tools.DotNet.DotNetTasks;
[CheckBuildProjectConfigurations]
[ShutdownDotNetAfterServerBuild]
[GitHubActions("ci", GitHubActionsImage.UbuntuLatest,
OnPushBranches = new[] {"master"},
AutoGenerate = true,
InvokedTargets = new[] {nameof(Ci)},
CacheKeyFiles = new string[0])]
class Build : NukeBuild
{
/// Support plugins are available for:
/// - JetBrains ReSharper https://nuke.build/resharper
/// - JetBrains Rider https://nuke.build/rider
/// - Microsoft VisualStudio https://nuke.build/visualstudio
/// - Microsoft VSCode https://nuke.build/vscode
public static int Main() => Execute<Build>(x => x.Package);
[Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")]
readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release;
[Solution] readonly Solution Solution;
Project SieveProject => Solution.AllProjects.First(p => p.Name == "Sieve");
[GitRepository] readonly GitRepository GitRepository;
[GitVersion(Framework = "netcoreapp3.1")] readonly GitVersion GitVersion;
AbsolutePath OutputDirectory => RootDirectory / "output";
Target Clean => _ => _
.Executes(() =>
{
DotNetClean();
EnsureCleanDirectory(OutputDirectory);
});
Target Restore => _ => _
.DependsOn(Clean)
.Executes(() =>
{
DotNetRestore(s => s
.SetProjectFile(Solution));
});
Target Compile => _ => _
.DependsOn(Restore)
.Executes(() =>
{
DotNetBuild(s => s
.SetProjectFile(Solution)
.SetConfiguration(Configuration)
.SetAssemblyVersion(GitVersion.AssemblySemVer)
.SetFileVersion(GitVersion.AssemblySemFileVer)
.SetInformationalVersion(GitVersion.InformationalVersion)
.EnableNoRestore());
});
Target Test => _ => _
.DependsOn(Compile)
.Executes(() =>
{
DotNetTest(s => s
.SetProjectFile(Solution)
.EnableNoRestore()
.EnableNoBuild());
});
Target Package => _ => _
.DependsOn(Test)
.Executes(() =>
{
DotNetPack(s => s
.SetProject(SieveProject)
.SetConfiguration(Configuration)
.SetOutputDirectory(OutputDirectory)
.SetVersion(GitVersion.NuGetVersionV2)
.EnableNoRestore()
.EnableNoBuild());
});
Target Ci => _ => _
.DependsOn(Package);
}

16
build/Configuration.cs Normal file
View File

@ -0,0 +1,16 @@
using System;
using System.ComponentModel;
using System.Linq;
using Nuke.Common.Tooling;
[TypeConverter(typeof(TypeConverter<Configuration>))]
public class Configuration : Enumeration
{
public static Configuration Debug = new Configuration { Value = nameof(Debug) };
public static Configuration Release = new Configuration { Value = nameof(Release) };
public static implicit operator string(Configuration configuration)
{
return configuration.Value;
}
}

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- This file prevents unintended imports of unrelated MSBuild files -->
<!-- Uncomment to include parent Directory.Build.props file -->
<!--<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />-->
</Project>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- This file prevents unintended imports of unrelated MSBuild files -->
<!-- Uncomment to include parent Directory.Build.targets file -->
<!--<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.targets', '$(MSBuildThisFileDirectory)../'))" />-->
</Project>

17
build/_build.csproj Normal file
View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<RootNamespace></RootNamespace>
<NoWarn>CS0649;CS0169</NoWarn>
<NukeRootDirectory>..</NukeRootDirectory>
<NukeScriptDirectory>..</NukeScriptDirectory>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nuke.Common" Version="5.1.1" />
<PackageDownload Include="GitVersion.Tool" Version="[5.6.7]" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,27 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=HeapView_002EDelegateAllocation/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=VariableHidesOuterVariable/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ClassNeverInstantiated_002EGlobal/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MemberCanBeMadeStatic_002ELocal/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/DEFAULT_INTERNAL_MODIFIER/@EntryValue">Implicit</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/DEFAULT_PRIVATE_MODIFIER/@EntryValue">Implicit</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/METHOD_OR_OPERATOR_BODY/@EntryValue">ExpressionBody</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/ThisQualifier/INSTANCE_MEMBERS_QUALIFY_MEMBERS/@EntryValue">0</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ANONYMOUS_METHOD_DECLARATION_BRACES/@EntryValue">NEXT_LINE</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_USER_LINEBREAKS/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_AFTER_INVOCATION_LPAR/@EntryValue">False</s:Boolean>
<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/MAX_ATTRIBUTE_LENGTH_FOR_SAME_LINE/@EntryValue">120</s:Int64>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_FIELD_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">IF_OWNER_IS_SINGLE_LINE</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_ARGUMENTS_STYLE/@EntryValue">WRAP_IF_LONG</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_ANONYMOUSMETHOD_ON_SINGLE_LINE/@EntryValue">False</s:Boolean>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpAttributeForSingleLineMethodUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpRenamePlacementToArrangementMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAddAccessorOwnerDeclarationBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002ECSharpPlaceAttributeOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>