15 Commits

Author SHA1 Message Date
ITDancer13
aedbc1ed96 Fix broken paging (#136)
* Add unit tests
* Use calculated page size instead of page

Co-authored-by: ITDancer139 <kevinitdancersommer@gmail.com>
2021-05-16 16:34:36 +02:00
ITDancer13
2c9d907764 * Throw exceptions by default (#133)
* Doc strings for SieveOptions.cs
* Simplify MaxPageSize calculation

Co-authored-by: ITDancer139 <kevinitdancersommer@gmail.com>
2021-05-15 18:06:40 +02:00
Keivn Sommer
8bd9ce85d9 Fix nuget target path 2021-05-14 23:35:47 +02:00
Keivn Sommer
f738e3bf1e Use NUGET_API_KEY to publish first pre-release 2021-05-14 23:33:05 +02:00
Keivn Sommer
dd1b0a9edc Disable publish for master - should be activated as soon as it's merged back for the first time. 2021-05-14 23:28:18 +02:00
Keivn Sommer
27838b062c Set mode to 'ContinuousDeployment' to get unique NuGetPreReleaseTagV2 on releases/* 2021-05-14 23:28:18 +02:00
Keivn Sommer
38af9af982 Publish requires to be executed on server 2021-05-14 23:27:57 +02:00
Kevin Sommer
d188bed4f0 Setup NuGet push (without api key) 2021-05-14 23:27:57 +02:00
Kevin Sommer
1e29271fd9 Check if NUGET_API_KEY can be accessed 2021-05-14 23:27:57 +02:00
Kevin Sommer
034730bffb Use ci.yml for PRs only 2021-05-14 23:27:57 +02:00
Kevin Sommer
79c825cb7a Prepare publish on releases/* and master 2021-05-14 23:27:57 +02:00
Kevin Sommer
028ab1d196 Build an PRs to releases/* 2021-05-14 23:27:57 +02:00
ITDancer139
9277690e96 Replace features by releases 2021-05-14 23:27:57 +02:00
ITDancer139
d5474478b3 Update pipeline to build on feature branches 2021-05-14 23:27:57 +02:00
ITDancer139
4c5510772a build on feature branches 2021-05-14 23:27:57 +02:00
24 changed files with 143 additions and 771 deletions

View File

@@ -1,38 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Feature Request
url: https://github.com/biarity/sieve/discussions/new
about: Share your ideas on how to make Sieve better.

View File

@@ -20,8 +20,6 @@ on:
push:
branches:
- 'releases/*'
tags:
- 'v*'
jobs:
ubuntu-latest:

View File

@@ -2,8 +2,7 @@
⚗️ 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.
[![NuGet Release](https://img.shields.io/nuget/v/Sieve?style=for-the-badge)](https://www.nuget.org/packages/Sieve)
[![NuGet Pre-Release](https://img.shields.io/nuget/vpre/Sieve?style=for-the-badge)](https://www.nuget.org/packages/Sieve)
[![NuGet Release](https://img.shields.io/nuget/v/Sieve.svg?style=flat-square)](https://www.nuget.org/packages/Sieve)
[Get Sieve on nuget](https://www.nuget.org/packages/Sieve/)
@@ -75,7 +74,7 @@ Where `SieveCustomSortMethodsOfPosts` for example is:
```C#
public class SieveCustomSortMethods : ISieveCustomSortMethods
{
public IQueryable<Post> Popularity(IQueryable<Post> source, bool useThenBy, bool desc) // The method is given an indicator of whether to use ThenBy(), and if the query is descending
public IQueryable<Post> Popularity(IQueryable<Post> source, bool useThenBy, bool desc) // The method is given an indicator of weather to use ThenBy(), and if the query is descending
{
var result = useThenBy ?
((IOrderedQueryable<Post>)source).ThenBy(p => p.LikeCount) : // ThenBy only works on IOrderedQueryable<TEntity>
@@ -128,9 +127,7 @@ Then you can add the configuration:
"CaseSensitive": "boolean: should property names be case-sensitive? Defaults to false",
"DefaultPageSize": "int number: optional number to fallback to when no page argument is given. Set <=0 to disable paging if no pageSize is specified (default).",
"MaxPageSize": "int number: maximum allowed page size. Set <=0 to make infinite (default)",
"ThrowExceptions": "boolean: should Sieve throw exceptions instead of silently failing? Defaults to false",
"IgnoreNullsOnNotEqual": "boolean: ignore null values when filtering using is not equal operator? Defaults to true",
"DisableNullableTypeExpressionForSorting": "boolean: disable the creation of nullable type expression for sorting. Some databases do not handle it (yet). Defaults to false"
"ThrowExceptions": "boolean: should Sieve throw exceptions instead of silently failing? Defaults to false"
}
}
```
@@ -160,10 +157,7 @@ More formally:
* `pageSize` is the number of items returned per page
Notes:
* You can use backslashes to escape special characters and sequences:
* commas: `Title@=some\,title` makes a match with "some,title"
* pipes: `Title@=some\|title` makes a match with "some|title"
* null values: `Title@=\null` will search for items with title equal to "null" (not a missing value, but "null"-string literally)
* You can use backslashes to escape commas and pipes within value fields
* You can have spaces anywhere except *within* `{Name}` or `{Operator}` fields
* If you need to look at the data before applying pagination (eg. get total count), use the optional paramters on `Apply` to defer pagination (an [example](https://github.com/Biarity/Sieve/issues/34))
* Here's a [good example on how to work with enumerables](https://github.com/Biarity/Sieve/issues/2)
@@ -208,13 +202,10 @@ You can replace this DSL with your own (eg. use JSON instead) by implementing an
| `<=` | Less than or equal to |
| `@=` | Contains |
| `_=` | Starts with |
| `_-=` | Ends with |
| `!@=` | Does not Contains |
| `!_=` | Does not Starts with |
| `!_-=` | Does not Ends with |
| `@=*` | Case-insensitive string Contains |
| `_=*` | Case-insensitive string Starts with |
| `_-=*` | Case-insensitive string Ends with |
| `==*` | Case-insensitive string Equals |
| `!=*` | Case-insensitive string Not equals |
| `!@=*` | Case-insensitive string does not Contains |
@@ -235,7 +226,7 @@ It is recommended that you write exception-handling middleware to globally handl
You can find an example project incorporating most Sieve concepts in [SieveTests](https://github.com/Biarity/Sieve/tree/master/SieveTests).
## Fluent API
To use the Fluent API instead of attributes in marking properties, setup an alternative `SieveProcessor` that overrides `MapProperties`. For [example](https://github.com/Biarity/Sieve/blob/master/Sieve.Sample/Services/ApplicationSieveProcessor.cs):
To use the Fluent API instead of attributes in marking properties, setup an alternative `SieveProcessor` that overrides `MapProperties`. For example:
```C#
public class ApplicationSieveProcessor : SieveProcessor
@@ -267,78 +258,13 @@ public class ApplicationSieveProcessor : SieveProcessor
}
```
Now you should inject the new class instead:
```C#
services.AddScoped<ISieveProcessor, ApplicationSieveProcessor>();
```
Find More on Sieve's Fluent API [here](https://github.com/Biarity/Sieve/issues/4#issuecomment-364629048).
### Modular Fluent API configuration
Adding all fluent mappings directly in the processor can become unwieldy on larger projects.
It can also clash with vertical architectures.
To enable functional grouping of mappings the `ISieveConfiguration` interface was created together with extensions to the default mapper.
```C#
public class SieveConfigurationForPost : ISieveConfiguration
{
public void Configure(SievePropertyMapper mapper)
{
mapper.Property<Post>(p => p.Title)
.CanFilter()
.HasName("a_different_query_name_here");
mapper.Property<Post>(p => p.CommentCount)
.CanSort();
mapper.Property<Post>(p => p.DateCreated)
.CanSort()
.CanFilter()
.HasName("created_on");
return mapper;
}
}
```
With the processor simplified to:
```C#
public class ApplicationSieveProcessor : SieveProcessor
{
public ApplicationSieveProcessor(
IOptions<SieveOptions> options,
ISieveCustomSortMethods customSortMethods,
ISieveCustomFilterMethods customFilterMethods)
: base(options, customSortMethods, customFilterMethods)
{
}
protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper)
{
return mapper
.ApplyConfiguration<SieveConfigurationForPost>()
.ApplyConfiguration<SieveConfigurationForComment>();
}
}
```
There is also the option to scan and add all configurations for a given assembly
```C#
public class ApplicationSieveProcessor : SieveProcessor
{
public ApplicationSieveProcessor(
IOptions<SieveOptions> options,
ISieveCustomSortMethods customSortMethods,
ISieveCustomFilterMethods customFilterMethods)
: base(options, customSortMethods, customFilterMethods)
{
}
protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper)
{
return mapper.ApplyConfigurationsFromAssembly(typeof(ApplicationSieveProcessor).Assembly);
}
}
```
## Upgrading to v2.2.0
2.2.0 introduced OR logic for filter values. This means your custom filters will need to accept multiple values rather than just the one.

View File

@@ -1,15 +0,0 @@
using Sieve.Services;
namespace Sieve.Sample.Entities
{
public class SieveConfigurationForPost : ISieveConfiguration
{
public void Configure(SievePropertyMapper mapper)
{
mapper.Property<Post>(p => p.Title)
.CanSort()
.CanFilter()
.HasName("CustomTitleName");
}
}
}

View File

@@ -13,18 +13,11 @@ namespace Sieve.Sample.Services
protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper)
{
// Option 1: Map all properties centrally
mapper.Property<Post>(p => p.Title)
.CanSort()
.CanFilter()
.HasName("CustomTitleName");
// Option 2: Manually apply functionally grouped mapping configurations
//mapper.ApplyConfiguration<SieveConfigurationForPost>();
// Option 3: Scan and apply all configurations
//mapper.ApplyConfigurationsFromAssembly(typeof(ApplicationSieveProcessor).Assembly);
return mapper;
}
}

View File

@@ -1,5 +1,6 @@
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",

View File

@@ -4,10 +4,10 @@
},
"Sieve": {
"CaseSensitive": false,
"DefaultPageSize": 10,
"IgnoreNullsOnNotEqual": true
"DefaultPageSize": 10
},
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Warning"

View File

@@ -12,10 +12,9 @@ namespace Sieve.Extensions
string fullPropertyName,
PropertyInfo propertyInfo,
bool desc,
bool useThenBy,
bool disableNullableTypeExpression = false)
bool useThenBy)
{
var lambda = GenerateLambdaWithSafeMemberAccess<TEntity>(fullPropertyName, propertyInfo, disableNullableTypeExpression);
var lambda = GenerateLambdaWithSafeMemberAccess<TEntity>(fullPropertyName, propertyInfo);
var command = desc
? (useThenBy ? "ThenByDescending" : "OrderByDescending")
@@ -34,8 +33,7 @@ namespace Sieve.Extensions
private static Expression<Func<TEntity, object>> GenerateLambdaWithSafeMemberAccess<TEntity>
(
string fullPropertyName,
PropertyInfo propertyInfo,
bool disableNullableTypeExpression
PropertyInfo propertyInfo
)
{
var parameter = Expression.Parameter(typeof(TEntity), "e");
@@ -54,7 +52,7 @@ namespace Sieve.Extensions
propertyValue = Expression.MakeMemberAccess(propertyValue, propertyInfo);
}
if (propertyValue.Type.IsNullable() && !disableNullableTypeExpression)
if (propertyValue.Type.IsNullable())
{
nullCheck = GenerateOrderNullCheckExpression(propertyValue, nullCheck);
}

View File

@@ -10,6 +10,5 @@
LessThanOrEqualTo,
Contains,
StartsWith,
EndsWith,
}
}

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
@@ -7,48 +6,44 @@ namespace Sieve.Models
{
public class FilterTerm : IFilterTerm, IEquatable<FilterTerm>
{
private const string EscapedPipePattern = @"(?<!($|[^\\]|^)(\\\\)*?\\)\|";
private const string OperatorsRegEx = @"(!@=\*|!_=\*|!_-=\*|!=\*|!@=|!_=|!_-=|==\*|@=\*|_=\*|_-=\*|==|!=|>=|<=|>|<|@=|_=|_-=)";
private const string EscapeNegPatternForOper = @"(?<!\\)" + OperatorsRegEx;
private const string EscapePosPatternForOper = @"(?<=\\)" + OperatorsRegEx;
public FilterTerm() { }
private static readonly HashSet<string> _escapedSequences = new HashSet<string>
{
@"\|",
@"\\"
private const string EscapedPipePattern = @"(?<!($|[^\\])(\\\\)*?\\)\|";
private static readonly string[] Operators = new string[] {
"!@=*",
"!_=*",
"!=*",
"!@=",
"!_=",
"==*",
"@=*",
"_=*",
"==",
"!=",
">=",
"<=",
">",
"<",
"@=",
"_="
};
public string Filter
{
set
{
var filterSplits = Regex.Split(value, EscapeNegPatternForOper).Select(t => t.Trim()).ToArray();
var filterSplits = value.Split(Operators, StringSplitOptions.RemoveEmptyEntries)
.Select(t => t.Trim()).ToArray();
Names = Regex.Split(filterSplits[0], EscapedPipePattern).Select(t => t.Trim()).ToArray();
if (filterSplits.Length > 2)
{
foreach (var match in Regex.Matches(filterSplits[2], EscapePosPatternForOper))
{
var matchStr = match.ToString();
filterSplits[2] = filterSplits[2].Replace('\\' + matchStr, matchStr);
}
Values = Regex.Split(filterSplits[2], EscapedPipePattern)
.Select(UnEscape)
.ToArray();
}
Operator = Regex.Match(value, EscapeNegPatternForOper).Value;
Values = filterSplits.Length > 1 ? Regex.Split(filterSplits[1], EscapedPipePattern).Select(t => t.Trim()).ToArray() : null;
Operator = Array.Find(Operators, o => value.Contains(o)) ?? "==";
OperatorParsed = GetOperatorParsed(Operator);
OperatorIsCaseInsensitive = Operator.EndsWith("*");
OperatorIsNegated = OperatorParsed != FilterOperator.NotEquals && Operator.StartsWith("!");
}
}
private string UnEscape(string escapedTerm)
=> _escapedSequences.Aggregate(escapedTerm,
(current, sequence) => Regex.Replace(current, $@"(\\)({sequence})", "$2"));
}
public string[] Names { get; private set; }
@@ -80,9 +75,6 @@ namespace Sieve.Models
case "_=":
case "!_=":
return FilterOperator.StartsWith;
case "_-=":
case "!_-=":
return FilterOperator.EndsWith;
default:
return FilterOperator.Equals;
}
@@ -98,5 +90,6 @@ namespace Sieve.Models
&& Values.SequenceEqual(other.Values)
&& Operator == other.Operator;
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;

View File

@@ -2,16 +2,27 @@
{
public class SieveOptions
{
/// <summary>
/// If flag is set, property names have to match including case sensitivity.
/// </summary>
public bool CaseSensitive { get; set; } = false;
/// <summary>
/// Fallback value of no page size is specified in the request.
/// </summary>
/// <remarks>Values less or equal to 0 disable paging.</remarks>
public int DefaultPageSize { get; set; } = 0;
/// <summary>
/// Specifies the upper limit of a page size to be requested.
/// </summary>
/// <remarks>Values less or equal to 0 are ignored.</remarks>
public int MaxPageSize { get; set; } = 0;
public bool ThrowExceptions { get; set; } = false;
public bool IgnoreNullsOnNotEqual { get; set; } = true;
public bool DisableNullableTypeExpressionForSorting { get; set; } = false;
/// <summary>
/// If flag is set, Sieve throws exception otherwise exceptions are caught and the already processed
/// result is returned.
/// </summary>
public bool ThrowExceptions { get; set; } = true;
}
}

View File

@@ -1,70 +0,0 @@
#nullable enable
using System;
using System.Linq;
using System.Reflection;
namespace Sieve.Services
{
/// <summary>
/// Use this interface to create SieveConfiguration (just like EntityTypeConfigurations are defined for EF)
/// </summary>
public interface ISieveConfiguration
{
/// <summary>
/// Configures sieve property mappings.
/// </summary>
/// <param name="mapper"> The mapper used to configure the sieve properties on. </param>
void Configure(SievePropertyMapper mapper);
}
/// <summary>
/// Configuration extensions to the <see cref="SievePropertyMapper" />
/// </summary>
public static class SieveConfigurationExtensions
{
/// <summary>
/// Applies configuration that is defined in an <see cref="ISieveConfiguration" /> instance.
/// </summary>
/// <param name="mapper"> The mapper to apply the configuration on. </param>
/// <typeparam name="T">The configuration to be applied. </typeparam>
/// <returns>
/// The same <see cref="SievePropertyMapper" /> instance so that additional configuration calls can be chained.
/// </returns>
public static SievePropertyMapper ApplyConfiguration<T>(this SievePropertyMapper mapper) where T : ISieveConfiguration, new()
{
var configuration = new T();
configuration.Configure(mapper);
return mapper;
}
/// <summary>
/// Applies configuration from all <see cref="ISieveConfiguration" />
/// instances that are defined in provided assembly.
/// </summary>
/// <param name="mapper"> The mapper to apply the configuration on. </param>
/// <param name="assembly"> The assembly to scan. </param>
/// <returns>
/// The same <see cref="SievePropertyMapper" /> instance so that additional configuration calls can be chained.
/// </returns>
public static SievePropertyMapper ApplyConfigurationsFromAssembly(this SievePropertyMapper mapper, Assembly assembly)
{
foreach (var type in assembly.GetTypes().Where(t => !t.IsAbstract && !t.IsGenericTypeDefinition))
{
// Only accept types that contain a parameterless constructor, are not abstract.
var noArgConstructor = type.GetConstructor(Type.EmptyTypes);
if (noArgConstructor is null)
{
continue;
}
if (type.GetInterfaces().Any(t => t == typeof(ISieveConfiguration)))
{
var configuration = (ISieveConfiguration)noArgConstructor.Invoke(new object?[] { });
configuration.Configure(mapper);
}
}
return mapper;
}
}
}

View File

@@ -68,7 +68,7 @@ namespace Sieve.Services
where TSortTerm : ISortTerm, new()
{
private const string NullFilterValue = "null";
private const char EscapeChar = '\\';
private readonly IOptions<SieveOptions> _options;
private readonly ISieveCustomSortMethods _customSortMethods;
private readonly ISieveCustomFilterMethods _customFilterMethods;
private readonly SievePropertyMapper _mapper = new SievePropertyMapper();
@@ -78,7 +78,7 @@ namespace Sieve.Services
ISieveCustomFilterMethods customFilterMethods)
{
_mapper = MapProperties(_mapper);
Options = options;
_options = options;
_customSortMethods = customSortMethods;
_customFilterMethods = customFilterMethods;
}
@@ -87,7 +87,7 @@ namespace Sieve.Services
ISieveCustomSortMethods customSortMethods)
{
_mapper = MapProperties(_mapper);
Options = options;
_options = options;
_customSortMethods = customSortMethods;
}
@@ -95,18 +95,16 @@ namespace Sieve.Services
ISieveCustomFilterMethods customFilterMethods)
{
_mapper = MapProperties(_mapper);
Options = options;
_options = options;
_customFilterMethods = customFilterMethods;
}
public SieveProcessor(IOptions<SieveOptions> options)
{
_mapper = MapProperties(_mapper);
Options = options;
_options = options;
}
protected IOptions<SieveOptions> Options { get; }
/// <summary>
/// Apply filtering, sorting, and pagination parameters found in `model` to `source`
/// </summary>
@@ -150,7 +148,7 @@ namespace Sieve.Services
}
catch (Exception ex)
{
if (!Options.Value.ThrowExceptions)
if (!_options.Value.ThrowExceptions)
{
return result;
}
@@ -164,7 +162,7 @@ namespace Sieve.Services
}
}
protected virtual 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)
@@ -200,7 +198,7 @@ namespace Sieve.Services
? Expression.Constant(null, property.PropertyType)
: ConvertStringValueToConstantExpression(filterTermValue, property, converter);
if (filterTerm.OperatorIsCaseInsensitive && !isFilterTermValueNull)
if (filterTerm.OperatorIsCaseInsensitive)
{
propertyValue = Expression.Call(propertyValue,
typeof(string).GetMethods()
@@ -218,13 +216,10 @@ namespace Sieve.Services
expression = Expression.Not(expression);
}
if (expression.NodeType != ExpressionType.NotEqual || Options.Value.IgnoreNullsOnNotEqual)
var filterValueNullCheck = GetFilterValueNullCheck(parameter, fullPropertyName, isFilterTermValueNull);
if (filterValueNullCheck != null)
{
var filterValueNullCheck = GetFilterValueNullCheck(parameter, fullPropertyName, isFilterTermValueNull);
if (filterValueNullCheck != null)
{
expression = Expression.AndAlso(filterValueNullCheck, expression);
}
expression = Expression.AndAlso(filterValueNullCheck, expression);
}
innerExpression = innerExpression == null
@@ -258,7 +253,8 @@ namespace Sieve.Services
: result.Where(Expression.Lambda<Func<TEntity, bool>>(outerExpression, parameter));
}
private static Expression GetFilterValueNullCheck(Expression parameter, string fullPropertyName, bool isFilterTermValueNull)
private static Expression GetFilterValueNullCheck(Expression parameter, string fullPropertyName,
bool isFilterTermValueNull)
{
var (propertyValue, nullCheck) = GetPropertyValueAndNullCheckExpression(parameter, fullPropertyName);
@@ -312,10 +308,6 @@ namespace Sieve.Services
private static Expression ConvertStringValueToConstantExpression(string value, PropertyInfo property,
TypeConverter converter)
{
// to allow user to distinguish between prop==null (as null) and prop==\null (as "null"-string)
value = value.Equals(EscapeChar + NullFilterValue, StringComparison.InvariantCultureIgnoreCase)
? value.TrimStart(EscapeChar)
: value;
dynamic constantVal = converter.CanConvertFrom(typeof(string))
? converter.ConvertFrom(value)
: Convert.ChangeType(value, property.PropertyType);
@@ -339,21 +331,20 @@ namespace Sieve.Services
FilterOperator.StartsWith => Expression.Call(propertyValue,
typeof(string).GetMethods().First(m => m.Name == "StartsWith" && m.GetParameters().Length == 1),
filterValue),
FilterOperator.EndsWith => Expression.Call(propertyValue,
typeof(string).GetMethods().First(m => m.Name == "EndsWith" && 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 static Expression GetClosureOverConstant<T>(T constant, Type targetType)
{
Expression<Func<T>> hoistedConstant = () => constant;
return Expression.Convert(hoistedConstant.Body, targetType);
return Expression.Constant(constant, targetType);
}
protected virtual 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)
@@ -368,7 +359,7 @@ namespace Sieve.Services
if (property != null)
{
result = result.OrderByDynamic(fullName, property, sortTerm.Descending, useThenBy, Options.Value.DisableNullableTypeExpressionForSorting);
result = result.OrderByDynamic(fullName, property, sortTerm.Descending, useThenBy);
}
else
{
@@ -382,11 +373,15 @@ namespace Sieve.Services
return result;
}
protected virtual 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;
var pageSize = model?.PageSize ?? _options.Value.DefaultPageSize;
if (_options.Value.MaxPageSize > 0)
{
pageSize = Math.Min(pageSize, _options.Value.MaxPageSize);
}
if (pageSize <= 0)
{
@@ -394,7 +389,7 @@ namespace Sieve.Services
}
result = result.Skip((page - 1) * pageSize);
result = result.Take(Math.Min(pageSize, maxPageSize));
result = result.Take(pageSize);
return result;
}
@@ -408,14 +403,14 @@ namespace Sieve.Services
string name)
{
var property = _mapper.FindProperty<TEntity>(canSortRequired, canFilterRequired, name,
Options.Value.CaseSensitive);
_options.Value.CaseSensitive);
if (property.Item1 != null)
{
return property;
}
var prop = FindPropertyBySieveAttribute<TEntity>(canSortRequired, canFilterRequired, name,
Options.Value.CaseSensitive);
_options.Value.CaseSensitive);
return (prop?.Name, prop);
}
@@ -435,7 +430,7 @@ namespace Sieve.Services
{
var customMethod = parent?.GetType()
.GetMethodExt(name,
Options.Value.CaseSensitive
_options.Value.CaseSensitive
? BindingFlags.Default
: BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance,
typeof(IQueryable<TEntity>));
@@ -446,7 +441,7 @@ namespace Sieve.Services
// Find generic methods `public IQueryable<T> Filter<T>(IQueryable<T> source, ...)`
var genericCustomMethod = parent?.GetType()
.GetMethodExt(name,
Options.Value.CaseSensitive
_options.Value.CaseSensitive
? BindingFlags.Default
: BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance,
typeof(IQueryable<>));
@@ -490,11 +485,11 @@ namespace Sieve.Services
var incompatibleCustomMethods =
parent?
.GetType()
.GetMethods(Options.Value.CaseSensitive
.GetMethods(_options.Value.CaseSensitive
? BindingFlags.Default
: BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance)
.Where(method => string.Equals(method.Name, name,
Options.Value.CaseSensitive
_options.Value.CaseSensitive
? StringComparison.InvariantCulture
: StringComparison.InvariantCultureIgnoreCase))
.ToList()

View File

@@ -1,37 +0,0 @@
using Sieve.Services;
namespace SieveUnitTests.Abstractions.Entity
{
public class SieveConfigurationForIPost : ISieveConfiguration
{
public void Configure(SievePropertyMapper mapper)
{
mapper.Property<IPost>(p => p.ThisHasNoAttributeButIsAccessible)
.CanSort()
.CanFilter()
.HasName("shortname");
mapper.Property<IPost>(p => p.TopComment.Text)
.CanFilter();
mapper.Property<IPost>(p => p.TopComment.Id)
.CanSort();
mapper.Property<IPost>(p => p.OnlySortableViaFluentApi)
.CanSort();
mapper.Property<IPost>(p => p.TopComment.Text)
.CanFilter()
.HasName("topc");
mapper.Property<IPost>(p => p.FeaturedComment.Text)
.CanFilter()
.HasName("featc");
mapper
.Property<IPost>(p => p.DateCreated)
.CanSort()
.HasName("CreateDate");
}
}
}

View File

@@ -1,37 +0,0 @@
using Sieve.Services;
namespace SieveUnitTests.Entities
{
public class SieveConfigurationForPost : ISieveConfiguration
{
public void Configure(SievePropertyMapper mapper)
{
mapper.Property<Post>(p => p.ThisHasNoAttributeButIsAccessible)
.CanSort()
.CanFilter()
.HasName("shortname");
mapper.Property<Post>(p => p.TopComment.Text)
.CanFilter();
mapper.Property<Post>(p => p.TopComment.Id)
.CanSort();
mapper.Property<Post>(p => p.OnlySortableViaFluentApi)
.CanSort();
mapper.Property<Post>(p => p.TopComment.Text)
.CanFilter()
.HasName("topc");
mapper.Property<Post>(p => p.FeaturedComment.Text)
.CanFilter()
.HasName("featc");
mapper
.Property<Post>(p => p.DateCreated)
.CanSort()
.HasName("CreateDate");
}
}
}

View File

@@ -15,24 +15,16 @@ namespace SieveUnitTests
{
private readonly ITestOutputHelper _testOutputHelper;
private readonly SieveProcessor _processor;
private readonly SieveProcessor _nullableProcessor;
private readonly IQueryable<Post> _posts;
private readonly IQueryable<Comment> _comments;
public General(ITestOutputHelper testOutputHelper)
{
var nullableAccessor = new SieveOptionsAccessor();
nullableAccessor.Value.IgnoreNullsOnNotEqual = false;
_testOutputHelper = testOutputHelper;
_processor = new ApplicationSieveProcessor(new SieveOptionsAccessor(),
new SieveCustomSortMethods(),
new SieveCustomFilterMethods());
_nullableProcessor = new ApplicationSieveProcessor(nullableAccessor,
new SieveCustomSortMethods(),
new SieveCustomFilterMethods());
_posts = new List<Post>
{
new Post
@@ -74,16 +66,6 @@ namespace SieveUnitTests
TopComment = new Comment { Id = 1, Text = "D1" },
FeaturedComment = new Comment { Id = 7, Text = "D2" }
},
new Post
{
Id = 4,
Title = "Yen",
LikeCount = 5,
IsDraft = true,
CategoryId = 5,
TopComment = new Comment { Id = 4, Text = "Yen3" },
FeaturedComment = new Comment { Id = 8, Text = "Yen4" }
}
}.AsQueryable();
_comments = new List<Comment>
@@ -134,43 +116,7 @@ namespace SieveUnitTests
var result = _processor.Apply(model, _posts);
Assert.Equal(1, result.First().Id);
Assert.True(result.Count() == 4);
}
[Fact]
public void EndsWithWorks()
{
var model = new SieveModel
{
Filters = "Title_-=n"
};
_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.Equal(4, result.First().Id);
Assert.True(result.Count() == 1);
}
[Fact]
public void EndsWithCanBeCaseInsensitive()
{
var model = new SieveModel
{
Filters = "Title_-=*N"
};
_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.Equal(4, result.First().Id);
Assert.True(result.Count() == 1);
Assert.True(result.Count() == 3);
}
[Fact]
@@ -196,7 +142,7 @@ namespace SieveUnitTests
var result = _processor.Apply(model, _posts);
Assert.True(result.Count() == 4);
Assert.True(result.Count() == 3);
}
[Fact]
@@ -234,27 +180,10 @@ namespace SieveUnitTests
};
var result = _processor.Apply(model, _posts);
var nullableResult = _nullableProcessor.Apply(model, _posts);
Assert.True(result.Count() == 2);
Assert.True(nullableResult.Count() == 2);
}
[Fact]
public void CanFilterNullableIntsWithNotEqual()
{
var model = new SieveModel()
{
Filters = "CategoryId!=1"
};
var result = _processor.Apply(model, _posts);
var nullableResult = _nullableProcessor.Apply(model, _posts);
Assert.True(result.Count() == 2);
Assert.True(nullableResult.Count() == 3);
}
[Theory]
[InlineData(@"Text@=*\,")]
[InlineData(@"Text@=*\, ")]
@@ -301,7 +230,7 @@ namespace SieveUnitTests
var result = _processor.Apply(model, _posts);
Assert.False(result.Any(p => p.Id == 0));
Assert.True(result.Count() == 4);
Assert.True(result.Count() == 3);
}
[Fact]
@@ -520,12 +449,11 @@ namespace SieveUnitTests
};
var result = _processor.Apply(model, _posts);
Assert.Equal(4, result.Count());
Assert.Equal(3, result.Count());
var posts = result.ToList();
Assert.Contains("B", posts[0].TopComment.Text);
Assert.Contains("C", posts[1].TopComment.Text);
Assert.Contains("D", posts[2].TopComment.Text);
Assert.Contains("Yen", posts[3].TopComment.Text);
}
[Fact]
@@ -537,13 +465,12 @@ namespace SieveUnitTests
};
var result = _processor.Apply(model, _posts);
Assert.Equal(5, result.Count());
Assert.Equal(4, result.Count());
var posts = result.ToList();
Assert.Equal(0, posts[0].Id);
Assert.Equal(3, posts[1].Id);
Assert.Equal(2, posts[2].Id);
Assert.Equal(1, posts[3].Id);
Assert.Equal(4, posts[4].Id);
}
[Fact]
@@ -678,175 +605,52 @@ namespace SieveUnitTests
};
var result = _processor.Apply(model, _posts);
Assert.Equal(5, result.Count());
Assert.Equal(4, result.Count());
var posts = result.ToList();
Assert.Equal(4, posts[0].Id);
Assert.Equal(3,posts[1].Id);
Assert.Equal(2,posts[2].Id);
Assert.Equal(1,posts[3].Id);
Assert.Equal(0,posts[4].Id);
}
[Fact]
public void CanFilter_WithEscapeCharacter()
{
var comments = new List<Comment>
{
new Comment
{
Id = 0,
DateCreated = DateTimeOffset.UtcNow,
Text = "Here is, a comment"
},
new Comment
{
Id = 1,
DateCreated = DateTimeOffset.UtcNow.AddDays(-1),
Text = "Here is, another comment"
},
}.AsQueryable();
var model = new SieveModel
{
Filters = "Text==Here is\\, another comment"
};
var result = _processor.Apply(model, comments);
Assert.Equal(1, result.Count());
}
[Fact]
public void OrEscapedPipeValueFilteringWorks()
{
var comments = new List<Comment>
{
new Comment
{
Id = 0,
DateCreated = DateTimeOffset.UtcNow,
Text = "Here is | a comment"
},
new Comment
{
Id = 1,
DateCreated = DateTimeOffset.UtcNow.AddDays(-1),
Text = "Here is | another comment"
},
new Comment
{
Id = 2,
DateCreated = DateTimeOffset.UtcNow.AddDays(-1),
Text = @"Here is \| another comment(1)"
}
}.AsQueryable();
var model = new SieveModel
{
Filters = @"Text==Here is \| a comment|Here is \| another comment|Here is \\\| another comment(1)",
};
var result = _processor.Apply(model, comments);
Assert.Equal(3, result.Count());
Assert.Equal(3,posts[0].Id);
Assert.Equal(2,posts[1].Id);
Assert.Equal(1,posts[2].Id);
Assert.Equal(0,posts[3].Id);
}
[Theory]
[InlineData("CategoryId==1,(CategoryId|LikeCount)==50")]
[InlineData("(CategoryId|LikeCount)==50,CategoryId==1")]
public void CanFilterWithEscape(string filter)
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
public void Paging_DifferentPages(int page)
{
var model = new SieveModel
{
Filters = filter
Page = page,
PageSize = 1,
};
var result = _processor.Apply(model, _posts);
var entry = result.FirstOrDefault();
var resultCount = result.Count();
Assert.NotNull(entry);
Assert.Equal(1, resultCount);
var posts = result.ToList();
Assert.Single(posts);
var expectedId = page - 1;
Assert.Equal(expectedId, posts.First().Id);
}
[Theory]
[InlineData(@"Title@=\\")]
public void CanFilterWithEscapedBackSlash(string filter)
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
public void Paging_DifferentPageSizes(int pageSize)
{
var posts = new List<Post>
{
new Post
{
Id = 1,
Title = "E\\",
LikeCount = 4,
IsDraft = true,
CategoryId = 1,
TopComment = new Comment { Id = 1, Text = "E1" },
FeaturedComment = new Comment { Id = 7, Text = "E2" }
}
}.AsQueryable();
var model = new SieveModel
{
Filters = filter
Page = 1,
PageSize = pageSize,
};
var result = _processor.Apply(model, posts);
var entry = result.FirstOrDefault();
var resultCount = result.Count();
var result = _processor.Apply(model, _posts);
Assert.NotNull(entry);
Assert.Equal(1, resultCount);
Assert.Equal(pageSize, result.Count());
}
[Theory]
[InlineData(@"Title@=\== ")]
[InlineData(@"Title@=\!= ")]
[InlineData(@"Title@=\> ")]
[InlineData(@"Title@=\< ")]
[InlineData(@"Title@=\<= ")]
[InlineData(@"Title@=\>= ")]
[InlineData(@"Title@=\@= ")]
[InlineData(@"Title@=\_= ")]
[InlineData(@"Title@=\_-= ")]
[InlineData(@"Title@=!\@= ")]
[InlineData(@"Title@=!\_= ")]
[InlineData(@"Title@=!\_-= ")]
[InlineData(@"Title@=\@=* ")]
[InlineData(@"Title@=\_=* ")]
[InlineData(@"Title@=\_-=* ")]
[InlineData(@"Title@=\==* ")]
[InlineData(@"Title@=\!=* ")]
[InlineData(@"Title@=!\@=* ")]
public void CanFilterWithEscapedOperators(string filter)
{
var posts = new List<Post>
{
new Post
{
Id = 1,
Title = @"Operators: == != > < >= <= @= _= _-= !@= !_= !_-= @=* _=* ==* !=* !@=* !_=* !_-=* ",
LikeCount = 1,
IsDraft = true,
CategoryId = 1,
TopComment = new Comment { Id = 1, Text = "F1" },
FeaturedComment = new Comment { Id = 7, Text = "F2" }
}
}.AsQueryable();
var model = new SieveModel
{
Filters = filter,
};
var result = _processor.Apply(model, posts);
var entry = result.FirstOrDefault();
var resultCount = result.Count();
Assert.NotNull(entry);
Assert.Equal(1, resultCount);
}
}
}

View File

@@ -16,24 +16,16 @@ namespace SieveUnitTests
{
private readonly ITestOutputHelper _testOutputHelper;
private readonly SieveProcessor _processor;
private readonly SieveProcessor _nullableProcessor;
private readonly IQueryable<IPost> _posts;
private readonly IQueryable<Comment> _comments;
public GeneralWithInterfaces(ITestOutputHelper testOutputHelper)
{
var nullableAccessor = new SieveOptionsAccessor();
nullableAccessor.Value.IgnoreNullsOnNotEqual = false;
_testOutputHelper = testOutputHelper;
_processor = new ApplicationSieveProcessor(new SieveOptionsAccessor(),
new SieveCustomSortMethods(),
new SieveCustomFilterMethods());
_nullableProcessor = new ApplicationSieveProcessor(nullableAccessor,
new SieveCustomSortMethods(),
new SieveCustomFilterMethods());
_posts = new List<IPost>
{
new Post
@@ -189,25 +181,8 @@ namespace SieveUnitTests
};
var result = _processor.Apply(model, _posts);
var nullableResult = _nullableProcessor.Apply(model, _posts);
Assert.True(result.Count() == 2);
Assert.True(nullableResult.Count() == 2);
}
[Fact]
public void CanFilterNullableIntsWithNotEqual()
{
var model = new SieveModel()
{
Filters = "CategoryId!=1"
};
var result = _processor.Apply(model, _posts);
var nullableResult = _nullableProcessor.Apply(model, _posts);
Assert.True(result.Count() == 1);
Assert.True(nullableResult.Count() == 2);
}
[Fact]

View File

@@ -2,7 +2,6 @@ using System.Collections.Generic;
using System.Linq;
using Sieve.Exceptions;
using Sieve.Models;
using Sieve.Services;
using SieveUnitTests.Entities;
using SieveUnitTests.Services;
using Xunit;
@@ -11,10 +10,15 @@ namespace SieveUnitTests
{
public class Mapper
{
private readonly ApplicationSieveProcessor _processor;
private readonly IQueryable<Post> _posts;
public Mapper()
{
_processor = new ApplicationSieveProcessor(new SieveOptionsAccessor(),
new SieveCustomSortMethods(),
new SieveCustomFilterMethods());
_posts = new List<Post>
{
new Post
@@ -41,49 +45,23 @@ namespace SieveUnitTests
}.AsQueryable();
}
/// <summary>
/// Processors with the same mappings but configured via a different method.
/// </summary>
/// <returns></returns>
public static IEnumerable<object[]> GetProcessors()
{
yield return new object[] {
new ApplicationSieveProcessor(
new SieveOptionsAccessor(),
new SieveCustomSortMethods(),
new SieveCustomFilterMethods())};
yield return new object[] {
new ModularConfigurationSieveProcessor(
new SieveOptionsAccessor(),
new SieveCustomSortMethods(),
new SieveCustomFilterMethods())};
yield return new object[] {
new ModularConfigurationWithScanSieveProcessor(
new SieveOptionsAccessor(),
new SieveCustomSortMethods(),
new SieveCustomFilterMethods())};
}
[Theory]
[MemberData(nameof(GetProcessors))]
public void MapperWorks(ISieveProcessor processor)
[Fact]
public void MapperWorks()
{
var model = new SieveModel
{
Filters = "shortname@=A",
};
var result = processor.Apply(model, _posts);
var result = _processor.Apply(model, _posts);
Assert.Equal("A", result.First().ThisHasNoAttributeButIsAccessible);
Assert.True(result.Count() == 1);
}
[Theory]
[MemberData(nameof(GetProcessors))]
public void MapperSortOnlyWorks(ISieveProcessor processor)
[Fact]
public void MapperSortOnlyWorks()
{
var model = new SieveModel
{
@@ -91,9 +69,9 @@ namespace SieveUnitTests
Sorts = "OnlySortableViaFluentApi"
};
var result = processor.Apply(model, _posts, applyFiltering: false, applyPagination: false);
var result = _processor.Apply(model, _posts, applyFiltering: false, applyPagination: false);
Assert.Throws<SieveMethodNotFoundException>(() => processor.Apply(model, _posts));
Assert.Throws<SieveMethodNotFoundException>(() => _processor.Apply(model, _posts));
Assert.Equal(3, result.First().Id);

View File

@@ -1,26 +0,0 @@
using Microsoft.Extensions.Options;
using Sieve.Models;
using Sieve.Services;
using SieveUnitTests.Abstractions.Entity;
using SieveUnitTests.Entities;
namespace SieveUnitTests.Services
{
public class ModularConfigurationSieveProcessor : SieveProcessor
{
public ModularConfigurationSieveProcessor(
IOptions<SieveOptions> options,
ISieveCustomSortMethods customSortMethods,
ISieveCustomFilterMethods customFilterMethods)
: base(options, customSortMethods, customFilterMethods)
{
}
protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper)
{
return mapper
.ApplyConfiguration<SieveConfigurationForPost>()
.ApplyConfiguration<SieveConfigurationForIPost>();
}
}
}

View File

@@ -1,20 +0,0 @@
using Microsoft.Extensions.Options;
using Sieve.Models;
using Sieve.Services;
namespace SieveUnitTests.Services
{
public class ModularConfigurationWithScanSieveProcessor : SieveProcessor
{
public ModularConfigurationWithScanSieveProcessor(
IOptions<SieveOptions> options,
ISieveCustomSortMethods customSortMethods,
ISieveCustomFilterMethods customFilterMethods)
: base(options, customSortMethods, customFilterMethods)
{
}
protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper) =>
mapper.ApplyConfigurationsFromAssembly(typeof(ModularConfigurationWithScanSieveProcessor).Assembly);
}
}

View File

@@ -31,14 +31,14 @@ namespace SieveUnitTests
{
Id = 1,
DateCreated = DateTimeOffset.UtcNow,
Text = "null is here twice in the text ending by null",
Text = "null is here in the text",
Author = "Cat",
},
new Comment
{
Id = 2,
DateCreated = DateTimeOffset.UtcNow,
Text = "Regular comment without n*ll",
Text = "Regular comment without n*ll.",
Author = "Mouse",
},
new Comment
@@ -47,28 +47,24 @@ namespace SieveUnitTests
DateCreated = DateTimeOffset.UtcNow,
Text = null,
Author = "null",
}
},
}.AsQueryable();
}
[Theory]
[InlineData("Text==null")]
[InlineData("Text==*null")]
public void Filter_Equals_Null(string filter)
[Fact]
public void Filter_Equals_Null()
{
var model = new SieveModel {Filters = filter};
var model = new SieveModel {Filters = "Text==null"};
var result = _processor.Apply(model, _comments);
Assert.Equal(100, result.Single().Id);
}
[Theory]
[InlineData("Text!=null")]
[InlineData("Text!=*null")]
public void Filter_NotEquals_Null(string filter)
[Fact]
public void Filter_NotEquals_Null()
{
var model = new SieveModel {Filters = filter};
var model = new SieveModel {Filters = "Text!=null"};
var result = _processor.Apply(model, _comments);
@@ -105,22 +101,6 @@ namespace SieveUnitTests
Assert.Equal(expectedIds, result.Select(p => p.Id));
}
[Theory]
[InlineData(@"Author==\null", 100)]
[InlineData(@"Author==*\null", 100)]
[InlineData(@"Author==*\NuLl", 100)]
[InlineData(@"Author!=*\null", 0, 1, 2)]
[InlineData(@"Author!=*\NulL", 0, 1, 2)]
[InlineData(@"Author!=\null", 0, 1, 2)]
public void SingleFilter_Equals_NullStringEscaped(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")]
@@ -136,21 +116,6 @@ namespace SieveUnitTests
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_EndsWith_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")]
@@ -179,19 +144,5 @@ namespace SieveUnitTests
Assert.Equal(new[] {0, 2}, result.Select(p => p.Id));
}
[Theory]
[InlineData("Text!_-=null")]
[InlineData("Text!_-=*null")]
[InlineData("Text!_-=*NULL")]
[InlineData("Text!_-=*NulL")]
public void Filter_DoesNotEndsWith_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));
}
}
}

View File

@@ -21,8 +21,7 @@ using static Nuke.Common.Tools.DotNet.DotNetTasks;
InvokedTargets = new[] {nameof(Ci)},
CacheKeyFiles = new string[0])]
[GitHubActions("ci_publish", GitHubActionsImage.UbuntuLatest,
OnPushBranches = new[] { "releases/*" },
OnPushTags = new[] { "v*" },
OnPushBranches = new[] {"releases/*"},
AutoGenerate = true,
InvokedTargets = new[] {nameof(CiPublish)},
CacheKeyFiles = new string[0],
@@ -84,7 +83,6 @@ class Build : NukeBuild
Target Package => _ => _
.DependsOn(Test)
.Executes(() =>
{
DotNetPack(s => s
.SetProject(SieveProject)