mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Parse new filters
This commit is contained in:
parent
e9aaa184cf
commit
edc6d11824
@ -14,6 +14,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Sprache" Version="2.3.1" />
|
||||
<PackageReference Include="System.ComponentModel.Composition" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -17,8 +17,13 @@
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using Sprache;
|
||||
|
||||
namespace Kyoo.Abstractions.Models.Utils;
|
||||
|
||||
public abstract record Filter
|
||||
@ -46,24 +51,154 @@ public abstract record Filter<T> : Filter
|
||||
|
||||
public record Eq(string property, object value) : Filter<T>;
|
||||
|
||||
public record Ne<T2>(string property, T2 value) : Filter<T>;
|
||||
public record Ne(string property, object value) : Filter<T>;
|
||||
|
||||
public record Gt<T2>(string property, T2 value) : Filter<T>;
|
||||
public record Gt(string property, object value) : Filter<T>;
|
||||
|
||||
public record Ge<T2>(string property, T2 value) : Filter<T>;
|
||||
public record Ge(string property, object value) : Filter<T>;
|
||||
|
||||
public record Lt<T2>(string property, T2 value) : Filter<T>;
|
||||
public record Lt(string property, object value) : Filter<T>;
|
||||
|
||||
public record Le<T2>(string property, T2 value) : Filter<T>;
|
||||
public record Le(string property, object value) : Filter<T>;
|
||||
|
||||
public record Has<T2>(string property, T2 value) : Filter<T>;
|
||||
public record Has(string property, object value) : Filter<T>;
|
||||
|
||||
public record In(string property, object[] value) : Filter<T>;
|
||||
|
||||
public record Lambda(Expression<Func<T, bool>> lambda) : Filter<T>;
|
||||
|
||||
public static Filter<T> From(string filter)
|
||||
public static class FilterParsers
|
||||
{
|
||||
public static readonly Parser<Filter<T>> Filter =
|
||||
Parse.Ref(() => Bracket)
|
||||
.Or(Parse.Ref(() => Not))
|
||||
.Or(Parse.Ref(() => Eq))
|
||||
.Or(Parse.Ref(() => Ne))
|
||||
.Or(Parse.Ref(() => Gt))
|
||||
.Or(Parse.Ref(() => Ge))
|
||||
.Or(Parse.Ref(() => Lt))
|
||||
.Or(Parse.Ref(() => Le));
|
||||
|
||||
public static readonly Parser<Filter<T>> CompleteFilter =
|
||||
Parse.Ref(() => Or)
|
||||
.Or(Parse.Ref(() => And))
|
||||
.Or(Filter);
|
||||
|
||||
public static readonly Parser<Filter<T>> Bracket =
|
||||
from open in Parse.Char('(').Token()
|
||||
from filter in CompleteFilter
|
||||
from close in Parse.Char(')').Token()
|
||||
select filter;
|
||||
|
||||
public static readonly Parser<IEnumerable<char>> AndOperator = Parse.IgnoreCase("and")
|
||||
.Or(Parse.String("&&"))
|
||||
.Token();
|
||||
|
||||
public static readonly Parser<IEnumerable<char>> OrOperator = Parse.IgnoreCase("or")
|
||||
.Or(Parse.String("||"))
|
||||
.Token();
|
||||
|
||||
public static readonly Parser<Filter<T>> And = Parse.ChainOperator(AndOperator, Filter, (_, a, b) => new Filter<T>.And(a, b));
|
||||
|
||||
public static readonly Parser<Filter<T>> Or = Parse.ChainOperator(OrOperator, And.Or(Filter), (_, a, b) => new Filter<T>.Or(a, b));
|
||||
|
||||
public static readonly Parser<Filter<T>> Not =
|
||||
from not in Parse.IgnoreCase("not")
|
||||
.Or(Parse.String("!"))
|
||||
.Token()
|
||||
from filter in CompleteFilter
|
||||
select new Filter<T>.Not(filter);
|
||||
|
||||
private static Parser<Filter<T>> _GetOperationParser(Parser<object> op, Func<string, object, Filter<T>> apply)
|
||||
{
|
||||
Parser<string> property = Parse.LetterOrDigit.AtLeastOnce().Text();
|
||||
|
||||
return property.Then(prop =>
|
||||
{
|
||||
PropertyInfo? propInfo = typeof(T).GetProperty(prop, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
|
||||
if (propInfo == null)
|
||||
throw new ValidationException($"The given filter {property} is invalid.");
|
||||
|
||||
Parser<object> value;
|
||||
|
||||
if (propInfo.PropertyType == typeof(int))
|
||||
value = Parse.Number.Select(x => int.Parse(x) as object);
|
||||
else if (propInfo.PropertyType == typeof(float))
|
||||
{
|
||||
value =
|
||||
from a in Parse.Number
|
||||
from dot in Parse.Char('.')
|
||||
from b in Parse.Number
|
||||
select float.Parse($"{a}.{b}") as object;
|
||||
}
|
||||
else if (propInfo.PropertyType == typeof(string))
|
||||
{
|
||||
value = (
|
||||
from lq in Parse.Char('"').Or(Parse.Char('\''))
|
||||
from str in Parse.AnyChar.Where(x => x is not '"' and not '\'').Many().Text()
|
||||
from rq in Parse.Char('"').Or(Parse.Char('\''))
|
||||
select str
|
||||
).Or(Parse.LetterOrDigit.Many().Text());
|
||||
}
|
||||
else if (propInfo.PropertyType.IsEnum)
|
||||
{
|
||||
value = Parse.LetterOrDigit.Many().Text().Select(x =>
|
||||
{
|
||||
if (Enum.TryParse(propInfo.PropertyType, x, true, out object? value))
|
||||
return value!;
|
||||
throw new ValidationException($"Invalid enum value. Unexpected {x}");
|
||||
});
|
||||
}
|
||||
// TODO: Support arrays
|
||||
else
|
||||
throw new ValidationException("Unfilterable field found");
|
||||
|
||||
return
|
||||
from eq in op
|
||||
from val in value
|
||||
select apply(prop, val);
|
||||
});
|
||||
}
|
||||
|
||||
public static readonly Parser<Filter<T>> Eq = _GetOperationParser(
|
||||
Parse.IgnoreCase("eq").Or(Parse.String("=")).Token(),
|
||||
(property, value) => new Eq(property, value)
|
||||
);
|
||||
|
||||
public static readonly Parser<Filter<T>> Ne = _GetOperationParser(
|
||||
Parse.IgnoreCase("ne").Or(Parse.String("!=")).Token(),
|
||||
(property, value) => new Ne(property, value)
|
||||
);
|
||||
|
||||
public static readonly Parser<Filter<T>> Gt = _GetOperationParser(
|
||||
Parse.IgnoreCase("gt").Or(Parse.String(">")).Token(),
|
||||
(property, value) => new Gt(property, value)
|
||||
);
|
||||
|
||||
public static readonly Parser<Filter<T>> Ge = _GetOperationParser(
|
||||
Parse.IgnoreCase("ge").Or(Parse.String(">=")).Token(),
|
||||
(property, value) => new Ge(property, value)
|
||||
);
|
||||
|
||||
public static readonly Parser<Filter<T>> Lt = _GetOperationParser(
|
||||
Parse.IgnoreCase("lt").Or(Parse.String("<")).Token(),
|
||||
(property, value) => new Lt(property, value)
|
||||
);
|
||||
|
||||
public static readonly Parser<Filter<T>> Le = _GetOperationParser(
|
||||
Parse.IgnoreCase("le").Or(Parse.String("<=")).Token(),
|
||||
(property, value) => new Le(property, value)
|
||||
);
|
||||
}
|
||||
|
||||
public static Filter<T>? From(string? filter)
|
||||
{
|
||||
if (filter == null)
|
||||
return null;
|
||||
|
||||
IResult<Filter<T>> ret = FilterParsers.CompleteFilter.End().TryParse(filter);
|
||||
if (ret.WasSuccessful)
|
||||
return ret.Value;
|
||||
throw new ValidationException($"Could not parse filter argument: {ret.Message}. Not parsed: {filter[ret.Remainder.Position..]}");
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ namespace Kyoo.Abstractions.Controllers
|
||||
/// <summary>
|
||||
/// Information about the pagination. How many items should be displayed and where to start.
|
||||
/// </summary>
|
||||
public struct Pagination
|
||||
public class Pagination
|
||||
{
|
||||
/// <summary>
|
||||
/// The count of items to return.
|
||||
|
@ -80,6 +80,7 @@ namespace Kyoo.Core
|
||||
options.Filters.Add<ExceptionFilter>();
|
||||
options.ModelBinderProviders.Insert(0, new SortBinder.Provider());
|
||||
options.ModelBinderProviders.Insert(0, new IncludeBinder.Provider());
|
||||
options.ModelBinderProviders.Insert(0, new FilterBinder.Provider());
|
||||
})
|
||||
.AddNewtonsoftJson(x =>
|
||||
{
|
||||
|
@ -48,9 +48,6 @@ namespace Kyoo.Core
|
||||
{
|
||||
switch (context.Exception)
|
||||
{
|
||||
case ArgumentException ex:
|
||||
context.Result = new BadRequestObjectResult(new RequestError(ex.Message));
|
||||
break;
|
||||
case ValidationException ex:
|
||||
context.Result = new BadRequestObjectResult(new RequestError(ex.Message));
|
||||
break;
|
||||
|
@ -1,204 +0,0 @@
|
||||
// Kyoo - A portable and vast media library solution.
|
||||
// Copyright (c) Kyoo.
|
||||
//
|
||||
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
//
|
||||
// Kyoo is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// Kyoo is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using JetBrains.Annotations;
|
||||
using Kyoo.Abstractions.Models;
|
||||
|
||||
namespace Kyoo.Core.Api
|
||||
{
|
||||
/// <summary>
|
||||
/// A static class containing methods to parse the <c>where</c> query string.
|
||||
/// </summary>
|
||||
public static class ApiHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Make an expression (like
|
||||
/// <see cref="Expression.LessThan(System.Linq.Expressions.Expression,System.Linq.Expressions.Expression)"/>
|
||||
/// compatible with strings). If the expressions are not strings, the given <paramref name="operand"/> is
|
||||
/// constructed but if the expressions are strings, this method make the <paramref name="operand"/> compatible with
|
||||
/// strings.
|
||||
/// </summary>
|
||||
/// <param name="operand">
|
||||
/// The expression to make compatible. It should be something like
|
||||
/// <see cref="Expression.LessThan(System.Linq.Expressions.Expression,System.Linq.Expressions.Expression)"/> or
|
||||
/// <see cref="Expression.Equal(System.Linq.Expressions.Expression,System.Linq.Expressions.Expression)"/>.
|
||||
/// </param>
|
||||
/// <param name="left">The first parameter to compare.</param>
|
||||
/// <param name="right">The second parameter to compare.</param>
|
||||
/// <returns>A comparison expression compatible with strings</returns>
|
||||
public static BinaryExpression StringCompatibleExpression(
|
||||
Func<Expression, Expression, BinaryExpression> operand,
|
||||
Expression left,
|
||||
Expression right)
|
||||
{
|
||||
if (left.Type != typeof(string))
|
||||
return operand(left, right);
|
||||
MethodCallExpression call = Expression.Call(typeof(string), "Compare", null, left, right);
|
||||
return operand(call, Expression.Constant(0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a <c>where</c> query for the given <typeparamref name="T"/>. Items can be filtered by any property
|
||||
/// of the given type.
|
||||
/// </summary>
|
||||
/// <param name="where">The list of filters.</param>
|
||||
/// <param name="defaultWhere">
|
||||
/// A custom expression to initially filter a collection. It will be combined with the parsed expression.
|
||||
/// </param>
|
||||
/// <typeparam name="T">The type to create filters for.</typeparam>
|
||||
/// <exception cref="ArgumentException">A filter is invalid.</exception>
|
||||
/// <returns>An expression representing the filters that can be used anywhere or compiled</returns>
|
||||
public static Expression<Func<T, bool>>? ParseWhere<T>(Dictionary<string, string>? where,
|
||||
Expression<Func<T, bool>>? defaultWhere = null)
|
||||
{
|
||||
if (where == null || where.Count == 0)
|
||||
return defaultWhere;
|
||||
|
||||
ParameterExpression param = defaultWhere?.Parameters.First() ?? Expression.Parameter(typeof(T));
|
||||
Expression? expression = defaultWhere?.Body;
|
||||
|
||||
foreach ((string key, string desired) in where)
|
||||
{
|
||||
if (key == null || desired == null)
|
||||
throw new ArgumentException("Invalid key/value pair. Can't be null.");
|
||||
|
||||
string value = desired;
|
||||
string operand = "eq";
|
||||
if (desired.Contains(':'))
|
||||
{
|
||||
operand = desired.Substring(0, desired.IndexOf(':'));
|
||||
value = desired.Substring(desired.IndexOf(':') + 1);
|
||||
}
|
||||
|
||||
PropertyInfo? property = typeof(T).GetProperty(key, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase);
|
||||
if (property == null)
|
||||
throw new ArgumentException($"No filterable parameter with the name {key}.");
|
||||
MemberExpression propertyExpr = Expression.Property(param, property);
|
||||
|
||||
ConstantExpression? valueExpr = null;
|
||||
bool isList = typeof(IEnumerable).IsAssignableFrom(propertyExpr.Type) && propertyExpr.Type != typeof(string);
|
||||
if (operand != "ctn" && !typeof(IResource).IsAssignableFrom(propertyExpr.Type) && !isList)
|
||||
{
|
||||
Type propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
|
||||
object? val;
|
||||
try
|
||||
{
|
||||
val = string.IsNullOrEmpty(value) || value.Equals("null", StringComparison.OrdinalIgnoreCase)
|
||||
? null
|
||||
: Convert.ChangeType(value, propertyType);
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
throw new ArgumentException("Comparing two different value's type.");
|
||||
}
|
||||
|
||||
valueExpr = Expression.Constant(val, property.PropertyType);
|
||||
}
|
||||
|
||||
Expression condition = operand switch
|
||||
{
|
||||
"eq" when isList => _ContainsResourceExpression(propertyExpr, value),
|
||||
"ctn" => _ContainsResourceExpression(propertyExpr, value),
|
||||
|
||||
"eq" when valueExpr == null => _ResourceEqual(propertyExpr, value),
|
||||
"not" when valueExpr == null => _ResourceEqual(propertyExpr, value, true),
|
||||
|
||||
"eq" => Expression.Equal(propertyExpr, valueExpr),
|
||||
"not" => Expression.NotEqual(propertyExpr, valueExpr),
|
||||
"lt" => StringCompatibleExpression(Expression.LessThan, propertyExpr, valueExpr!),
|
||||
"lte" => StringCompatibleExpression(Expression.LessThanOrEqual, propertyExpr, valueExpr!),
|
||||
"gt" => StringCompatibleExpression(Expression.GreaterThan, propertyExpr, valueExpr!),
|
||||
"gte" => StringCompatibleExpression(Expression.GreaterThanOrEqual, propertyExpr, valueExpr!),
|
||||
_ => throw new ArgumentException($"Invalid operand: {operand}")
|
||||
};
|
||||
|
||||
expression = expression != null
|
||||
? Expression.AndAlso(expression, condition)
|
||||
: condition;
|
||||
}
|
||||
|
||||
return Expression.Lambda<Func<T, bool>>(expression!, param);
|
||||
}
|
||||
|
||||
private static Expression _ResourceEqual(Expression parameter, string value, bool notEqual = false)
|
||||
{
|
||||
MemberExpression field;
|
||||
ConstantExpression valueConst;
|
||||
if (int.TryParse(value, out int id))
|
||||
{
|
||||
field = Expression.Property(parameter, "ID");
|
||||
valueConst = Expression.Constant(id);
|
||||
}
|
||||
else
|
||||
{
|
||||
field = Expression.Property(parameter, "Slug");
|
||||
valueConst = Expression.Constant(value);
|
||||
}
|
||||
|
||||
return notEqual
|
||||
? Expression.NotEqual(field, valueConst)
|
||||
: Expression.Equal(field, valueConst);
|
||||
}
|
||||
|
||||
private static Expression _ContainsResourceExpression(MemberExpression xProperty, string value)
|
||||
{
|
||||
if (xProperty.Type == typeof(string))
|
||||
{
|
||||
// x.PROPRETY.Contains(value);
|
||||
return Expression.Call(xProperty, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, Expression.Constant(value));
|
||||
}
|
||||
|
||||
// x.PROPERTY is either a List<> or a []
|
||||
Type inner = xProperty.Type.GetElementType() ?? xProperty.Type.GenericTypeArguments.First();
|
||||
|
||||
if (inner.IsAssignableTo(typeof(string)))
|
||||
{
|
||||
return Expression.Call(typeof(Enumerable), "Contains", new[] { inner }, xProperty, Expression.Constant(value));
|
||||
}
|
||||
|
||||
if (inner.IsEnum && Enum.TryParse(inner, value, true, out object? enumValue))
|
||||
{
|
||||
return Expression.Call(typeof(Enumerable), "Contains", new[] { inner }, xProperty, Expression.Constant(enumValue));
|
||||
}
|
||||
|
||||
if (!inner.IsAssignableTo(typeof(IResource)))
|
||||
throw new ArgumentException("Contain (ctn) not appliable for this property.");
|
||||
|
||||
// x => x.PROPERTY.Any(y => y.Slug == value)
|
||||
Expression? ret = null;
|
||||
ParameterExpression y = Expression.Parameter(inner, "y");
|
||||
foreach (string val in value.Split(','))
|
||||
{
|
||||
LambdaExpression lambda = Expression.Lambda(_ResourceEqual(y, val), y);
|
||||
Expression iteration = Expression.Call(typeof(Enumerable), "Any", new[] { inner }, xProperty, lambda);
|
||||
|
||||
if (ret == null)
|
||||
ret = iteration;
|
||||
else
|
||||
ret = Expression.AndAlso(ret, iteration);
|
||||
}
|
||||
return ret!;
|
||||
}
|
||||
}
|
||||
}
|
57
back/src/Kyoo.Core/Views/Helper/FilterBinder.cs
Normal file
57
back/src/Kyoo.Core/Views/Helper/FilterBinder.cs
Normal file
@ -0,0 +1,57 @@
|
||||
// Kyoo - A portable and vast media library solution.
|
||||
// Copyright (c) Kyoo.
|
||||
//
|
||||
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
//
|
||||
// Kyoo is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// Kyoo is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.Abstractions.Models.Utils;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
|
||||
|
||||
namespace Kyoo.Core.Api;
|
||||
|
||||
public class FilterBinder : IModelBinder
|
||||
{
|
||||
public Task BindModelAsync(ModelBindingContext bindingContext)
|
||||
{
|
||||
ValueProviderResult fields = bindingContext.ValueProvider.GetValue(bindingContext.FieldName);
|
||||
try
|
||||
{
|
||||
object? filter = bindingContext.ModelType.GetMethod(nameof(Filter<object>.From))!
|
||||
.Invoke(null, new object?[] { fields.FirstValue });
|
||||
bindingContext.Result = ModelBindingResult.Success(filter);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
catch (TargetInvocationException ex)
|
||||
{
|
||||
throw ex.InnerException!;
|
||||
}
|
||||
}
|
||||
|
||||
public class Provider : IModelBinderProvider
|
||||
{
|
||||
public IModelBinder GetBinder(ModelBinderProviderContext context)
|
||||
{
|
||||
if (context.Metadata.ModelType.Name == "Filter`1")
|
||||
{
|
||||
return new BinderTypeModelBinder(typeof(FilterBinder));
|
||||
}
|
||||
|
||||
return null!;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
// Kyoo - A portable and vast media library solution.
|
||||
// Copyright (c) Kyoo.
|
||||
//
|
||||
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
//
|
||||
// Kyoo is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// Kyoo is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.Utils;
|
||||
using Xunit;
|
||||
|
||||
namespace Kyoo.Tests.Utility
|
||||
{
|
||||
public class TaskTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ThenTest()
|
||||
{
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => Task.FromResult(1)
|
||||
.Then(_ => throw new ArgumentException()));
|
||||
Assert.Equal(1, await Task.FromResult(1)
|
||||
.Then(_ => { }));
|
||||
|
||||
static async Task<int> Faulted()
|
||||
{
|
||||
await Task.Delay(1);
|
||||
throw new ArgumentException();
|
||||
}
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => Faulted().Then(_ => KAssert.Fail()));
|
||||
|
||||
static async Task<int> Infinite()
|
||||
{
|
||||
await Task.Delay(100000);
|
||||
return 1;
|
||||
}
|
||||
|
||||
CancellationTokenSource token = new();
|
||||
token.Cancel();
|
||||
await Assert.ThrowsAsync<TaskCanceledException>(() => Task.Run(Infinite, token.Token)
|
||||
.Then(_ => { }));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user