From 00b23fe12e5bdb28550dd9a22ec26c8e59a37918 Mon Sep 17 00:00:00 2001 From: Paul DeVito Date: Wed, 30 Sep 2026 16:22:58 +0300 Subject: [PATCH] fix(parser)!: throw query kit exceptions for bad values and sort directions A value that does not convert to the property type (for example `Age == "abc"`, `Rating > abc`, or an int overflow) threw FormatException or OverflowException. It now throws ParsingException, with the original exception as the inner exception. An invalid sort direction now throws QueryKitParsingException instead of ArgumentException. An unknown logical operator throws QueryKitParsingException instead of a plain Exception. All of these types derive from QueryKitException. BREAKING CHANGE: a bad filter value throws ParsingException, not FormatException or OverflowException. An invalid sort direction throws QueryKitParsingException, not ArgumentException. An unknown logical operator throws QueryKitParsingException, not System.Exception. Catch QueryKitException to handle all of them. --- .../Tests/FilterParsingRegressionTests.cs | 19 ++++++++++ .../FilterParsingRegressionTests.cs | 35 +++++++++++++++++++ QueryKit/FilterParser.cs | 8 +++++ QueryKit/Operators/LogicalOperator.cs | 3 +- QueryKit/SortParser.cs | 2 +- 5 files changed, 65 insertions(+), 2 deletions(-) diff --git a/QueryKit.IntegrationTests/Tests/FilterParsingRegressionTests.cs b/QueryKit.IntegrationTests/Tests/FilterParsingRegressionTests.cs index f192c3d..0de2641 100644 --- a/QueryKit.IntegrationTests/Tests/FilterParsingRegressionTests.cs +++ b/QueryKit.IntegrationTests/Tests/FilterParsingRegressionTests.cs @@ -3,6 +3,7 @@ namespace QueryKit.IntegrationTests.Tests; using System.Globalization; using System.Linq.Expressions; using Configuration; +using Exceptions; using FluentAssertions; using Microsoft.EntityFrameworkCore; using Operators; @@ -320,4 +321,22 @@ public async Task has_returns_matching_rows(string input, string[] expectedTitle // Assert recipes.Select(x => x.Title[prefix.Length..]).Should().BeEquivalentTo(expectedTitles); } + + [Theory] + [InlineData("""Age == "abc" """)] + [InlineData("""Age == abc""")] + [InlineData("""Rating > "abc" """)] + [InlineData("""Rating > abc""")] + public async Task invalid_value_throws_parsing_exception(string input) + { + // Arrange + var testingServiceScope = new TestingServiceScope(); + + // Act + var queryablePeople = testingServiceScope.DbContext().People; + var act = async () => await queryablePeople.ApplyQueryKitFilter(input).ToListAsync(); + + // Assert + await act.Should().ThrowAsync(); + } } diff --git a/QueryKit.UnitTests/FilterParsingRegressionTests.cs b/QueryKit.UnitTests/FilterParsingRegressionTests.cs index 2116f59..59f6929 100644 --- a/QueryKit.UnitTests/FilterParsingRegressionTests.cs +++ b/QueryKit.UnitTests/FilterParsingRegressionTests.cs @@ -286,6 +286,41 @@ public void has_returns_matching_rows(string input, string[] expectedTitles) result.Select(x => x.Title).Should().BeEquivalentTo(expectedTitles); } + [Theory] + [InlineData("""Age == "abc" """)] + [InlineData("""Age == abc""")] + [InlineData("""Rating > "abc" """)] + [InlineData("""Rating > abc""")] + [InlineData("""Age == 99999999999""")] + [InlineData("""Id == "abc" """)] + [InlineData("""SpecificDateTime == "abc" """)] + [InlineData("""Favorite == "abc" """)] + [InlineData("""Age ^^ ["abc"]""")] + public void invalid_value_throws_parsing_exception(string input) + { + var act = () => FilterParser.ParseFilter(input); + + act.Should().Throw(); + } + + [Theory] + [InlineData("Age sideways")] + [InlineData("Title, Age up")] + public void invalid_sort_direction_throws_query_kit_parsing_exception(string input) + { + var act = () => SortParser.ParseSort(input); + + act.Should().Throw().WithMessage("Invalid direction: *"); + } + + [Fact] + public void unknown_logical_operator_throws_query_kit_parsing_exception() + { + var act = () => LogicalOperator.GetByOperatorString("xor"); + + act.Should().Throw().WithMessage("Operator xor is not supported"); + } + private static TResult WithCulture(string cultureName, Func action) { var originalCulture = CultureInfo.CurrentCulture; diff --git a/QueryKit/FilterParser.cs b/QueryKit/FilterParser.cs index 37bac4d..6d08a87 100644 --- a/QueryKit/FilterParser.cs +++ b/QueryKit/FilterParser.cs @@ -45,6 +45,14 @@ public static Expression> ParseFilter(string input, IQueryKitCo { throw new ParsingException(e); } + catch (FormatException e) + { + throw new ParsingException(e); + } + catch (OverflowException e) + { + throw new ParsingException(e); + } return Expression.Lambda>(expr, parameter); } diff --git a/QueryKit/Operators/LogicalOperator.cs b/QueryKit/Operators/LogicalOperator.cs index 020c8f4..4bf7447 100644 --- a/QueryKit/Operators/LogicalOperator.cs +++ b/QueryKit/Operators/LogicalOperator.cs @@ -3,6 +3,7 @@ namespace QueryKit.Operators; using System.Linq.Expressions; using Ardalis.SmartEnum; using Configuration; +using Exceptions; public abstract class LogicalOperator : SmartEnum { @@ -15,7 +16,7 @@ public static LogicalOperator GetByOperatorString(string op) var logicalOperator = List.FirstOrDefault(x => x.Operator() == op); if (logicalOperator == null) { - throw new Exception($"Operator {op} is not supported"); + throw new QueryKitParsingException($"Operator {op} is not supported"); } return logicalOperator; } diff --git a/QueryKit/SortParser.cs b/QueryKit/SortParser.cs index 26c0280..d92b5d5 100644 --- a/QueryKit/SortParser.cs +++ b/QueryKit/SortParser.cs @@ -52,7 +52,7 @@ private static SortExpressionInfo CreateSortExpression(string sortClause, if (direction != Ascending && direction != Descending) { - throw new ArgumentException($"Invalid direction: {direction}. Allowed values are '{Ascending}' and '{Descending}'."); + throw new QueryKitParsingException($"Invalid direction: {direction}. Allowed values are '{Ascending}' and '{Descending}'."); } var propertyPath = config?.GetPropertyPathByQueryName(propertyName) ?? propertyName;