From ed85018554299417db4473d155ee9e812a8e87ab Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Mon, 2 Dec 2024 20:26:37 +0100 Subject: [PATCH 01/56] Add more unit tests for sort expression provider --- .../Sorting/SortExpressionProviderTests.cs | 112 +++++++++++++++++- 1 file changed, 111 insertions(+), 1 deletion(-) diff --git a/test/Strainer.UnitTests/Services/Sorting/SortExpressionProviderTests.cs b/test/Strainer.UnitTests/Services/Sorting/SortExpressionProviderTests.cs index 9dfb5ef5..946c2de2 100644 --- a/test/Strainer.UnitTests/Services/Sorting/SortExpressionProviderTests.cs +++ b/test/Strainer.UnitTests/Services/Sorting/SortExpressionProviderTests.cs @@ -1,7 +1,9 @@ -using Fluorite.Strainer.Models.Metadata; +using Fluorite.Strainer.Exceptions; +using Fluorite.Strainer.Models.Metadata; using Fluorite.Strainer.Models.Sorting.Terms; using Fluorite.Strainer.Services.Metadata; using Fluorite.Strainer.Services.Sorting; +using NSubstitute.ReceivedExtensions; using NSubstitute.ReturnsExtensions; using System.Reflection; @@ -18,6 +20,114 @@ public class SortExpressionProviderTests _provider = new SortExpressionProvider(_metadataProvidersFacadeMock); } + [Fact] + public void Provider_Returns_NullDefaultExpression_When_MetadataForTypeIsNull() + { + // Arrange + _metadataProvidersFacadeMock + .GetDefaultMetadata() + .ReturnsNull(); + + // Act + var result = _provider.GetDefaultExpression(); + + // Assert + result.Should().BeNull(); + + _metadataProvidersFacadeMock + .Received(1) + .GetDefaultMetadata(); + } + + [Fact] + public void Provider_Throws_ForDefaultExpression_When_MetadataIsMissingPropertyInfo() + { + // Arrange + var propertyMetadata = Substitute.For(); + + _metadataProvidersFacadeMock + .GetDefaultMetadata() + .Returns(propertyMetadata); + + // Act + Action act = () => _provider.GetDefaultExpression(); + + // Assert + act.Should().ThrowExactly() + .WithMessage($"Metadata for {propertyMetadata.Name} has been found but contains null PropertyInfo."); + } + + [Fact] + public void Provider_Returns_NullDefaultExpression_When_TypeMetadataIsFound_ButProperty() + { + // Arrange + var name = "foo"; + var propertyInfo = Substitute.For(); + var propertyMetadata = Substitute.For(); + propertyMetadata.PropertyInfo.Returns(propertyInfo); + propertyMetadata.DisplayName.ReturnsNull(); + propertyMetadata.Name.Returns(name); + + _metadataProvidersFacadeMock + .GetDefaultMetadata() + .Returns(propertyMetadata); + _metadataProvidersFacadeMock + .GetMetadata(isSortableRequired: true, isFilterableRequired: false, name) + .ReturnsNull(); + + // Act + var result = _provider.GetDefaultExpression(); + + // Assert + result.Should().BeNull(); + + _metadataProvidersFacadeMock + .Received(1) + .GetDefaultMetadata(); + _metadataProvidersFacadeMock + .Received(1) + .GetMetadata(isSortableRequired: true, isFilterableRequired: false, name); + } + + [Fact] + public void Provider_Returns_DefaultExpression() + { + // Arrange + var name = nameof(Comment.Id); + var propertyInfo = typeof(Comment).GetProperty(name); + var defaultMetadata = Substitute.For(); + defaultMetadata.PropertyInfo.Returns(propertyInfo); + defaultMetadata.DisplayName.ReturnsNull(); + defaultMetadata.Name.Returns(name); + var propertyMetadata = Substitute.For(); + propertyMetadata.IsDefaultSorting.Returns(true); + propertyMetadata.Name.Returns(name); + + _metadataProvidersFacadeMock + .GetDefaultMetadata() + .Returns(defaultMetadata); + _metadataProvidersFacadeMock + .GetMetadata(isSortableRequired: true, isFilterableRequired: false, name) + .Returns(propertyMetadata); + + // Act + var result = _provider.GetDefaultExpression(); + + // Assert + result.Should().NotBeNull(); + result.Expression.Should().NotBeNull(); + result.IsDefault.Should().BeTrue(); + result.IsDescending.Should().Be(propertyMetadata.IsDefaultSortingDescending); + result.IsSubsequent.Should().BeFalse(); + + _metadataProvidersFacadeMock + .Received(1) + .GetDefaultMetadata(); + _metadataProvidersFacadeMock + .Received(1) + .GetMetadata(isSortableRequired: true, isFilterableRequired: false, name); + } + [Fact] public void Provider_Returns_ListOfSortExpressions() { -- GitLab From 23417a88c87dff23288c213bb87a551def6573cd Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Tue, 3 Dec 2024 20:48:05 +0100 Subject: [PATCH 02/56] Add unit test for sort property metadata builder --- .../SortPropertyMetadataBuilderTests.cs | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 test/Strainer.UnitTests/Services/Metadata/SortPropertyMetadataBuilderTests.cs diff --git a/test/Strainer.UnitTests/Services/Metadata/SortPropertyMetadataBuilderTests.cs b/test/Strainer.UnitTests/Services/Metadata/SortPropertyMetadataBuilderTests.cs new file mode 100644 index 00000000..c7aa2e5a --- /dev/null +++ b/test/Strainer.UnitTests/Services/Metadata/SortPropertyMetadataBuilderTests.cs @@ -0,0 +1,84 @@ +using Fluorite.Strainer.Models.Metadata; +using Fluorite.Strainer.Services.Sorting; +using NSubstitute.ReturnsExtensions; + +namespace Fluorite.Strainer.UnitTests.Services.Metadata; + +public class SortPropertyMetadataBuilderTests +{ + [Fact] + public void Should_Save_PropertyMetadata_WhenCreatingBuilder() + { + // Arrange + var propertyMetadata = new Dictionary>(); + var defaultMetadata = new Dictionary(); + var type = typeof(Blog); + var propertyInfo = type.GetProperty(nameof(Blog.Title)); + var propertyName = nameof(Blog.Title); + var basePropertyMetadata = Substitute.For(); + basePropertyMetadata.DisplayName.ReturnsNull(); + + // Act + _ = new SortPropertyMetadataBuilder( + propertyMetadata, + defaultMetadata, + propertyInfo, + propertyName, + basePropertyMetadata); + + // Assert + propertyMetadata.Should().NotBeEmpty(); + propertyMetadata.Keys.Should().BeEquivalentTo([type]); + propertyMetadata[type].Should().NotBeNullOrEmpty(); + propertyMetadata[type].Keys.Should().BeEquivalentTo(propertyName); + propertyMetadata[type][propertyName].DisplayName.Should().BeNull(); + propertyMetadata[type][propertyName].IsDefaultSorting.Should().BeFalse(); + propertyMetadata[type][propertyName].IsDefaultSortingDescending.Should().BeFalse(); + propertyMetadata[type][propertyName].IsFilterable.Should().BeFalse(); + propertyMetadata[type][propertyName].IsSortable.Should().BeFalse(); + propertyMetadata[type][propertyName].Name.Should().Be(propertyName); + propertyMetadata[type][propertyName].PropertyInfo.Should().BeSameAs(propertyInfo); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Should_Save_PropertyMetadata_WithDefaultSortingOption(bool isDescending) + { + // Arrange + var propertyMetadata = new Dictionary>(); + var defaultMetadata = new Dictionary(); + var type = typeof(Blog); + var propertyInfo = type.GetProperty(nameof(Blog.Title)); + var propertyName = nameof(Blog.Title); + var basePropertyMetadata = Substitute.For(); + basePropertyMetadata.DisplayName.ReturnsNull(); + + // Act + var builder = new SortPropertyMetadataBuilder( + propertyMetadata, + defaultMetadata, + propertyInfo, + propertyName, + basePropertyMetadata); + builder.IsDefaultSort(isDescending); + + // Assert + propertyMetadata.Should().NotBeEmpty(); + propertyMetadata.Keys.Should().BeEquivalentTo([type]); + propertyMetadata[type].Should().NotBeNullOrEmpty(); + propertyMetadata[type].Keys.Should().BeEquivalentTo(propertyName); + propertyMetadata[type][propertyName].DisplayName.Should().BeNull(); + propertyMetadata[type][propertyName].IsDefaultSorting.Should().BeTrue(); + propertyMetadata[type][propertyName].IsDefaultSortingDescending.Should().Be(isDescending); + propertyMetadata[type][propertyName].IsFilterable.Should().BeFalse(); + propertyMetadata[type][propertyName].IsSortable.Should().BeFalse(); + propertyMetadata[type][propertyName].Name.Should().Be(propertyName); + propertyMetadata[type][propertyName].PropertyInfo.Should().BeSameAs(propertyInfo); + } + + private class Blog + { + public string Title { get; set; } + } +} -- GitLab From d71a39db9246429feab411731bd67e7448a95157 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sun, 8 Dec 2024 13:43:21 +0100 Subject: [PATCH 03/56] Add missing test case for filter operator validator --- .../Filtering/FilterOperatorValidatorTests.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/Strainer.UnitTests/Services/Filtering/FilterOperatorValidatorTests.cs b/test/Strainer.UnitTests/Services/Filtering/FilterOperatorValidatorTests.cs index 505f3d12..d77fbacc 100644 --- a/test/Strainer.UnitTests/Services/Filtering/FilterOperatorValidatorTests.cs +++ b/test/Strainer.UnitTests/Services/Filtering/FilterOperatorValidatorTests.cs @@ -86,6 +86,19 @@ public class FilterOperatorValidatorTests .WithMessage("*expression*"); } + [Fact] + public void Validator_DoesNot_Throw_Exception_For_EmptyOperators() + { + // Arrange + var filterOperators = Array.Empty(); + var excludedBuiltInFilterOperators = new ReadOnlyHashSet(new HashSet()); + var validator = new FilterOperatorValidator(); + + // Act & Assert + Action action = () => validator.Validate(filterOperators, excludedBuiltInFilterOperators); + action.Should().NotThrow(); + } + [Fact] public void Validator_Throw_Exception_For_Operators_With_TheSameSymbol() { -- GitLab From 30cf3653d03bb73a9ccca87bf41366fb31c7f3e7 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sun, 8 Dec 2024 13:47:37 +0100 Subject: [PATCH 04/56] Add missing test case, simplify checking for unknown sorting direction --- .../Sorting/SuffixSortingWayFormatter.cs | 24 ++++++++----------- .../Sorting/SuffixSortingWayFormatterTests.cs | 4 ++-- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/Strainer/Services/Sorting/SuffixSortingWayFormatter.cs b/src/Strainer/Services/Sorting/SuffixSortingWayFormatter.cs index be77e881..aa2f0a5a 100644 --- a/src/Strainer/Services/Sorting/SuffixSortingWayFormatter.cs +++ b/src/Strainer/Services/Sorting/SuffixSortingWayFormatter.cs @@ -42,19 +42,22 @@ public class SuffixSortingWayFormatter : ISortingWayFormatter { Guard.Against.Null(input); - if (sortingWay == SortingWay.Unknown) - { - throw new ArgumentException( - $"{nameof(sortingWay)} cannot be {nameof(SortingWay.Unknown)}.", - nameof(sortingWay)); - } - if (string.IsNullOrWhiteSpace(input)) { return input; } return input + GetSuffix(sortingWay); + + static string GetSuffix(SortingWay sortingWay) + { + return sortingWay switch + { + SortingWay.Descending => DescendingSuffix, + SortingWay.Ascending => AscendingSuffix, + _ => throw new ArgumentException($"{nameof(sortingWay)} with value '{sortingWay}' is not supported."), + }; + } } /// @@ -118,11 +121,4 @@ public class SuffixSortingWayFormatter : ISortingWayFormatter return input; } - - private string GetSuffix(SortingWay sortingWay) => sortingWay switch - { - SortingWay.Descending => DescendingSuffix, - SortingWay.Ascending => AscendingSuffix, - _ => throw new NotSupportedException($"{nameof(sortingWay)} with value '{sortingWay}' is not supported."), - }; } diff --git a/test/Strainer.UnitTests/Services/Sorting/SuffixSortingWayFormatterTests.cs b/test/Strainer.UnitTests/Services/Sorting/SuffixSortingWayFormatterTests.cs index d1f34b98..46436abd 100644 --- a/test/Strainer.UnitTests/Services/Sorting/SuffixSortingWayFormatterTests.cs +++ b/test/Strainer.UnitTests/Services/Sorting/SuffixSortingWayFormatterTests.cs @@ -33,7 +33,7 @@ public class SuffixSortingWayFormatterTests public void Formatter_Throws_ForUnkownSortingWay_WhenFormatting() { // Arrange - var input = string.Empty; + var input = "foo"; var sortingWay = SortingWay.Unknown; // Act @@ -41,7 +41,7 @@ public class SuffixSortingWayFormatterTests // Assert act.Should().ThrowExactly() - .WithMessage($"{nameof(sortingWay)} cannot be {nameof(SortingWay.Unknown)}.*"); + .WithMessage($"{nameof(sortingWay)} with value '{sortingWay}' is not supported."); } [Theory] -- GitLab From 202ce62f646d1a6e1f33148a1baa523c5c815a77 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sat, 14 Dec 2024 21:14:39 +0100 Subject: [PATCH 05/56] Throw an exception when trying to overwrite property via display name --- .../Metadata/PropertyMetadataBuilder.cs | 42 ++++++- .../Metadata/PropertyMetadataBuilderTests.cs | 117 +++++++++++++++++- .../SortPropertyMetadataBuilderTests.cs | 4 + 3 files changed, 158 insertions(+), 5 deletions(-) diff --git a/src/Strainer/Services/Metadata/PropertyMetadataBuilder.cs b/src/Strainer/Services/Metadata/PropertyMetadataBuilder.cs index aef673b3..9a9fa5e8 100644 --- a/src/Strainer/Services/Metadata/PropertyMetadataBuilder.cs +++ b/src/Strainer/Services/Metadata/PropertyMetadataBuilder.cs @@ -1,4 +1,5 @@ -using Fluorite.Strainer.Models.Metadata; +using Fluorite.Strainer.Exceptions; +using Fluorite.Strainer.Models.Metadata; using Fluorite.Strainer.Services.Sorting; using System.Reflection; @@ -69,10 +70,25 @@ public class PropertyMetadataBuilder : IPropertyMetadataBuilder : IPropertyMetadataBuilder metadata, string displayName, string fullName) + { + if (!metadata.Any()) + { + return; + } + + if (metadata.TryGetValue(displayName, out var existingMetadata)) + { + if (existingMetadata.PropertyInfo != PropertyInfo) + { + throw new StrainerException( + $"Cannot overwrite different property {existingMetadata.DisplayName ?? existingMetadata.Name} " + + $"on type {typeof(TEntity).Name} with metadata using display name {displayName} for property {fullName}."); + } + } + } } diff --git a/test/Strainer.UnitTests/Services/Metadata/PropertyMetadataBuilderTests.cs b/test/Strainer.UnitTests/Services/Metadata/PropertyMetadataBuilderTests.cs index d6fe79e6..d81d8dcf 100644 --- a/test/Strainer.UnitTests/Services/Metadata/PropertyMetadataBuilderTests.cs +++ b/test/Strainer.UnitTests/Services/Metadata/PropertyMetadataBuilderTests.cs @@ -1,5 +1,7 @@ -using Fluorite.Strainer.Models.Metadata; +using Fluorite.Strainer.Exceptions; +using Fluorite.Strainer.Models.Metadata; using Fluorite.Strainer.Services.Metadata; +using NSubstitute.ReturnsExtensions; namespace Fluorite.Strainer.UnitTests.Services.Metadata; @@ -135,8 +137,121 @@ public class PropertyMetadataBuilderTests propertyMetadata[type][displayName].PropertyInfo.Should().BeSameAs(propertyInfo); } + [Fact] + public void Should_Save_PropertyMetadata_WhenSettingDisplayNameMultipletimes() + { + // Arrange + var propertyMetadata = new Dictionary>(); + var defaultMetadata = new Dictionary(); + var type = typeof(Blog); + var propertyInfo = type.GetProperty(nameof(Blog.Title)); + var propertyName = nameof(Blog.Title); + var displayName1 = "SuperTitle"; + var displayName2 = "SuperTitle"; + var displayName3 = "SuperTitle"; + + // Act + var builder = new PropertyMetadataBuilder( + propertyMetadata, + defaultMetadata, + propertyInfo, + propertyName); + builder.HasDisplayName(displayName1); + builder.HasDisplayName(displayName2); + builder.HasDisplayName(displayName3); + + // Assert + propertyMetadata.Should().NotBeEmpty(); + propertyMetadata.Keys.Should().BeEquivalentTo([type]); + propertyMetadata[type].Should().NotBeNullOrEmpty(); + propertyMetadata[type].Keys.Should().BeEquivalentTo(displayName3); + propertyMetadata[type][displayName3].DisplayName.Should().Be(displayName3); + propertyMetadata[type][displayName3].IsDefaultSorting.Should().BeFalse(); + propertyMetadata[type][displayName3].IsDefaultSortingDescending.Should().BeFalse(); + propertyMetadata[type][displayName3].IsFilterable.Should().BeFalse(); + propertyMetadata[type][displayName3].IsSortable.Should().BeFalse(); + propertyMetadata[type][displayName3].Name.Should().Be(propertyName); + propertyMetadata[type][displayName3].PropertyInfo.Should().BeSameAs(propertyInfo); + } + + [Fact] + public void Should_Allow_ForOverwritingOldName_UsingDisplayName() + { + // Arrange + var type = typeof(Blog); + var propertyName = nameof(Blog.Title); + var propertyInfo = type.GetProperty(nameof(Blog.Title)); + var displayName = "SuperTitle"; + var preexistingMetadata = Substitute.For(); + preexistingMetadata.PropertyInfo.Returns(propertyInfo); + preexistingMetadata.DisplayName.ReturnsNull(); + preexistingMetadata.Name.Returns(propertyName); + var propertyMetadata = new Dictionary> + { + [type] = new Dictionary + { + [propertyName] = preexistingMetadata, + }, + }; + var defaultMetadata = new Dictionary(); + + // Act + var builder = new PropertyMetadataBuilder( + propertyMetadata, + defaultMetadata, + propertyInfo, + propertyName); + builder.HasDisplayName(displayName); + + // Assert + propertyMetadata.Should().NotBeEmpty(); + propertyMetadata.Keys.Should().BeEquivalentTo([type]); + propertyMetadata[type].Should().NotBeNullOrEmpty(); + propertyMetadata[type].Keys.Should().BeEquivalentTo(displayName); + propertyMetadata[type][displayName].DisplayName.Should().Be(displayName); + } + + [Fact] + public void Should_Throw_OnAttemptOfOverwriting_DifferentPropertyWithAlreadyUsedName() + { + // Arrange + var type = typeof(Blog); + var authorPropertyName = nameof(Blog.Author); + var titlePropertyName = nameof(Blog.Title); + var authorPropertyInfo = type.GetProperty(nameof(Blog.Author)); + var titlePropertyInfo = type.GetProperty(nameof(Blog.Title)); + var preexistingMetadata = Substitute.For(); + preexistingMetadata.PropertyInfo.Returns(titlePropertyInfo); + preexistingMetadata.DisplayName.ReturnsNull(); + preexistingMetadata.Name.Returns(titlePropertyName); + var propertyMetadata = new Dictionary> + { + [type] = new Dictionary + { + [titlePropertyName] = preexistingMetadata, + }, + }; + var defaultMetadata = new Dictionary(); + + // Act + var builder = new PropertyMetadataBuilder( + propertyMetadata, + defaultMetadata, + authorPropertyInfo, + authorPropertyName); + Action act = () => builder.HasDisplayName(titlePropertyName); + + // Assert + act.Should().ThrowExactly() + .WithMessage( + $"Cannot overwrite different property {titlePropertyName} " + + $"on type {type.Name} with metadata using display name {titlePropertyName} for property {authorPropertyName}."); + } + private class Blog { public string Title { get; set; } + + public string Author { get; set; } } } diff --git a/test/Strainer.UnitTests/Services/Metadata/SortPropertyMetadataBuilderTests.cs b/test/Strainer.UnitTests/Services/Metadata/SortPropertyMetadataBuilderTests.cs index c7aa2e5a..15415b98 100644 --- a/test/Strainer.UnitTests/Services/Metadata/SortPropertyMetadataBuilderTests.cs +++ b/test/Strainer.UnitTests/Services/Metadata/SortPropertyMetadataBuilderTests.cs @@ -38,6 +38,7 @@ public class SortPropertyMetadataBuilderTests propertyMetadata[type][propertyName].IsSortable.Should().BeFalse(); propertyMetadata[type][propertyName].Name.Should().Be(propertyName); propertyMetadata[type][propertyName].PropertyInfo.Should().BeSameAs(propertyInfo); + defaultMetadata.Should().BeEmpty(); } [Theory] @@ -75,6 +76,9 @@ public class SortPropertyMetadataBuilderTests propertyMetadata[type][propertyName].IsSortable.Should().BeFalse(); propertyMetadata[type][propertyName].Name.Should().Be(propertyName); propertyMetadata[type][propertyName].PropertyInfo.Should().BeSameAs(propertyInfo); + defaultMetadata.Should().NotBeEmpty(); + defaultMetadata.Keys.Should().BeEquivalentTo([type]); + defaultMetadata.Values.First().Name.Should().Be(propertyName); } private class Blog -- GitLab From ee3f5ce622e8902d57b0414bb15be21094c7843e Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sat, 14 Dec 2024 21:24:35 +0100 Subject: [PATCH 06/56] Remove redunand if statement --- src/Strainer/Services/Metadata/PropertyMetadataBuilder.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Strainer/Services/Metadata/PropertyMetadataBuilder.cs b/src/Strainer/Services/Metadata/PropertyMetadataBuilder.cs index 9a9fa5e8..581a3964 100644 --- a/src/Strainer/Services/Metadata/PropertyMetadataBuilder.cs +++ b/src/Strainer/Services/Metadata/PropertyMetadataBuilder.cs @@ -116,11 +116,6 @@ public class PropertyMetadataBuilder : IPropertyMetadataBuilder metadata, string displayName, string fullName) { - if (!metadata.Any()) - { - return; - } - if (metadata.TryGetValue(displayName, out var existingMetadata)) { if (existingMetadata.PropertyInfo != PropertyInfo) -- GitLab From 2b12738dcd7b6385b9f734d90f306f9788b703ba Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sat, 14 Dec 2024 22:55:37 +0100 Subject: [PATCH 07/56] Rename filter operator expression property --- .../Filtering/Operators/FilterOperator.cs | 10 +++++----- .../Filtering/Operators/IFilterOperator.cs | 6 +++--- .../Filtering/Steps/ApplyFilterOperatorStep.cs | 2 +- .../Validation/FilterOperatorValidator.cs | 4 ++-- .../Filtering/FilterOperatorBuilderTests.cs | 4 ++-- .../Filtering/FilterOperatorValidatorTests.cs | 17 ++--------------- .../Steps/ApplyFilterOperatorStepTests.cs | 4 ++-- 7 files changed, 17 insertions(+), 30 deletions(-) diff --git a/src/Strainer/Models/Filtering/Operators/FilterOperator.cs b/src/Strainer/Models/Filtering/Operators/FilterOperator.cs index 980ca5c5..7db9fcfa 100644 --- a/src/Strainer/Models/Filtering/Operators/FilterOperator.cs +++ b/src/Strainer/Models/Filtering/Operators/FilterOperator.cs @@ -12,18 +12,18 @@ public class FilterOperator : IFilterOperator /// /// Initializes a new instance of the class. /// - public FilterOperator(string name, string symbol, Func expression) + public FilterOperator(string name, string symbol, Func expressionFunc) { Name = Guard.Against.NullOrWhiteSpace(name); Symbol = Guard.Against.NullOrWhiteSpace(symbol); - Expression = Guard.Against.Null(expression); + ExpressionProvider = Guard.Against.Null(expressionFunc); } /// - /// Gets a func providing an + /// Gets a func providing an /// with filter operator applied when supplied a . /// - public Func Expression { get; } + public Func ExpressionProvider { get; } /// /// Gets or sets a value indicating whether current @@ -33,7 +33,7 @@ public class FilterOperator : IFilterOperator /// /// Gets or sets a value indicating whether associated - /// uses method + /// uses method /// based on a instance like /// or . /// diff --git a/src/Strainer/Models/Filtering/Operators/IFilterOperator.cs b/src/Strainer/Models/Filtering/Operators/IFilterOperator.cs index 89360339..64a30f13 100644 --- a/src/Strainer/Models/Filtering/Operators/IFilterOperator.cs +++ b/src/Strainer/Models/Filtering/Operators/IFilterOperator.cs @@ -8,10 +8,10 @@ namespace Fluorite.Strainer.Models.Filtering.Operators; public interface IFilterOperator { /// - /// Gets a func providing an + /// Gets a func providing an /// with filter operator applied when supplied a . /// - Func Expression { get; } + Func ExpressionProvider { get; } /// /// Gets a value indicating whether current @@ -21,7 +21,7 @@ public interface IFilterOperator /// /// Gets a value indicating whether associated - /// uses method + /// uses method /// based on a instance like /// or . /// diff --git a/src/Strainer/Services/Filtering/Steps/ApplyFilterOperatorStep.cs b/src/Strainer/Services/Filtering/Steps/ApplyFilterOperatorStep.cs index 6fca398f..eb6e0c49 100644 --- a/src/Strainer/Services/Filtering/Steps/ApplyFilterOperatorStep.cs +++ b/src/Strainer/Services/Filtering/Steps/ApplyFilterOperatorStep.cs @@ -20,7 +20,7 @@ public class ApplyFilterOperatorStep : IApplyFilterOperatorStep try { - context.FinalExpression = context.Term.Operator.Expression(filterOperatorContext); + context.FinalExpression = context.Term.Operator.ExpressionProvider(filterOperatorContext); } catch (Exception ex) { diff --git a/src/Strainer/Services/Validation/FilterOperatorValidator.cs b/src/Strainer/Services/Validation/FilterOperatorValidator.cs index 3488e7d2..a17d0fcd 100644 --- a/src/Strainer/Services/Validation/FilterOperatorValidator.cs +++ b/src/Strainer/Services/Validation/FilterOperatorValidator.cs @@ -23,10 +23,10 @@ public class FilterOperatorValidator : IFilterOperatorValidator $"only whitespace characters."); } - if (filterOperator.Expression == null) + if (filterOperator.ExpressionProvider == null) { throw new InvalidOperationException( - $"{nameof(IFilterOperator.Expression)} for filter operator " + + $"{nameof(IFilterOperator.ExpressionProvider)} for filter operator " + $"\"{filterOperator}\" cannot be null."); } } diff --git a/test/Strainer.UnitTests/Services/Filtering/FilterOperatorBuilderTests.cs b/test/Strainer.UnitTests/Services/Filtering/FilterOperatorBuilderTests.cs index 1bb1ab6b..b4c5152e 100644 --- a/test/Strainer.UnitTests/Services/Filtering/FilterOperatorBuilderTests.cs +++ b/test/Strainer.UnitTests/Services/Filtering/FilterOperatorBuilderTests.cs @@ -26,7 +26,7 @@ public class FilterOperatorBuilderTests filterOperator.Name.Should().Be(name); filterOperator.IsCaseInsensitive.Should().BeFalse(); filterOperator.IsStringBased.Should().BeFalse(); - filterOperator.Expression.Should().NotBeNull(); - filterOperator.Expression.Invoke(Substitute.For()).Should().BeSameAs(expression); + filterOperator.ExpressionProvider.Should().NotBeNull(); + filterOperator.ExpressionProvider.Invoke(Substitute.For()).Should().BeSameAs(expression); } } diff --git a/test/Strainer.UnitTests/Services/Filtering/FilterOperatorValidatorTests.cs b/test/Strainer.UnitTests/Services/Filtering/FilterOperatorValidatorTests.cs index d77fbacc..afd2c676 100644 --- a/test/Strainer.UnitTests/Services/Filtering/FilterOperatorValidatorTests.cs +++ b/test/Strainer.UnitTests/Services/Filtering/FilterOperatorValidatorTests.cs @@ -75,7 +75,7 @@ public class FilterOperatorValidatorTests // Arrange var filterOperator = Substitute.For(); filterOperator.Symbol.Returns("@"); - filterOperator.Expression.ReturnsNull(); + filterOperator.ExpressionProvider.ReturnsNull(); var validator = new FilterOperatorValidator(); // Act & Assert @@ -86,26 +86,13 @@ public class FilterOperatorValidatorTests .WithMessage("*expression*"); } - [Fact] - public void Validator_DoesNot_Throw_Exception_For_EmptyOperators() - { - // Arrange - var filterOperators = Array.Empty(); - var excludedBuiltInFilterOperators = new ReadOnlyHashSet(new HashSet()); - var validator = new FilterOperatorValidator(); - - // Act & Assert - Action action = () => validator.Validate(filterOperators, excludedBuiltInFilterOperators); - action.Should().NotThrow(); - } - [Fact] public void Validator_Throw_Exception_For_Operators_With_TheSameSymbol() { // Arrange var filterOperator = Substitute.For(); filterOperator.Symbol.Returns("@"); - filterOperator.Expression.Returns(_ => Expression.Empty()); + filterOperator.ExpressionProvider.Returns(_ => Expression.Empty()); var filterOperators = new IFilterOperator[] { filterOperator, diff --git a/test/Strainer.UnitTests/Services/Filtering/Steps/ApplyFilterOperatorStepTests.cs b/test/Strainer.UnitTests/Services/Filtering/Steps/ApplyFilterOperatorStepTests.cs index aa917f08..278a2034 100644 --- a/test/Strainer.UnitTests/Services/Filtering/Steps/ApplyFilterOperatorStepTests.cs +++ b/test/Strainer.UnitTests/Services/Filtering/Steps/ApplyFilterOperatorStepTests.cs @@ -38,7 +38,7 @@ public class ApplyFilterOperatorStepTests .Returns(finalExpression); var filterOperator = Substitute.For(); filterOperator - .Expression + .ExpressionProvider .Returns(funcMock); var filterTerm = Substitute.For(); filterTerm @@ -72,7 +72,7 @@ public class ApplyFilterOperatorStepTests .Throws(innerException); var filterOperator = Substitute.For(); filterOperator - .Expression + .ExpressionProvider .Returns(func); var filterTerm = Substitute.For(); filterTerm -- GitLab From fd320fb5ea913262e327717d51681500e4920f7e Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sun, 15 Dec 2024 02:00:28 +0100 Subject: [PATCH 08/56] Use string methods with comparison overloads, add tests --- .../Filtering/FilterOperatorMapper.cs | 79 ++- .../Filtering/FilterOperatorMapperTests.cs | 476 ++++++++++++++++++ 2 files changed, 535 insertions(+), 20 deletions(-) create mode 100644 test/Strainer.UnitTests/Services/Filtering/FilterOperatorMapperTests.cs diff --git a/src/Strainer/Services/Filtering/FilterOperatorMapper.cs b/src/Strainer/Services/Filtering/FilterOperatorMapper.cs index fefee36e..f024fdc6 100644 --- a/src/Strainer/Services/Filtering/FilterOperatorMapper.cs +++ b/src/Strainer/Services/Filtering/FilterOperatorMapper.cs @@ -2,6 +2,7 @@ using Fluorite.Strainer.Models.Filtering.Operators; using System.Collections.ObjectModel; using System.Linq.Expressions; +using System.Reflection; namespace Fluorite.Strainer.Services.Filtering; @@ -36,11 +37,11 @@ public static class FilterOperatorMapper { new FilterOperatorBuilder(symbol: FilterOperatorSymbols.EqualsSymbol) .HasName("equal") - .HasExpression((context) => Expression.Equal(context.FilterValue, context.PropertyValue)) + .HasExpression((context) => Expression.Equal(context.PropertyValue, context.FilterValue)) .Build(), new FilterOperatorBuilder(symbol: FilterOperatorSymbols.DoesNotEqual) .HasName("does not equal") - .HasExpression((context) => Expression.NotEqual(context.FilterValue, context.PropertyValue)) + .HasExpression((context) => Expression.NotEqual(context.PropertyValue, context.FilterValue)) .Build(), }; } @@ -144,13 +145,31 @@ public static class FilterOperatorMapper new FilterOperatorBuilder(symbol: FilterOperatorSymbols.EqualsCaseInsensitive) .HasName("equal (case insensitive)") .IsStringBased() - .HasExpression((context) => Expression.Equal(context.FilterValue, context.PropertyValue)) + .HasExpression((context) => Expression.Call( + typeof(string).GetMethod( + nameof(string.Equals), + BindingFlags.Static | BindingFlags.Public, + null, + new Type[] { typeof(string), typeof(string), typeof(StringComparison) }, + null), + context.PropertyValue, + context.FilterValue, + Expression.Constant(StringComparison.OrdinalIgnoreCase))) .IsCaseInsensitive() .Build(), new FilterOperatorBuilder(symbol: FilterOperatorSymbols.DoesNotEqualCaseInsensitive) .HasName("does not equal (case insensitive)") .IsStringBased() - .HasExpression((context) => Expression.NotEqual(context.FilterValue, context.PropertyValue)) + .HasExpression((context) => Expression.Not(Expression.Call( + typeof(string).GetMethod( + nameof(string.Equals), + BindingFlags.Static | BindingFlags.Public, + null, + new Type[] { typeof(string), typeof(string), typeof(StringComparison) }, + null), + context.PropertyValue, + context.FilterValue, + Expression.Constant(StringComparison.OrdinalIgnoreCase)))) .IsCaseInsensitive() .Build(), }; @@ -163,10 +182,18 @@ public static class FilterOperatorMapper new FilterOperatorBuilder(symbol: FilterOperatorSymbols.ContainsCaseInsensitive) .HasName("contains (case insensitive)") .IsStringBased() - .HasExpression((context) => Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string) }), - context.FilterValue)) + .HasExpression((context) => + { +#if NETSTANDARD2_0 + return Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string), typeof(StringComparison) }), + context.FilterValue, + Expression.Constant(StringComparison.OrdinalIgnoreCase)); +#else + throw new NotSupportedException("Runtime version does not support case-insensitive contains method."); +#endif + }) .IsCaseInsensitive() .Build(), new FilterOperatorBuilder(symbol: FilterOperatorSymbols.StartsWithCaseInsensitive) @@ -174,8 +201,9 @@ public static class FilterOperatorMapper .IsStringBased() .HasExpression((context) => Expression.Call( context.PropertyValue, - typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string) }), - context.FilterValue)) + typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string), typeof(StringComparison) }), + context.FilterValue, + Expression.Constant(StringComparison.OrdinalIgnoreCase))) .IsCaseInsensitive() .Build(), new FilterOperatorBuilder(symbol: FilterOperatorSymbols.EndsWithCaseInsensitive) @@ -183,8 +211,9 @@ public static class FilterOperatorMapper .IsStringBased() .HasExpression((context) => Expression.Call( context.PropertyValue, - typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string) }), - context.FilterValue)) + typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string), typeof(StringComparison) }), + context.FilterValue, + Expression.Constant(StringComparison.OrdinalIgnoreCase))) .IsCaseInsensitive() .Build(), }; @@ -197,10 +226,18 @@ public static class FilterOperatorMapper new FilterOperatorBuilder(symbol: FilterOperatorSymbols.DoesNotContainCaseInsensitive) .HasName("does not contain (case insensitive)") .IsStringBased() - .HasExpression((context) => Expression.Not(Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string) }), - context.FilterValue))) + .HasExpression((context) => + { +#if NETSTANDARD2_0 + return Expression.Not(Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string), typeof(StringComparison) }), + context.FilterValue, + Expression.Constant(StringComparison.OrdinalIgnoreCase))); +#else + throw new NotSupportedException("Runtime version does not support case-insensitive contains method."); +#endif + }) .IsCaseInsensitive() .Build(), new FilterOperatorBuilder(symbol: FilterOperatorSymbols.DoesNotStartWithCaseInsensitive) @@ -208,8 +245,9 @@ public static class FilterOperatorMapper .IsStringBased() .HasExpression((context) => Expression.Not(Expression.Call( context.PropertyValue, - typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string) }), - context.FilterValue))) + typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string), typeof(StringComparison) }), + context.FilterValue, + Expression.Constant(StringComparison.OrdinalIgnoreCase)))) .IsCaseInsensitive() .Build(), new FilterOperatorBuilder(symbol: FilterOperatorSymbols.DoesNotEndWithCaseInsensitive) @@ -217,8 +255,9 @@ public static class FilterOperatorMapper .IsStringBased() .HasExpression((context) => Expression.Not(Expression.Call( context.PropertyValue, - typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string) }), - context.FilterValue))) + typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string), typeof(StringComparison) }), + context.FilterValue, + Expression.Constant(StringComparison.OrdinalIgnoreCase)))) .IsCaseInsensitive() .Build(), }; diff --git a/test/Strainer.UnitTests/Services/Filtering/FilterOperatorMapperTests.cs b/test/Strainer.UnitTests/Services/Filtering/FilterOperatorMapperTests.cs new file mode 100644 index 00000000..d98520d8 --- /dev/null +++ b/test/Strainer.UnitTests/Services/Filtering/FilterOperatorMapperTests.cs @@ -0,0 +1,476 @@ +using Fluorite.Strainer.Models.Filtering.Operators; +using Fluorite.Strainer.Services.Filtering; +using System.Linq.Expressions; +using static Fluorite.Strainer.Services.Filtering.FilterOperatorMapper; + +namespace Fluorite.Strainer.UnitTests.Services.Filtering; + +public class FilterOperatorMapperTests +{ + [Theory] + [InlineData(null, null, true)] + [InlineData("foo", null, false)] + [InlineData(null, "foo", false)] + [InlineData("", "", true)] + [InlineData(" ", " ", true)] + [InlineData("123", "123", true)] + [InlineData("foo", "foo", true)] + [InlineData("foo", "FOO", false)] + [InlineData("foo", "bar", false)] + public void Should_work_on_equal_operator(string value1, string value2, bool expectedEqual) + { + // Arrange + var operatorSymbol = FilterOperatorSymbols.EqualsSymbol; + var lambda = BuildLambdaExpression(value1, value2, operatorSymbol); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [InlineData(null, null, false)] + [InlineData("foo", null, true)] + [InlineData(null, "foo", true)] + [InlineData("", "", false)] + [InlineData(" ", " ", false)] + [InlineData("123", "123", false)] + [InlineData("foo", "foo", false)] + [InlineData("foo", "FOO", true)] + [InlineData("foo", "bar", true)] + public void Should_work_on_does_not_equal_operator(string value1, string value2, bool expectedEqual) + { + // Arrange + var operatorSymbol = FilterOperatorSymbols.DoesNotEqual; + var lambda = BuildLambdaExpression(value1, value2, operatorSymbol); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [InlineData(null, null, false)] + [InlineData(1000, null, false)] + [InlineData(null, 1000, false)] + [InlineData(1000, 1000, false)] + [InlineData(1000, 2000, false)] + [InlineData(2000, 1000, true)] + public void Should_work_on_greater_than_operator(int? value1, int? value2, bool expectedEqual) + { + // Arrange + var operatorSymbol = FilterOperatorSymbols.GreaterThan; + var lambda = BuildLambdaExpression(value1, value2, operatorSymbol); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [InlineData(null, null, false)] + [InlineData(1000, null, false)] + [InlineData(null, 1000, false)] + [InlineData(1000, 1000, true)] + [InlineData(1000, 2000, false)] + [InlineData(2000, 1000, true)] + public void Should_work_on_greater_than_or_equal_to_operator(int? value1, int? value2, bool expectedEqual) + { + // Arrange + var operatorSymbol = FilterOperatorSymbols.GreaterThanOrEqualTo; + var lambda = BuildLambdaExpression(value1, value2, operatorSymbol); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [InlineData(null, null, false)] + [InlineData(1000, null, false)] + [InlineData(null, 1000, false)] + [InlineData(1000, 1000, false)] + [InlineData(1000, 2000, true)] + [InlineData(2000, 1000, false)] + public void Should_work_on_less_than_operator(int? value1, int? value2, bool expectedEqual) + { + // Arrange + var operatorSymbol = FilterOperatorSymbols.LessThan; + var lambda = BuildLambdaExpression(value1, value2, operatorSymbol); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [InlineData(null, null, false)] + [InlineData(1000, null, false)] + [InlineData(null, 1000, false)] + [InlineData(1000, 1000, true)] + [InlineData(1000, 2000, true)] + [InlineData(2000, 1000, false)] + public void Should_work_on_less_than_or_equal_to_operator(int? value1, int? value2, bool expectedEqual) + { + // Arrange + var operatorSymbol = FilterOperatorSymbols.LessThanOrEqualTo; + var lambda = BuildLambdaExpression(value1, value2, operatorSymbol); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [InlineData("", "", true)] + [InlineData(" ", " ", true)] + [InlineData("123", "123", true)] + [InlineData("foo", "foo", true)] + [InlineData("foo", "FOO", false)] + [InlineData("foo", "bar", false)] + [InlineData("foobar", "foo", true)] + [InlineData("barfoo", "foo", true)] + public void Should_work_on_contains_operator(string propertyFilter, string filterFilter, bool expectedEqual) + { + // Arrange + var operatorSymbol = FilterOperatorSymbols.Contains; + var lambda = BuildLambdaExpression(propertyFilter, filterFilter, operatorSymbol); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [InlineData("", "", false)] + [InlineData(" ", " ", false)] + [InlineData("123", "123", false)] + [InlineData("foo", "foo", false)] + [InlineData("foo", "FOO", true)] + [InlineData("foo", "bar", true)] + [InlineData("foobar", "foo", false)] + [InlineData("barfoo", "foo", false)] + public void Should_work_on_does_not_contain_operator(string propertyFilter, string filterFilter, bool expectedEqual) + { + // Arrange + var operatorSymbol = FilterOperatorSymbols.DoesNotContain; + var lambda = BuildLambdaExpression(propertyFilter, filterFilter, operatorSymbol); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [InlineData("", "", true)] + [InlineData(" ", " ", true)] + [InlineData("123", "123", true)] + [InlineData("foo", "foo", true)] + [InlineData("foo", "FOO", false)] + [InlineData("foo", "bar", false)] + [InlineData("foobar", "foo", true)] + [InlineData("barfoo", "foo", false)] + public void Should_work_on_starts_with_operator(string propertyFilter, string filterFilter, bool expectedEqual) + { + // Arrange + var operatorSymbol = FilterOperatorSymbols.StartsWith; + var lambda = BuildLambdaExpression(propertyFilter, filterFilter, operatorSymbol); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [InlineData("", "", false)] + [InlineData(" ", " ", false)] + [InlineData("123", "123", false)] + [InlineData("foo", "foo", false)] + [InlineData("foo", "FOO", true)] + [InlineData("foo", "bar", true)] + [InlineData("foobar", "foo", false)] + [InlineData("barfoo", "foo", true)] + public void Should_work_on_does_not_start_with_operator(string propertyFilter, string filterFilter, bool expectedEqual) + { + // Arrange + var operatorSymbol = FilterOperatorSymbols.DoesNotStartWith; + var lambda = BuildLambdaExpression(propertyFilter, filterFilter, operatorSymbol); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [InlineData("", "", true)] + [InlineData(" ", " ", true)] + [InlineData("123", "123", true)] + [InlineData("foo", "foo", true)] + [InlineData("foo", "FOO", false)] + [InlineData("foo", "bar", false)] + [InlineData("foobar", "foo", false)] + [InlineData("barfoo", "foo", true)] + public void Should_work_on_ends_with_operator(string propertyFilter, string filterFilter, bool expectedEqual) + { + // Arrange + var operatorSymbol = FilterOperatorSymbols.EndsWith; + var lambda = BuildLambdaExpression(propertyFilter, filterFilter, operatorSymbol); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [InlineData("", "", false)] + [InlineData(" ", " ", false)] + [InlineData("123", "123", false)] + [InlineData("foo", "foo", false)] + [InlineData("foo", "FOO", true)] + [InlineData("foo", "bar", true)] + [InlineData("foobar", "foo", true)] + [InlineData("barfoo", "foo", false)] + public void Should_work_on_does_not_end_with_operator(string propertyFilter, string filterFilter, bool expectedEqual) + { + // Arrange + var operatorSymbol = FilterOperatorSymbols.DoesNotEndWith; + var lambda = BuildLambdaExpression(propertyFilter, filterFilter, operatorSymbol); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [InlineData("", "", true)] + [InlineData(" ", " ", true)] + [InlineData("123", "123", true)] + [InlineData("foo", "foo", true)] + [InlineData("foo", "FOO", true)] + [InlineData("foo", "bar", false)] + [InlineData("foobar", "foo", true)] + [InlineData("barfoo", "foo", true)] + public void Should_work_on_contains_case_insensitive_operator(string propertyFilter, string filterFilter, bool expectedEqual) + { + // Arrange + var operatorSymbol = FilterOperatorSymbols.ContainsCaseInsensitive; + var lambda = BuildLambdaExpression(propertyFilter, filterFilter, operatorSymbol); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [InlineData("", "", false)] + [InlineData(" ", " ", false)] + [InlineData("123", "123", false)] + [InlineData("foo", "foo", false)] + [InlineData("foo", "FOO", false)] + [InlineData("foo", "bar", true)] + [InlineData("foobar", "foo", false)] + [InlineData("barfoo", "foo", false)] + public void Should_work_on_does_not_contain_case_insensitive_operator(string propertyFilter, string filterFilter, bool expectedEqual) + { + // Arrange + var operatorSymbol = FilterOperatorSymbols.DoesNotContainCaseInsensitive; + var lambda = BuildLambdaExpression(propertyFilter, filterFilter, operatorSymbol); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [InlineData("", "", true)] + [InlineData(" ", " ", true)] + [InlineData("123", "123", true)] + [InlineData("foo", "foo", true)] + [InlineData("foo", "FOO", true)] + [InlineData("foo", "bar", false)] + [InlineData("foobar", "foo", true)] + [InlineData("barfoo", "foo", false)] + public void Should_work_on_starts_with_case_insensitive_operator(string propertyFilter, string filterFilter, bool expectedEqual) + { + // Arrange + var operatorSymbol = FilterOperatorSymbols.StartsWithCaseInsensitive; + var lambda = BuildLambdaExpression(propertyFilter, filterFilter, operatorSymbol); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [InlineData("", "", false)] + [InlineData(" ", " ", false)] + [InlineData("123", "123", false)] + [InlineData("foo", "foo", false)] + [InlineData("foo", "FOO", false)] + [InlineData("foo", "bar", true)] + [InlineData("foobar", "foo", false)] + [InlineData("barfoo", "foo", true)] + public void Should_work_on_does_not_start_with_case_insensitive_operator(string propertyFilter, string filterFilter, bool expectedEqual) + { + // Arrange + var operatorSymbol = FilterOperatorSymbols.DoesNotStartWithCaseInsensitive; + var lambda = BuildLambdaExpression(propertyFilter, filterFilter, operatorSymbol); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [InlineData("", "", true)] + [InlineData(" ", " ", true)] + [InlineData("123", "123", true)] + [InlineData("foo", "foo", true)] + [InlineData("foo", "FOO", true)] + [InlineData("foo", "bar", false)] + [InlineData("foobar", "foo", false)] + [InlineData("barfoo", "foo", true)] + public void Should_work_on_ends_with_case_insensitive_operator(string propertyFilter, string filterFilter, bool expectedEqual) + { + // Arrange + var operatorSymbol = FilterOperatorSymbols.EndsWithCaseInsensitive; + var lambda = BuildLambdaExpression(propertyFilter, filterFilter, operatorSymbol); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [InlineData("", "", false)] + [InlineData(" ", " ", false)] + [InlineData("123", "123", false)] + [InlineData("foo", "foo", false)] + [InlineData("foo", "FOO", false)] + [InlineData("foo", "bar", true)] + [InlineData("foobar", "foo", true)] + [InlineData("barfoo", "foo", false)] + public void Should_work_on_does_not_end_with_case_insensitive_operator(string propertyFilter, string filterFilter, bool expectedEqual) + { + // Arrange + var operatorSymbol = FilterOperatorSymbols.DoesNotEndWithCaseInsensitive; + var lambda = BuildLambdaExpression(propertyFilter, filterFilter, operatorSymbol); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [InlineData(null, null, true)] + [InlineData("foo", null, false)] + [InlineData(null, "foo", false)] + [InlineData("", "", true)] + [InlineData(" ", " ", true)] + [InlineData("123", "123", true)] + [InlineData("foo", "foo", true)] + [InlineData("foo", "FOO", true)] + [InlineData("foo", "bar", false)] + public void Should_work_on_equal_case_insensitive_operator(string value1, string value2, bool expectedEqual) + { + // Arrange + var operatorSymbol = FilterOperatorSymbols.EqualsCaseInsensitive; + var lambda = BuildLambdaExpression(value1, value2, operatorSymbol); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [InlineData(null, null, false)] + [InlineData("foo", null, true)] + [InlineData(null, "foo", true)] + [InlineData("", "", false)] + [InlineData(" ", " ", false)] + [InlineData("123", "123", false)] + [InlineData("foo", "foo", false)] + [InlineData("foo", "FOO", false)] + [InlineData("foo", "bar", true)] + public void Should_work_on_does_not_equal_case_insensitive_operator(string value1, string value2, bool expectedEqual) + { + // Arrange + var operatorSymbol = FilterOperatorSymbols.DoesNotEqualCaseInsensitive; + var lambda = BuildLambdaExpression(value1, value2, operatorSymbol); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + private static Expression> BuildLambdaExpression(T property, T filter, string operatorSymbol) + { + var filterOperator = DefaultOperators[operatorSymbol]; + var propertyValue = Expression.Constant(property, typeof(T)); + var filterValue = Expression.Constant(filter, typeof(T)); + var filterExpressionContext = new FilterExpressionContext(filterValue, propertyValue); + var expression = filterOperator.ExpressionProvider.Invoke(filterExpressionContext); + + return Expression.Lambda>(expression); + } +} -- GitLab From e834f6272aa7e8cabb3aed2f0f52ef67436823f5 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sat, 21 Dec 2024 00:02:40 +0100 Subject: [PATCH 09/56] Add missing test case for filter operator validator --- .../Filtering/FilterOperatorValidatorTests.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/Strainer.UnitTests/Services/Filtering/FilterOperatorValidatorTests.cs b/test/Strainer.UnitTests/Services/Filtering/FilterOperatorValidatorTests.cs index afd2c676..b9837768 100644 --- a/test/Strainer.UnitTests/Services/Filtering/FilterOperatorValidatorTests.cs +++ b/test/Strainer.UnitTests/Services/Filtering/FilterOperatorValidatorTests.cs @@ -86,6 +86,19 @@ public class FilterOperatorValidatorTests .WithMessage("*expression*"); } + [Fact] + public void Validator_DoesNotThrow_Exception_For_EmptyOperatorsCollection() + { + // Arrange + var filterOperators = Array.Empty(); + var excludedBuiltInFilterOperators = new ReadOnlyHashSet(new HashSet()); + var validator = new FilterOperatorValidator(); + + // Act & Assert + Action action = () => validator.Validate(filterOperators, excludedBuiltInFilterOperators); + action.Should().NotThrow(); + } + [Fact] public void Validator_Throw_Exception_For_Operators_With_TheSameSymbol() { -- GitLab From e3264659632f0abd967419ebfa568044224fb47c Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sat, 21 Dec 2024 00:12:17 +0100 Subject: [PATCH 10/56] Add missing test case for sort term parser --- .../Services/Sorting/SortTermParserTests.cs | 47 +++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/test/Strainer.UnitTests/Services/Sorting/SortTermParserTests.cs b/test/Strainer.UnitTests/Services/Sorting/SortTermParserTests.cs index e939c1df..17ee0195 100644 --- a/test/Strainer.UnitTests/Services/Sorting/SortTermParserTests.cs +++ b/test/Strainer.UnitTests/Services/Sorting/SortTermParserTests.cs @@ -80,9 +80,48 @@ public class SortTermParserTests // Assert sortTermList.Should().NotBeNullOrEmpty(); sortTermList.Should().HaveCount(1); - sortTermList.First().Should().NotBeNull(); - sortTermList.First().Input.Should().Be(parsedInput); - sortTermList.First().IsDescending.Should().Be(sortingWay == SortingWay.Descending); - sortTermList.First().Name.Should().Be(formattedValue); + sortTermList[0].Should().NotBeNull(); + sortTermList[0].Input.Should().Be(parsedInput); + sortTermList[0].IsDescending.Should().Be(sortingWay == SortingWay.Descending); + sortTermList[0].Name.Should().Be(formattedValue); + } + + [Fact] + public void Parser_Returns_SortTerms_UsingDefaultSortingWay() + { + // Arrange + var input = "foo"; + var parsedInput = "parsed"; + var formattedValue = "bar"; + var sortingWay = SortingWay.Unknown; + var defaultSortingWay = SortingWay.Ascending; + var options = new StrainerOptions + { + DefaultSortingWay = defaultSortingWay, + }; + + _sortTermValueParserMock + .GetParsedValues(input) + .Returns([parsedInput]); + _sortingWayFormatterMock + .GetSortingWay(parsedInput) + .Returns(sortingWay); + _sortingWayFormatterMock + .Unformat(parsedInput, defaultSortingWay) + .Returns(formattedValue); + _strainerOptionsProviderMock + .GetStrainerOptions() + .Returns(options); + + // Act + var sortTermList = _parser.GetParsedTerms(input); + + // Assert + sortTermList.Should().NotBeNullOrEmpty(); + sortTermList.Should().HaveCount(1); + sortTermList[0].Should().NotBeNull(); + sortTermList[0].Input.Should().Be(parsedInput); + sortTermList[0].IsDescending.Should().Be(sortingWay == SortingWay.Descending); + sortTermList[0].Name.Should().Be(formattedValue); } } -- GitLab From 9f423be2cb0cdbf04ce0422414dffb1692d4061d Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sat, 21 Dec 2024 00:24:24 +0100 Subject: [PATCH 11/56] Cover filter operator conflict in configuration builder tests --- .../StrainerConfigurationBuilder.cs | 5 ++- .../StrainerConfigurationBuilderTests.cs | 40 +++++++++++++++++++ .../AttributeMetadataRetrieverTests.cs | 2 +- 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 test/Strainer.UnitTests/Services/Configuration/StrainerConfigurationBuilderTests.cs diff --git a/src/Strainer/Services/Configuration/StrainerConfigurationBuilder.cs b/src/Strainer/Services/Configuration/StrainerConfigurationBuilder.cs index 9bbcd03b..498e1ffd 100644 --- a/src/Strainer/Services/Configuration/StrainerConfigurationBuilder.cs +++ b/src/Strainer/Services/Configuration/StrainerConfigurationBuilder.cs @@ -33,7 +33,10 @@ public class StrainerConfigurationBuilder : IStrainerConfigurationBuilder public IStrainerConfiguration Build() { - var filterOperatorsWithAddedBuiltIn = AddBuiltInFilterOperators(filterOperators, excludedBuiltInFilterOperators, FilterOperatorMapper.DefaultOperators); + var filterOperatorsWithAddedBuiltIn = AddBuiltInFilterOperators( + filterOperators, + excludedBuiltInFilterOperators, + FilterOperatorMapper.DefaultOperators); return new StrainerConfiguration( customFilterMethods, diff --git a/test/Strainer.UnitTests/Services/Configuration/StrainerConfigurationBuilderTests.cs b/test/Strainer.UnitTests/Services/Configuration/StrainerConfigurationBuilderTests.cs new file mode 100644 index 00000000..cc9abbd2 --- /dev/null +++ b/test/Strainer.UnitTests/Services/Configuration/StrainerConfigurationBuilderTests.cs @@ -0,0 +1,40 @@ +using Fluorite.Strainer.Models.Filtering.Operators; +using Fluorite.Strainer.Services.Configuration; +using Fluorite.Strainer.Services.Filtering; +using Fluorite.Strainer.Services.Modules; + +namespace Fluorite.Strainer.UnitTests.Services.Configuration; + +public class StrainerConfigurationBuilderTests +{ + [Fact] + public void Should_Throw_ForConflictingCustomFilterOperators() + { + // Arrange + var builder = new StrainerConfigurationBuilder(); + var conflictingSymbol = FilterOperatorSymbols.EqualsSymbol; + var filterOperator = Substitute.For(); + var filterOperators = new Dictionary + { + [conflictingSymbol] = filterOperator, + }; + var module = Substitute.For(); + module + .FilterOperators + .Returns(filterOperators); + var modules = new[] + { + module, + }; + builder.WithCustomFilterOperators(modules); + + // Act + Action act = () => builder.Build(); + + // Assert + act.Should().ThrowExactly() + .WithMessage( + $"A custom filter operator is conflicting with built-in filter operator on symbol {conflictingSymbol}. " + + $"Either mark the built-in filter operator to be excluded or remove custom filter operator."); + } +} diff --git a/test/Strainer.UnitTests/Services/Metadata/Attributes/AttributeMetadataRetrieverTests.cs b/test/Strainer.UnitTests/Services/Metadata/Attributes/AttributeMetadataRetrieverTests.cs index 4a9935c8..40104ffe 100644 --- a/test/Strainer.UnitTests/Services/Metadata/Attributes/AttributeMetadataRetrieverTests.cs +++ b/test/Strainer.UnitTests/Services/Metadata/Attributes/AttributeMetadataRetrieverTests.cs @@ -68,7 +68,7 @@ public class AttributeMetadataRetrieverTests Action act = () => _retriever.GetDefaultMetadataFromObjectAttribute(modelType); // Assert - act.Should().Throw(); + act.Should().ThrowExactly(); } [Fact] -- GitLab From 645003dd15083f24514b0ab25177360c157c351d Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Tue, 24 Dec 2024 00:26:51 +0100 Subject: [PATCH 12/56] Remove type converter from filter expression workflow context --- .../Filtering/FilterExpressionProvider.cs | 1 - .../FilterExpressionWorkflowContext.cs | 3 - .../Steps/ChangeTypeOfFilterValueStep.cs | 11 +++- .../Steps/ConvertFilterValueToStringStep.cs | 13 +++-- .../FilterExpressionProviderTests.cs | 6 +- .../Steps/ChangeTypeOfFilterValueStepTests.cs | 58 ++++++++++++++----- .../ConvertFilterValueToStringStepTests.cs | 23 +++++--- 7 files changed, 76 insertions(+), 39 deletions(-) diff --git a/src/Strainer/Services/Filtering/FilterExpressionProvider.cs b/src/Strainer/Services/Filtering/FilterExpressionProvider.cs index 8a15bf93..37c9d174 100644 --- a/src/Strainer/Services/Filtering/FilterExpressionProvider.cs +++ b/src/Strainer/Services/Filtering/FilterExpressionProvider.cs @@ -72,7 +72,6 @@ public class FilterExpressionProvider : IFilterExpressionProvider PropertyMetadata = metadata, PropertyValue = propertyExpression, Term = filterTerm, - TypeConverter = typeConverter, }; var expression = workflow.Run(context); diff --git a/src/Strainer/Services/Filtering/FilterExpressionWorkflowContext.cs b/src/Strainer/Services/Filtering/FilterExpressionWorkflowContext.cs index 000efc65..2f0d1c20 100644 --- a/src/Strainer/Services/Filtering/FilterExpressionWorkflowContext.cs +++ b/src/Strainer/Services/Filtering/FilterExpressionWorkflowContext.cs @@ -1,6 +1,5 @@ using Fluorite.Strainer.Models.Filtering.Terms; using Fluorite.Strainer.Models.Metadata; -using Fluorite.Strainer.Services.Conversion; using System.Linq.Expressions; namespace Fluorite.Strainer.Services.Filtering; @@ -21,6 +20,4 @@ public class FilterExpressionWorkflowContext public Expression? PropertyValue { get; set; } public IFilterTerm? Term { get; set; } - - public ITypeConverter? TypeConverter { get; set; } } diff --git a/src/Strainer/Services/Filtering/Steps/ChangeTypeOfFilterValueStep.cs b/src/Strainer/Services/Filtering/Steps/ChangeTypeOfFilterValueStep.cs index b2a77981..b6eb57f6 100644 --- a/src/Strainer/Services/Filtering/Steps/ChangeTypeOfFilterValueStep.cs +++ b/src/Strainer/Services/Filtering/Steps/ChangeTypeOfFilterValueStep.cs @@ -5,10 +5,14 @@ namespace Fluorite.Strainer.Services.Filtering.Steps; public class ChangeTypeOfFilterValueStep : IChangeTypeOfFilterValueStep { private readonly ITypeChanger _typeChanger; + private readonly ITypeConverterProvider _typeConverterProvider; - public ChangeTypeOfFilterValueStep(ITypeChanger typeChanger) + public ChangeTypeOfFilterValueStep( + ITypeChanger typeChanger, + ITypeConverterProvider typeConverterProvider) { _typeChanger = Guard.Against.Null(typeChanger); + _typeConverterProvider = Guard.Against.Null(typeConverterProvider); } public void Execute(FilterExpressionWorkflowContext context) @@ -18,7 +22,6 @@ public class ChangeTypeOfFilterValueStep : IChangeTypeOfFilterValueStep Guard.Against.Null(context.Term.Operator); Guard.Against.Null(context.PropertyMetadata); Guard.Against.Null(context.PropertyMetadata.PropertyInfo); - Guard.Against.Null(context.TypeConverter); Guard.Against.Null(context.FilterTermValue); if (context.Term.Operator.IsStringBased) @@ -26,9 +29,11 @@ public class ChangeTypeOfFilterValueStep : IChangeTypeOfFilterValueStep return; } + var propertyType = context.PropertyMetadata!.PropertyInfo!.PropertyType; + var typeConverter = _typeConverterProvider.GetTypeConverter(propertyType); var canConvertFromString = context.PropertyMetadata.PropertyInfo.PropertyType != typeof(string) - && context.TypeConverter.CanConvertFrom(typeof(string)); + && typeConverter.CanConvertFrom(typeof(string)); if (canConvertFromString == false) { diff --git a/src/Strainer/Services/Filtering/Steps/ConvertFilterValueToStringStep.cs b/src/Strainer/Services/Filtering/Steps/ConvertFilterValueToStringStep.cs index 525806ae..e78e1ca6 100644 --- a/src/Strainer/Services/Filtering/Steps/ConvertFilterValueToStringStep.cs +++ b/src/Strainer/Services/Filtering/Steps/ConvertFilterValueToStringStep.cs @@ -4,10 +4,14 @@ namespace Fluorite.Strainer.Services.Filtering.Steps; public class ConvertFilterValueToStringStep : IConvertFilterValueToStringStep { + private readonly ITypeConverterProvider _typeConverterProvider; private readonly IStringValueConverter _stringValueConverter; - public ConvertFilterValueToStringStep(IStringValueConverter stringValueConverter) + public ConvertFilterValueToStringStep( + ITypeConverterProvider typeConverterProvider, + IStringValueConverter stringValueConverter) { + _typeConverterProvider = Guard.Against.Null(typeConverterProvider); _stringValueConverter = Guard.Against.Null(stringValueConverter); } @@ -18,7 +22,6 @@ public class ConvertFilterValueToStringStep : IConvertFilterValueToStringStep Guard.Against.Null(context.Term.Operator); Guard.Against.Null(context.PropertyMetadata); Guard.Against.Null(context.PropertyMetadata.PropertyInfo); - Guard.Against.Null(context.TypeConverter); Guard.Against.Null(context.FilterTermValue); if (context.Term.Operator.IsStringBased) @@ -26,16 +29,18 @@ public class ConvertFilterValueToStringStep : IConvertFilterValueToStringStep return; } + var propertyType = context.PropertyMetadata!.PropertyInfo!.PropertyType; + var typeConverter = _typeConverterProvider.GetTypeConverter(propertyType); var canConvertFromString = context.PropertyMetadata.PropertyInfo.PropertyType != typeof(string) - && context.TypeConverter.CanConvertFrom(typeof(string)); + && typeConverter.CanConvertFrom(typeof(string)); if (canConvertFromString == true) { context.FilterTermConstant = _stringValueConverter.Convert( context.FilterTermValue, context.PropertyMetadata.PropertyInfo.PropertyType, - context.TypeConverter); + typeConverter); } } } diff --git a/test/Strainer.UnitTests/Services/Filtering/FilterExpressionProviderTests.cs b/test/Strainer.UnitTests/Services/Filtering/FilterExpressionProviderTests.cs index 733da165..c561d78e 100644 --- a/test/Strainer.UnitTests/Services/Filtering/FilterExpressionProviderTests.cs +++ b/test/Strainer.UnitTests/Services/Filtering/FilterExpressionProviderTests.cs @@ -109,8 +109,7 @@ public class FilterExpressionProviderTests && x.FilterTermValue == value && x.FinalExpression == null && x.PropertyMetadata.Equals(metadata) - && x.Term == filterTerm - && x.TypeConverter.Equals(typeConverterMock))) + && x.Term == filterTerm)) .Returns(expression); // Act @@ -153,8 +152,7 @@ public class FilterExpressionProviderTests && x.FilterTermValue == value && x.FinalExpression == null && x.PropertyMetadata.Equals(metadata) - && x.Term == filterTerm - && x.TypeConverter.Equals(typeConverterMock))) + && x.Term == filterTerm)) .Returns(expression); // Act diff --git a/test/Strainer.UnitTests/Services/Filtering/Steps/ChangeTypeOfFilterValueStepTests.cs b/test/Strainer.UnitTests/Services/Filtering/Steps/ChangeTypeOfFilterValueStepTests.cs index d691252a..9f0f2b63 100644 --- a/test/Strainer.UnitTests/Services/Filtering/Steps/ChangeTypeOfFilterValueStepTests.cs +++ b/test/Strainer.UnitTests/Services/Filtering/Steps/ChangeTypeOfFilterValueStepTests.cs @@ -4,6 +4,7 @@ using Fluorite.Strainer.Models.Metadata; using Fluorite.Strainer.Services.Conversion; using Fluorite.Strainer.Services.Filtering; using Fluorite.Strainer.Services.Filtering.Steps; +using System.ComponentModel; using System.Linq.Expressions; using System.Reflection; @@ -11,13 +12,16 @@ namespace Fluorite.Strainer.UnitTests.Services.Filtering.Steps; public class ChangeTypeOfFilterValueStepTests { - private readonly ITypeChanger _typeChangeMock = Substitute.For(); + private readonly ITypeChanger _typeChangerMock = Substitute.For(); + private readonly ITypeConverterProvider _typeConverterProviderMock = Substitute.For(); private readonly ChangeTypeOfFilterValueStep _step; public ChangeTypeOfFilterValueStepTests() { - _step = new ChangeTypeOfFilterValueStep(_typeChangeMock); + _step = new ChangeTypeOfFilterValueStep( + _typeChangerMock, + _typeConverterProviderMock); } [Fact] @@ -31,12 +35,10 @@ public class ChangeTypeOfFilterValueStepTests var propertyInfo = Substitute.For(); var propertyMetadata = Substitute.For(); propertyMetadata.PropertyInfo.Returns(propertyInfo); - var typeConverter = Substitute.For(); var context = new FilterExpressionWorkflowContext { Term = term, PropertyMetadata = propertyMetadata, - TypeConverter = typeConverter, FilterTermValue = "test", }; @@ -44,9 +46,10 @@ public class ChangeTypeOfFilterValueStepTests _step.Execute(context); // Assert - _typeChangeMock + _typeChangerMock .DidNotReceive() .ChangeType(Arg.Any(), Arg.Any()); + _typeConverterProviderMock.ReceivedCalls().Should().BeEmpty(); } [Fact] @@ -69,10 +72,9 @@ public class ChangeTypeOfFilterValueStepTests FilterTermValue = filterValue, PropertyMetadata = propertyMetadata, Term = term, - TypeConverter = typeConverter, }; var typeChangingResult = Expression.Constant("bar"); - _typeChangeMock + _typeChangerMock .ChangeType(filterValue, propertyType) .Returns(typeChangingResult); @@ -83,9 +85,15 @@ public class ChangeTypeOfFilterValueStepTests context.FilterTermConstant.Should().NotBeNull(); context.FilterTermConstant.Should().Be(typeChangingResult); - _typeChangeMock + _typeChangerMock .Received(1) .ChangeType(filterValue, propertyType); + _typeConverterProviderMock + .Received(1) + .GetTypeConverter(propertyType); + typeConverter + .DidNotReceive() + .CanConvertFrom(typeof(string)); } [Fact] @@ -102,19 +110,24 @@ public class ChangeTypeOfFilterValueStepTests propertyInfo.PropertyType.Returns(propertyType); var propertyMetadata = Substitute.For(); propertyMetadata.PropertyInfo.Returns(propertyInfo); - var typeConverter = Substitute.For(); - typeConverter.CanConvertFrom(typeof(string)).Returns(false); var context = new FilterExpressionWorkflowContext { FilterTermValue = filterValue, PropertyMetadata = propertyMetadata, Term = term, - TypeConverter = typeConverter, }; var typeChangingResult = Expression.Constant("bar"); - _typeChangeMock + + _typeChangerMock .ChangeType(filterValue, propertyType) .Returns(typeChangingResult); + var typeConverter = Substitute.For(); + typeConverter + .CanConvertFrom(typeof(string)) + .Returns(false); + _typeConverterProviderMock + .GetTypeConverter(propertyType) + .Returns(typeConverter); // Act _step.Execute(context); @@ -123,9 +136,15 @@ public class ChangeTypeOfFilterValueStepTests context.FilterTermConstant.Should().NotBeNull(); context.FilterTermConstant.Should().Be(typeChangingResult); - _typeChangeMock + _typeChangerMock .Received(1) .ChangeType(filterValue, propertyType); + _typeConverterProviderMock + .Received(1) + .GetTypeConverter(propertyType); + typeConverter + .Received(1) + .CanConvertFrom(typeof(string)); } [Fact] @@ -147,18 +166,27 @@ public class ChangeTypeOfFilterValueStepTests { PropertyMetadata = propertyMetadata, Term = term, - TypeConverter = typeConverter, FilterTermValue = "test", }; + _typeConverterProviderMock + .GetTypeConverter(propertyType) + .Returns(typeConverter); + // Act _step.Execute(context); // Assert context.FilterTermConstant.Should().BeNull(); - _typeChangeMock + _typeChangerMock .DidNotReceive() .ChangeType(Arg.Any(), Arg.Any()); + _typeConverterProviderMock + .Received(1) + .GetTypeConverter(propertyType); + typeConverter + .Received(1) + .CanConvertFrom(typeof(string)); } } diff --git a/test/Strainer.UnitTests/Services/Filtering/Steps/ConvertFilterValueToStringStepTests.cs b/test/Strainer.UnitTests/Services/Filtering/Steps/ConvertFilterValueToStringStepTests.cs index 43c07b00..2b30f32f 100644 --- a/test/Strainer.UnitTests/Services/Filtering/Steps/ConvertFilterValueToStringStepTests.cs +++ b/test/Strainer.UnitTests/Services/Filtering/Steps/ConvertFilterValueToStringStepTests.cs @@ -11,13 +11,16 @@ namespace Fluorite.Strainer.UnitTests.Services.Filtering.Steps; public class ConvertFilterValueToStringStepTests { + private readonly ITypeConverterProvider _typeConverterProviderMock = Substitute.For(); private readonly IStringValueConverter _stringValueConverterMock = Substitute.For(); private readonly ConvertFilterValueToStringStep _step; public ConvertFilterValueToStringStepTests() { - _step = new ConvertFilterValueToStringStep(_stringValueConverterMock); + _step = new ConvertFilterValueToStringStep( + _typeConverterProviderMock, + _stringValueConverterMock); } [Fact] @@ -31,12 +34,10 @@ public class ConvertFilterValueToStringStepTests var propertyInfo = Substitute.For(); var propertyMetadata = Substitute.For(); propertyMetadata.PropertyInfo.Returns(propertyInfo); - var typeConverter = Substitute.For(); var context = new FilterExpressionWorkflowContext { Term = term, PropertyMetadata = propertyMetadata, - TypeConverter = typeConverter, FilterTermValue = "test", }; @@ -61,12 +62,10 @@ public class ConvertFilterValueToStringStepTests propertyInfo.PropertyType.Returns(typeof(string)); var propertyMetadata = Substitute.For(); propertyMetadata.PropertyInfo.Returns(propertyInfo); - var typeConverter = Substitute.For(); var context = new FilterExpressionWorkflowContext { PropertyMetadata = propertyMetadata, Term = term, - TypeConverter = typeConverter, FilterTermValue = "test", }; @@ -91,13 +90,10 @@ public class ConvertFilterValueToStringStepTests propertyInfo.PropertyType.Returns(typeof(int)); var propertyMetadata = Substitute.For(); propertyMetadata.PropertyInfo.Returns(propertyInfo); - var typeConverter = Substitute.For(); - typeConverter.CanConvertFrom(typeof(string)).Returns(false); var context = new FilterExpressionWorkflowContext { PropertyMetadata = propertyMetadata, Term = term, - TypeConverter = typeConverter, FilterTermValue = "test", }; @@ -131,12 +127,15 @@ public class ConvertFilterValueToStringStepTests FilterTermValue = filterValue, PropertyMetadata = propertyMetadata, Term = term, - TypeConverter = typeConverter, }; var convertingResult = Expression.Constant(20); + _stringValueConverterMock .Convert(filterValue, propertyType, typeConverter) .Returns(convertingResult); + _typeConverterProviderMock + .GetTypeConverter(propertyType) + .Returns(typeConverter); // Act _step.Execute(context); @@ -148,5 +147,11 @@ public class ConvertFilterValueToStringStepTests _stringValueConverterMock .Received(1) .Convert(filterValue, propertyType, typeConverter); + _typeConverterProviderMock + .Received(1) + .GetTypeConverter(propertyType); + typeConverter + .Received(1) + .CanConvertFrom(typeof(string)); } } -- GitLab From 704948f3e0c9b390545df6ab4f81d0991740e302 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Tue, 24 Dec 2024 00:26:53 +0100 Subject: [PATCH 13/56] Add unit tests for string value converter --- .../Conversion/StringValueConverterTests.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 test/Strainer.UnitTests/Services/Conversion/StringValueConverterTests.cs diff --git a/test/Strainer.UnitTests/Services/Conversion/StringValueConverterTests.cs b/test/Strainer.UnitTests/Services/Conversion/StringValueConverterTests.cs new file mode 100644 index 00000000..4af083cc --- /dev/null +++ b/test/Strainer.UnitTests/Services/Conversion/StringValueConverterTests.cs @@ -0,0 +1,56 @@ +using Fluorite.Strainer.Exceptions; +using Fluorite.Strainer.Services.Conversion; +using NSubstitute.ExceptionExtensions; + +namespace Fluorite.Strainer.UnitTests.Services.Conversion; + +public class StringValueConverterTests +{ + private readonly StringValueConverter _converter = new(); + + [Fact] + public void Convert_ThrowsException_WhenConverterThrows() + { + // Arrange + var value = "foo"; + var targetType = typeof(int); + var typeConverter = Substitute.For(); + var exception = new NotSupportedException(); + + typeConverter + .ConvertFrom(value) + .Throws(exception); + + // Act + Action act = () => _converter.Convert(value, targetType, typeConverter); + + // Assert + var thrownException = act.Should().ThrowExactly() + .WithMessage($"Failed to convert value '{value}' to type '{targetType.FullName}'.") + .Which; + + thrownException.InnerException.Should().BeSameAs(exception); + thrownException.TargetedType.Should().Be(targetType); + thrownException.Value.Should().Be(value); + } + + [Fact] + public void Convert_Returns_ConvertedValue() + { + // Arrange + var input = "123"; + var targetType = typeof(int); + var number = 123; + var typeConverter = Substitute.For(); + + typeConverter + .ConvertFrom(input) + .Returns(number); + + // Act + var result = _converter.Convert(input, targetType, typeConverter); + + // Assert + result.Should().Be(number); + } +} -- GitLab From fcfc68c5ff1a0ede04b2719543954ada6f300c81 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Thu, 9 Jan 2025 00:05:34 +0100 Subject: [PATCH 14/56] Use metadata instead of property info --- .../Pipelines/SortPipelineOperation.cs | 2 +- .../Sorting/ISortExpressionProvider.cs | 18 ++-- .../Services/Sorting/ISortingApplier.cs | 2 +- .../Sorting/SortExpressionProvider.cs | 41 ++++----- .../Services/Sorting/SortingApplier.cs | 27 +++--- .../Sorting/SortExpressionProviderTests.cs | 85 +++++-------------- 6 files changed, 68 insertions(+), 107 deletions(-) diff --git a/src/Strainer/Services/Pipelines/SortPipelineOperation.cs b/src/Strainer/Services/Pipelines/SortPipelineOperation.cs index ca724afb..b2e5ccdf 100644 --- a/src/Strainer/Services/Pipelines/SortPipelineOperation.cs +++ b/src/Strainer/Services/Pipelines/SortPipelineOperation.cs @@ -29,7 +29,7 @@ public class SortPipelineOperation : ISortPipelineOperation, IStrainerPipelineOp var isSortingApplied = _sortingApplier.TryApplySorting(parsedTerms, source, out var sortedSource); if (isSortingApplied) { - return sortedSource; + return sortedSource!; } var defaultSortExpression = _sortExpressionProvider.GetDefaultExpression(); diff --git a/src/Strainer/Services/Sorting/ISortExpressionProvider.cs b/src/Strainer/Services/Sorting/ISortExpressionProvider.cs index bc5229f6..fe840fca 100644 --- a/src/Strainer/Services/Sorting/ISortExpressionProvider.cs +++ b/src/Strainer/Services/Sorting/ISortExpressionProvider.cs @@ -1,24 +1,24 @@ -using Fluorite.Strainer.Models.Sorting; +using Fluorite.Strainer.Models.Metadata; +using Fluorite.Strainer.Models.Sorting; using Fluorite.Strainer.Models.Sorting.Terms; using Fluorite.Strainer.Services.Metadata; using System.Linq.Expressions; -using System.Reflection; namespace Fluorite.Strainer.Services.Sorting; /// -/// Provides means of tranlating into -/// of . +/// Provides means of translating into +/// of . /// -/// In other words - provides list of expressions which later can be used +/// In other words - provides a list of expressions that later can be used /// as arguments for ordering . /// public interface ISortExpressionProvider { ISortExpression? GetDefaultExpression(); - ISortExpression? GetExpression( - PropertyInfo propertyInfo, + ISortExpression GetExpression( + IPropertyMetadata propertyMetadata, ISortTerm sortTerm, bool isSubsequent); @@ -36,6 +36,6 @@ public interface ISortExpressionProvider /// /// A list of . /// - IReadOnlyCollection> GetExpressions( - IEnumerable> sortTerms); + IReadOnlyList> GetExpressions( + IEnumerable> sortTerms); } diff --git a/src/Strainer/Services/Sorting/ISortingApplier.cs b/src/Strainer/Services/Sorting/ISortingApplier.cs index ba9716d7..cd2416a1 100644 --- a/src/Strainer/Services/Sorting/ISortingApplier.cs +++ b/src/Strainer/Services/Sorting/ISortingApplier.cs @@ -4,5 +4,5 @@ namespace Fluorite.Strainer.Services.Sorting; public interface ISortingApplier { - bool TryApplySorting(IList sortTerms, IQueryable source, out IQueryable sortedSource); + bool TryApplySorting(IList sortTerms, IQueryable source, out IQueryable? sortedSource); } diff --git a/src/Strainer/Services/Sorting/SortExpressionProvider.cs b/src/Strainer/Services/Sorting/SortExpressionProvider.cs index c8445269..356bfa86 100644 --- a/src/Strainer/Services/Sorting/SortExpressionProvider.cs +++ b/src/Strainer/Services/Sorting/SortExpressionProvider.cs @@ -1,9 +1,9 @@ using Fluorite.Strainer.Exceptions; +using Fluorite.Strainer.Models.Metadata; using Fluorite.Strainer.Models.Sorting; using Fluorite.Strainer.Models.Sorting.Terms; using Fluorite.Strainer.Services.Metadata; using System.Linq.Expressions; -using System.Reflection; namespace Fluorite.Strainer.Services.Sorting; @@ -26,11 +26,11 @@ public class SortExpressionProvider : ISortExpressionProvider _metadataProvidersFacade = Guard.Against.Null(metadataProvidersFacade); } + /// public ISortExpression? GetDefaultExpression() { var propertyMetadata = _metadataProvidersFacade.GetDefaultMetadata(); - - if (propertyMetadata == null) + if (propertyMetadata is null) { return null; } @@ -47,33 +47,28 @@ public class SortExpressionProvider : ISortExpressionProvider IsDescending = propertyMetadata.IsDefaultSortingDescending, }; - return GetExpression(propertyMetadata.PropertyInfo, sortTerm, isSubsequent: false); + return GetExpression(propertyMetadata, sortTerm, isSubsequent: false); } - public ISortExpression? GetExpression( - PropertyInfo propertyInfo, + /// + /// + /// is . + /// is . + /// + public ISortExpression GetExpression( + IPropertyMetadata propertyMetadata, ISortTerm sortTerm, bool isSubsequent) { - Guard.Against.Null(propertyInfo); + Guard.Against.Null(propertyMetadata); Guard.Against.Null(sortTerm); - var metadata = _metadataProvidersFacade.GetMetadata( - isSortableRequired: true, - isFilterableRequired: false, - name: sortTerm.Name); - - if (metadata == null) - { - return null; - } - var parameter = Expression.Parameter(typeof(TEntity), "p"); Expression propertyValue = parameter; - if (metadata.Name.Contains(".")) + if (propertyMetadata.Name.Contains(".")) { - var parts = metadata.Name.Split('.'); + var parts = propertyMetadata.Name.Split('.'); for (var i = 0; i < parts.Length - 1; i++) { @@ -81,13 +76,13 @@ public class SortExpressionProvider : ISortExpressionProvider } } - var propertyAccess = Expression.MakeMemberAccess(propertyValue, propertyInfo); + var propertyAccess = Expression.MakeMemberAccess(propertyValue, propertyMetadata.PropertyInfo!); var conversion = Expression.Convert(propertyAccess, typeof(object)); var orderExpression = Expression.Lambda>(conversion, parameter); return new SortExpression(orderExpression) { - IsDefault = metadata.IsDefaultSorting, + IsDefault = propertyMetadata.IsDefaultSorting, IsDescending = sortTerm.IsDescending, IsSubsequent = isSubsequent, }; @@ -97,8 +92,8 @@ public class SortExpressionProvider : ISortExpressionProvider /// /// is . /// - public IReadOnlyCollection> GetExpressions( - IEnumerable> sortTerms) + public IReadOnlyList> GetExpressions( + IEnumerable> sortTerms) { Guard.Against.Null(sortTerms); diff --git a/src/Strainer/Services/Sorting/SortingApplier.cs b/src/Strainer/Services/Sorting/SortingApplier.cs index 5925a91c..d7a7b798 100644 --- a/src/Strainer/Services/Sorting/SortingApplier.cs +++ b/src/Strainer/Services/Sorting/SortingApplier.cs @@ -24,7 +24,7 @@ public class SortingApplier : ISortingApplier _strainerOptionsProvider = Guard.Against.Null(strainerOptionsProvider); } - public bool TryApplySorting(IList sortTerms, IQueryable source, out IQueryable sortedSource) + public bool TryApplySorting(IList sortTerms, IQueryable source, out IQueryable? sortedSource) { Guard.Against.Null(sortTerms); Guard.Against.Null(source); @@ -32,7 +32,7 @@ public class SortingApplier : ISortingApplier var options = _strainerOptionsProvider.GetStrainerOptions(); var isSubsequent = false; var isSortingApplied = false; - sortedSource = source; + sortedSource = null; foreach (var sortTerm in sortTerms) { @@ -49,18 +49,25 @@ public class SortingApplier : ISortingApplier $"Metadata for {metadata.Name} has been found but contains null PropertyInfo."); } - var sortExpression = _sortExpressionProvider.GetExpression(metadata.PropertyInfo, sortTerm, isSubsequent); + var sortExpression = _sortExpressionProvider.GetExpression(metadata, sortTerm, isSubsequent); if (sortExpression != null) { + sortedSource ??= source; sortedSource = sortedSource.OrderWithSortExpression(sortExpression); isSortingApplied = true; } } else { - try + if (_customSortingExpressionProvider.TryGetCustomExpression(sortTerm, isSubsequent, out var sortExpression)) { - if (!_customSortingExpressionProvider.TryGetCustomExpression(sortTerm, isSubsequent, out var sortExpression)) + sortedSource ??= source; + sortedSource = sortedSource.OrderWithSortExpression(sortExpression!); + isSortingApplied = true; + } + else + { + if (options.ThrowExceptions) { throw new StrainerMethodNotFoundException( sortTerm.Name, @@ -68,14 +75,12 @@ public class SortingApplier : ISortingApplier } else { - sortedSource = sortedSource.OrderWithSortExpression(sortExpression!); - isSortingApplied = true; + // Fail all terms since failing to find a correct method will affect sorting overall result. + sortedSource = null; + + return false; } } - catch (StrainerException) when (!options.ThrowExceptions) - { - return false; - } } isSubsequent = true; diff --git a/test/Strainer.UnitTests/Services/Sorting/SortExpressionProviderTests.cs b/test/Strainer.UnitTests/Services/Sorting/SortExpressionProviderTests.cs index 946c2de2..c66631e3 100644 --- a/test/Strainer.UnitTests/Services/Sorting/SortExpressionProviderTests.cs +++ b/test/Strainer.UnitTests/Services/Sorting/SortExpressionProviderTests.cs @@ -58,35 +58,29 @@ public class SortExpressionProviderTests } [Fact] - public void Provider_Returns_NullDefaultExpression_When_TypeMetadataIsFound_ButProperty() + public void Provider_Throws_WhenMetadataIsFound_ButItLacksPropertyInfo() { // Arrange var name = "foo"; - var propertyInfo = Substitute.For(); var propertyMetadata = Substitute.For(); - propertyMetadata.PropertyInfo.Returns(propertyInfo); + propertyMetadata.PropertyInfo.ReturnsNull(); propertyMetadata.DisplayName.ReturnsNull(); propertyMetadata.Name.Returns(name); _metadataProvidersFacadeMock .GetDefaultMetadata() .Returns(propertyMetadata); - _metadataProvidersFacadeMock - .GetMetadata(isSortableRequired: true, isFilterableRequired: false, name) - .ReturnsNull(); // Act - var result = _provider.GetDefaultExpression(); + Action act = () => _provider.GetDefaultExpression(); // Assert - result.Should().BeNull(); + act.Should().ThrowExactly() + .WithMessage($"Metadata for {propertyMetadata.Name} has been found but contains null PropertyInfo."); _metadataProvidersFacadeMock .Received(1) .GetDefaultMetadata(); - _metadataProvidersFacadeMock - .Received(1) - .GetMetadata(isSortableRequired: true, isFilterableRequired: false, name); } [Fact] @@ -98,17 +92,13 @@ public class SortExpressionProviderTests var defaultMetadata = Substitute.For(); defaultMetadata.PropertyInfo.Returns(propertyInfo); defaultMetadata.DisplayName.ReturnsNull(); + defaultMetadata.IsDefaultSorting.Returns(true); + defaultMetadata.IsDefaultSortingDescending.Returns(true); defaultMetadata.Name.Returns(name); - var propertyMetadata = Substitute.For(); - propertyMetadata.IsDefaultSorting.Returns(true); - propertyMetadata.Name.Returns(name); _metadataProvidersFacadeMock .GetDefaultMetadata() .Returns(defaultMetadata); - _metadataProvidersFacadeMock - .GetMetadata(isSortableRequired: true, isFilterableRequired: false, name) - .Returns(propertyMetadata); // Act var result = _provider.GetDefaultExpression(); @@ -117,15 +107,12 @@ public class SortExpressionProviderTests result.Should().NotBeNull(); result.Expression.Should().NotBeNull(); result.IsDefault.Should().BeTrue(); - result.IsDescending.Should().Be(propertyMetadata.IsDefaultSortingDescending); + result.IsDescending.Should().Be(defaultMetadata.IsDefaultSortingDescending); result.IsSubsequent.Should().BeFalse(); _metadataProvidersFacadeMock .Received(1) .GetDefaultMetadata(); - _metadataProvidersFacadeMock - .Received(1) - .GetMetadata(isSortableRequired: true, isFilterableRequired: false, name); } [Fact] @@ -141,9 +128,9 @@ public class SortExpressionProviderTests { IsDescending = false, }; - var sortTerms = new Dictionary + var sortTerms = new Dictionary { - { propertyInfo, sortTerm }, + { propertyMetadata, sortTerm }, }; _metadataProvidersFacadeMock @@ -160,57 +147,31 @@ public class SortExpressionProviderTests sortExpressions[0].IsSubsequent.Should().BeFalse(); } - [Fact] - public void Provider_Returns_EmptyListOfSortExpressions_When_NoMatchingPropertyIsFound() - { - // Arrange - var propertyInfo = Substitute.For(); - var sortTerm = Substitute.For(); - var sortTerms = new Dictionary - { - { propertyInfo, sortTerm }, - }; - - _metadataProvidersFacadeMock - .GetMetadata(Arg.Any(), Arg.Any(), Arg.Any()) - .ReturnsNull(); - - // Act - var sortExpressions = _provider.GetExpressions(sortTerms); - - // Assert - sortExpressions.Should().BeEmpty(); - - _metadataProvidersFacadeMock - .Received(1) - .GetMetadata(true, false, sortTerm.Name); - } - [Fact] public void Provider_Returns_ListOfSortExpressions_For_NestedProperty() { // Arrange - var name = $"{nameof(Post.TopComment)}.{nameof(Comment.Text)}.{nameof(string.Length)}"; + var name = $"{nameof(Post.TopComment)}.{nameof(Comment.Id)}"; var sortTerm = new SortTerm(name) { - Input = $"-{nameof(Post.TopComment)}.{nameof(Comment.Text)}.{nameof(string.Length)}", + Input = $"-{nameof(Post.TopComment)}.{nameof(Comment.Id)}", IsDescending = true, }; - var propertyInfo = typeof(string).GetProperty(nameof(string.Length)); - var sortTerms = new Dictionary - { - { propertyInfo, sortTerm }, - }; var propertyMetadata = Substitute.For(); + propertyMetadata.PropertyInfo.Returns(typeof(Comment).GetProperty(nameof(Comment.Id))); propertyMetadata.Name.Returns(sortTerm.Name); propertyMetadata.IsSortable.Returns(true); + var sortTerms = new Dictionary + { + { propertyMetadata, sortTerm }, + }; _metadataProvidersFacadeMock .GetMetadata(true, false, sortTerm.Name) .Returns(propertyMetadata); // Act - var sortExpressions = _provider.GetExpressions(sortTerms).ToList(); + var sortExpressions = _provider.GetExpressions(sortTerms); // Assert sortExpressions.Should().NotBeEmpty(); @@ -241,11 +202,6 @@ public class SortExpressionProviderTests typeof(Comment).GetProperty(nameof(Comment.Text)), typeof(Comment).GetProperty(nameof(Comment.Id)), }; - var sortTerms = new Dictionary - { - { properties[0], termsList[0] }, - { properties[1], termsList[1] }, - }; var propertyMetadatas = new[] { new PropertyMetadata(nameof(Comment.Text), properties[0]) @@ -257,6 +213,11 @@ public class SortExpressionProviderTests IsSortable = true, }, }; + var sortTerms = new Dictionary + { + { propertyMetadatas[0], termsList[0] }, + { propertyMetadatas[1], termsList[1] }, + }; _metadataProvidersFacadeMock .GetMetadata(true, false, termsList[0].Name) -- GitLab From a3799b0a612b4b572ae71c59ae79adf108ac2679 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Thu, 9 Jan 2025 00:05:38 +0100 Subject: [PATCH 15/56] Add unit tests for sorting applier --- .../Services/Sorting/SortingApplierTests.cs | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 test/Strainer.UnitTests/Services/Sorting/SortingApplierTests.cs diff --git a/test/Strainer.UnitTests/Services/Sorting/SortingApplierTests.cs b/test/Strainer.UnitTests/Services/Sorting/SortingApplierTests.cs new file mode 100644 index 00000000..e7f05ba1 --- /dev/null +++ b/test/Strainer.UnitTests/Services/Sorting/SortingApplierTests.cs @@ -0,0 +1,78 @@ +using Fluorite.Strainer.Models; +using Fluorite.Strainer.Models.Sorting.Terms; +using Fluorite.Strainer.Services; +using Fluorite.Strainer.Services.Metadata; +using Fluorite.Strainer.Services.Sorting; + +namespace Fluorite.Strainer.UnitTests.Services.Sorting; + +public class SortingApplierTests +{ + private readonly ICustomSortingExpressionProvider _customSortingExpressionProviderMock = Substitute.For(); + private readonly ISortExpressionProvider _sortExpressionProviderMock = Substitute.For(); + private readonly IMetadataFacade _metadataFacadeMock = Substitute.For(); + private readonly IStrainerOptionsProvider _strainerOptionsProviderMock = Substitute.For(); + + private readonly SortingApplier _applier; + + public SortingApplierTests() + { + _applier = new SortingApplier( + _customSortingExpressionProviderMock, + _sortExpressionProviderMock, + _metadataFacadeMock, + _strainerOptionsProviderMock); + } + + [Fact] + public void Should_DoNotApplySorting_WhenTermsCollectionIsEmpty() + { + // Arrange + var sortTerms = new List(); + var source = Array.Empty().AsQueryable(); + var strainerOptions = new StrainerOptions(); + + _strainerOptionsProviderMock + .GetStrainerOptions() + .Returns(strainerOptions); + + // Act + var result = _applier.TryApplySorting(sortTerms, source, out var sortedSource); + + // Assert + result.Should().BeFalse(); + sortedSource.Should().BeNull(); + + _strainerOptionsProviderMock + .Received(1) + .GetStrainerOptions(); + } + + [Fact] + public void Should_DoNotApplySorting_WhenPropertyMetadataIsNotFound_AndExceptionThrowingIsDisabled() + { + // Arrange + var sortTerms = new List(); + var source = Array.Empty().AsQueryable(); + var strainerOptions = new StrainerOptions(); + + _strainerOptionsProviderMock + .GetStrainerOptions() + .Returns(strainerOptions); + + // Act + var result = _applier.TryApplySorting(sortTerms, source, out var sortedSource); + + // Assert + result.Should().BeFalse(); + sortedSource.Should().BeNull(); + + _strainerOptionsProviderMock + .Received(1) + .GetStrainerOptions(); + } + + private class Post + { + } +} -- GitLab From 107d8f5563aedb92500dc486ed07b0cb9cdd8e27 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sat, 11 Jan 2025 18:51:57 +0100 Subject: [PATCH 16/56] Add new exception for filter and sort not found, update tests --- src/Strainer/Exceptions/StrainerException.cs | 5 ----- .../StrainerFilterNotFoundException.cs | 16 ++++++++++++++ .../StrainerMethodNotFoundException.cs | 21 ------------------- .../Exceptions/StrainerNotFoundException.cs | 21 +++++++++++++++++++ .../StrainerSortNotFoundException.cs | 16 ++++++++++++++ .../Pipelines/FilterPipelineOperation.cs | 2 +- .../Services/Sorting/SortingApplier.cs | 7 ++++--- ...sts.cs => FilterNotFoundExceptionTests.cs} | 6 +++--- .../Pipelines/FilterPipelineOperationTests.cs | 2 +- 9 files changed, 62 insertions(+), 34 deletions(-) create mode 100644 src/Strainer/Exceptions/StrainerFilterNotFoundException.cs delete mode 100644 src/Strainer/Exceptions/StrainerMethodNotFoundException.cs create mode 100644 src/Strainer/Exceptions/StrainerNotFoundException.cs create mode 100644 src/Strainer/Exceptions/StrainerSortNotFoundException.cs rename test/Strainer.IntegrationTests/Exceptions/{MethodNotFoundExceptionTests.cs => FilterNotFoundExceptionTests.cs} (76%) diff --git a/src/Strainer/Exceptions/StrainerException.cs b/src/Strainer/Exceptions/StrainerException.cs index 48f0d369..293a6250 100644 --- a/src/Strainer/Exceptions/StrainerException.cs +++ b/src/Strainer/Exceptions/StrainerException.cs @@ -18,9 +18,4 @@ public class StrainerException : Exception { } - - protected StrainerException(SerializationInfo info, StreamingContext context) : base(info, context) - { - - } } diff --git a/src/Strainer/Exceptions/StrainerFilterNotFoundException.cs b/src/Strainer/Exceptions/StrainerFilterNotFoundException.cs new file mode 100644 index 00000000..a0113f18 --- /dev/null +++ b/src/Strainer/Exceptions/StrainerFilterNotFoundException.cs @@ -0,0 +1,16 @@ +namespace Fluorite.Strainer.Exceptions; + +public class StrainerFilterNotFoundException : StrainerNotFoundException +{ + public StrainerFilterNotFoundException(string name) : base(name) + { + } + + public StrainerFilterNotFoundException(string name, string message) : base(name, message) + { + } + + public StrainerFilterNotFoundException(string name, string message, Exception innerException) : base(name, message, innerException) + { + } +} diff --git a/src/Strainer/Exceptions/StrainerMethodNotFoundException.cs b/src/Strainer/Exceptions/StrainerMethodNotFoundException.cs deleted file mode 100644 index 089f006a..00000000 --- a/src/Strainer/Exceptions/StrainerMethodNotFoundException.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Fluorite.Strainer.Exceptions; - -public class StrainerMethodNotFoundException : StrainerException -{ - public StrainerMethodNotFoundException(string methodName) - { - MethodName = Guard.Against.Null(methodName); - } - - public StrainerMethodNotFoundException(string methodName, string message) : base(message) - { - MethodName = Guard.Against.Null(methodName); - } - - public StrainerMethodNotFoundException(string methodName, string message, Exception innerException) : base(message, innerException) - { - MethodName = Guard.Against.Null(methodName); - } - - public string MethodName { get; } -} diff --git a/src/Strainer/Exceptions/StrainerNotFoundException.cs b/src/Strainer/Exceptions/StrainerNotFoundException.cs new file mode 100644 index 00000000..64dc899e --- /dev/null +++ b/src/Strainer/Exceptions/StrainerNotFoundException.cs @@ -0,0 +1,21 @@ +namespace Fluorite.Strainer.Exceptions; + +public abstract class StrainerNotFoundException : StrainerException +{ + protected StrainerNotFoundException(string name) + { + Name = Guard.Against.Null(name); + } + + protected StrainerNotFoundException(string name, string message) : base(message) + { + Name = Guard.Against.Null(name); + } + + protected StrainerNotFoundException(string name, string message, Exception innerException) : base(message, innerException) + { + Name = Guard.Against.Null(name); + } + + public string Name { get; } +} diff --git a/src/Strainer/Exceptions/StrainerSortNotFoundException.cs b/src/Strainer/Exceptions/StrainerSortNotFoundException.cs new file mode 100644 index 00000000..1f5c0759 --- /dev/null +++ b/src/Strainer/Exceptions/StrainerSortNotFoundException.cs @@ -0,0 +1,16 @@ +namespace Fluorite.Strainer.Exceptions; + +public class StrainerSortNotFoundException : StrainerNotFoundException +{ + public StrainerSortNotFoundException(string name) : base(name) + { + } + + public StrainerSortNotFoundException(string name, string message) : base(name, message) + { + } + + public StrainerSortNotFoundException(string name, string message, Exception innerException) : base(name, message, innerException) + { + } +} diff --git a/src/Strainer/Services/Pipelines/FilterPipelineOperation.cs b/src/Strainer/Services/Pipelines/FilterPipelineOperation.cs index e2b55123..90a24126 100644 --- a/src/Strainer/Services/Pipelines/FilterPipelineOperation.cs +++ b/src/Strainer/Services/Pipelines/FilterPipelineOperation.cs @@ -66,7 +66,7 @@ public class FilterPipelineOperation : IFilterPipelineOperation, IStrainerPipeli } else { - throw new StrainerMethodNotFoundException( + throw new StrainerFilterNotFoundException( filterTermName, $"Property or custom filter method '{filterTermName}' was not found."); } diff --git a/src/Strainer/Services/Sorting/SortingApplier.cs b/src/Strainer/Services/Sorting/SortingApplier.cs index d7a7b798..87249ad8 100644 --- a/src/Strainer/Services/Sorting/SortingApplier.cs +++ b/src/Strainer/Services/Sorting/SortingApplier.cs @@ -45,8 +45,7 @@ public class SortingApplier : ISortingApplier { if (metadata.PropertyInfo is null) { - throw new StrainerException( - $"Metadata for {metadata.Name} has been found but contains null PropertyInfo."); + throw new StrainerException($"Metadata for {metadata.Name} has been found but contains null PropertyInfo."); } var sortExpression = _sortExpressionProvider.GetExpression(metadata, sortTerm, isSubsequent); @@ -69,7 +68,9 @@ public class SortingApplier : ISortingApplier { if (options.ThrowExceptions) { - throw new StrainerMethodNotFoundException( + sortedSource = null; + + throw new StrainerNotFoundException( sortTerm.Name, $"Property or custom sorting method '{sortTerm.Name}' was not found."); } diff --git a/test/Strainer.IntegrationTests/Exceptions/MethodNotFoundExceptionTests.cs b/test/Strainer.IntegrationTests/Exceptions/FilterNotFoundExceptionTests.cs similarity index 76% rename from test/Strainer.IntegrationTests/Exceptions/MethodNotFoundExceptionTests.cs rename to test/Strainer.IntegrationTests/Exceptions/FilterNotFoundExceptionTests.cs index 0d060cd4..60cb4ac6 100644 --- a/test/Strainer.IntegrationTests/Exceptions/MethodNotFoundExceptionTests.cs +++ b/test/Strainer.IntegrationTests/Exceptions/FilterNotFoundExceptionTests.cs @@ -4,9 +4,9 @@ using Fluorite.Strainer.Models; namespace Fluorite.Strainer.IntegrationTests.Exceptions; -public class MethodNotFoundExceptionTests : StrainerFixtureBase +public class FilterNotFoundExceptionTests : StrainerFixtureBase { - public MethodNotFoundExceptionTests(StrainerFactory factory) : base(factory) + public FilterNotFoundExceptionTests(StrainerFactory factory) : base(factory) { } @@ -23,6 +23,6 @@ public class MethodNotFoundExceptionTests : StrainerFixtureBase var processor = Factory.CreateDefaultProcessor(options => options.ThrowExceptions = true); // Assert - Assert.Throws(() => processor.Apply(model, queryable)); + Assert.Throws(() => processor.Apply(model, queryable)); } } diff --git a/test/Strainer.UnitTests/Services/Pipelines/FilterPipelineOperationTests.cs b/test/Strainer.UnitTests/Services/Pipelines/FilterPipelineOperationTests.cs index cf62ca52..3af0419c 100644 --- a/test/Strainer.UnitTests/Services/Pipelines/FilterPipelineOperationTests.cs +++ b/test/Strainer.UnitTests/Services/Pipelines/FilterPipelineOperationTests.cs @@ -175,7 +175,7 @@ public class FilterPipelineOperationTests Action act = () => _operation.Execute(model, source); // Assert - act.Should().ThrowExactly() + act.Should().ThrowExactly() .WithMessage($"Property or custom filter method '{filterTermName}' was not found."); _strainerOptionsProvider.Received(1).GetStrainerOptions(); -- GitLab From be7552e2cddbfb46a7005cc0da2eb20a574a45d0 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sat, 11 Jan 2025 18:57:00 +0100 Subject: [PATCH 17/56] Remove redundant check and exception throw If it's null it's going to throw anyway --- src/Strainer/Exceptions/StrainerException.cs | 4 +--- .../Services/Sorting/SortExpressionProvider.cs | 6 ------ .../Services/Sorting/SortingApplier.cs | 7 +------ .../Sorting/SortExpressionProviderTests.cs | 18 ------------------ 4 files changed, 2 insertions(+), 33 deletions(-) diff --git a/src/Strainer/Exceptions/StrainerException.cs b/src/Strainer/Exceptions/StrainerException.cs index 293a6250..e3455540 100644 --- a/src/Strainer/Exceptions/StrainerException.cs +++ b/src/Strainer/Exceptions/StrainerException.cs @@ -1,6 +1,4 @@ -using System.Runtime.Serialization; - -namespace Fluorite.Strainer.Exceptions; +namespace Fluorite.Strainer.Exceptions; public class StrainerException : Exception { diff --git a/src/Strainer/Services/Sorting/SortExpressionProvider.cs b/src/Strainer/Services/Sorting/SortExpressionProvider.cs index 356bfa86..c9b71254 100644 --- a/src/Strainer/Services/Sorting/SortExpressionProvider.cs +++ b/src/Strainer/Services/Sorting/SortExpressionProvider.cs @@ -35,12 +35,6 @@ public class SortExpressionProvider : ISortExpressionProvider return null; } - if (propertyMetadata.PropertyInfo is null) - { - throw new StrainerException( - $"Metadata for {propertyMetadata.Name} has been found but contains null PropertyInfo."); - } - var name = propertyMetadata.DisplayName ?? propertyMetadata.Name; var sortTerm = new SortTerm(name) { diff --git a/src/Strainer/Services/Sorting/SortingApplier.cs b/src/Strainer/Services/Sorting/SortingApplier.cs index 87249ad8..d83968c0 100644 --- a/src/Strainer/Services/Sorting/SortingApplier.cs +++ b/src/Strainer/Services/Sorting/SortingApplier.cs @@ -43,11 +43,6 @@ public class SortingApplier : ISortingApplier if (metadata != null) { - if (metadata.PropertyInfo is null) - { - throw new StrainerException($"Metadata for {metadata.Name} has been found but contains null PropertyInfo."); - } - var sortExpression = _sortExpressionProvider.GetExpression(metadata, sortTerm, isSubsequent); if (sortExpression != null) { @@ -70,7 +65,7 @@ public class SortingApplier : ISortingApplier { sortedSource = null; - throw new StrainerNotFoundException( + throw new StrainerSortNotFoundException( sortTerm.Name, $"Property or custom sorting method '{sortTerm.Name}' was not found."); } diff --git a/test/Strainer.UnitTests/Services/Sorting/SortExpressionProviderTests.cs b/test/Strainer.UnitTests/Services/Sorting/SortExpressionProviderTests.cs index c66631e3..18c2a2b3 100644 --- a/test/Strainer.UnitTests/Services/Sorting/SortExpressionProviderTests.cs +++ b/test/Strainer.UnitTests/Services/Sorting/SortExpressionProviderTests.cs @@ -39,24 +39,6 @@ public class SortExpressionProviderTests .GetDefaultMetadata(); } - [Fact] - public void Provider_Throws_ForDefaultExpression_When_MetadataIsMissingPropertyInfo() - { - // Arrange - var propertyMetadata = Substitute.For(); - - _metadataProvidersFacadeMock - .GetDefaultMetadata() - .Returns(propertyMetadata); - - // Act - Action act = () => _provider.GetDefaultExpression(); - - // Assert - act.Should().ThrowExactly() - .WithMessage($"Metadata for {propertyMetadata.Name} has been found but contains null PropertyInfo."); - } - [Fact] public void Provider_Throws_WhenMetadataIsFound_ButItLacksPropertyInfo() { -- GitLab From fe67b9e74de6595b09d01f1f5b1ace4eee735c4f Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sat, 11 Jan 2025 19:42:51 +0100 Subject: [PATCH 18/56] Add unit tests for sorting applier --- .../Services/Sorting/SortingApplier.cs | 11 +- .../Sorting/SortExpressionProviderTests.cs | 27 +--- .../Services/Sorting/SortingApplierTests.cs | 130 +++++++++++++++++- 3 files changed, 132 insertions(+), 36 deletions(-) diff --git a/src/Strainer/Services/Sorting/SortingApplier.cs b/src/Strainer/Services/Sorting/SortingApplier.cs index d83968c0..d5dd2a00 100644 --- a/src/Strainer/Services/Sorting/SortingApplier.cs +++ b/src/Strainer/Services/Sorting/SortingApplier.cs @@ -44,19 +44,14 @@ public class SortingApplier : ISortingApplier if (metadata != null) { var sortExpression = _sortExpressionProvider.GetExpression(metadata, sortTerm, isSubsequent); - if (sortExpression != null) - { - sortedSource ??= source; - sortedSource = sortedSource.OrderWithSortExpression(sortExpression); - isSortingApplied = true; - } + sortedSource = (sortedSource ?? source).OrderWithSortExpression(sortExpression); + isSortingApplied = true; } else { if (_customSortingExpressionProvider.TryGetCustomExpression(sortTerm, isSubsequent, out var sortExpression)) { - sortedSource ??= source; - sortedSource = sortedSource.OrderWithSortExpression(sortExpression!); + sortedSource = (sortedSource ?? source).OrderWithSortExpression(sortExpression!); isSortingApplied = true; } else diff --git a/test/Strainer.UnitTests/Services/Sorting/SortExpressionProviderTests.cs b/test/Strainer.UnitTests/Services/Sorting/SortExpressionProviderTests.cs index 18c2a2b3..775a1b87 100644 --- a/test/Strainer.UnitTests/Services/Sorting/SortExpressionProviderTests.cs +++ b/test/Strainer.UnitTests/Services/Sorting/SortExpressionProviderTests.cs @@ -2,6 +2,7 @@ using Fluorite.Strainer.Models.Metadata; using Fluorite.Strainer.Models.Sorting.Terms; using Fluorite.Strainer.Services.Metadata; +using Fluorite.Strainer.Services.Metadata.Attributes; using Fluorite.Strainer.Services.Sorting; using NSubstitute.ReceivedExtensions; using NSubstitute.ReturnsExtensions; @@ -39,32 +40,6 @@ public class SortExpressionProviderTests .GetDefaultMetadata(); } - [Fact] - public void Provider_Throws_WhenMetadataIsFound_ButItLacksPropertyInfo() - { - // Arrange - var name = "foo"; - var propertyMetadata = Substitute.For(); - propertyMetadata.PropertyInfo.ReturnsNull(); - propertyMetadata.DisplayName.ReturnsNull(); - propertyMetadata.Name.Returns(name); - - _metadataProvidersFacadeMock - .GetDefaultMetadata() - .Returns(propertyMetadata); - - // Act - Action act = () => _provider.GetDefaultExpression(); - - // Assert - act.Should().ThrowExactly() - .WithMessage($"Metadata for {propertyMetadata.Name} has been found but contains null PropertyInfo."); - - _metadataProvidersFacadeMock - .Received(1) - .GetDefaultMetadata(); - } - [Fact] public void Provider_Returns_DefaultExpression() { diff --git a/test/Strainer.UnitTests/Services/Sorting/SortingApplierTests.cs b/test/Strainer.UnitTests/Services/Sorting/SortingApplierTests.cs index e7f05ba1..ef6987fd 100644 --- a/test/Strainer.UnitTests/Services/Sorting/SortingApplierTests.cs +++ b/test/Strainer.UnitTests/Services/Sorting/SortingApplierTests.cs @@ -1,8 +1,14 @@ -using Fluorite.Strainer.Models; +using Fluorite.Strainer.Exceptions; +using Fluorite.Strainer.Models; +using Fluorite.Strainer.Models.Metadata; +using Fluorite.Strainer.Models.Sorting; using Fluorite.Strainer.Models.Sorting.Terms; using Fluorite.Strainer.Services; using Fluorite.Strainer.Services.Metadata; using Fluorite.Strainer.Services.Sorting; +using NSubstitute.ReceivedExtensions; +using NSubstitute.ReturnsExtensions; +using System.Linq.Expressions; namespace Fluorite.Strainer.UnitTests.Services.Sorting; @@ -52,13 +58,24 @@ public class SortingApplierTests public void Should_DoNotApplySorting_WhenPropertyMetadataIsNotFound_AndExceptionThrowingIsDisabled() { // Arrange - var sortTerms = new List(); + var sortTerm = Substitute.For(); + sortTerm.Name.Returns("foo"); + var sortTerms = new List + { + sortTerm, + }; var source = Array.Empty().AsQueryable(); var strainerOptions = new StrainerOptions(); _strainerOptionsProviderMock .GetStrainerOptions() .Returns(strainerOptions); + _metadataFacadeMock + .GetMetadata(isSortableRequired: true, isFilterableRequired: false, sortTerm.Name) + .ReturnsNull(); + _customSortingExpressionProviderMock + .TryGetCustomExpression(sortTerm, isSubsequent: false, out var sortExpression) + .Returns(false); // Act var result = _applier.TryApplySorting(sortTerms, source, out var sortedSource); @@ -70,6 +87,115 @@ public class SortingApplierTests _strainerOptionsProviderMock .Received(1) .GetStrainerOptions(); + _metadataFacadeMock + .Received(1) + .GetMetadata(isSortableRequired: true, isFilterableRequired: false, sortTerm.Name); + _customSortingExpressionProviderMock + .Received(1) + .TryGetCustomExpression(sortTerm, isSubsequent: false, out sortExpression); + } + + [Fact] + public void Should_ApplySorting_FromCustomSortingMethod() + { + // Arrange + var sortTerm = Substitute.For(); + sortTerm.Name.Returns(nameof(string.Length)); + var sortTerms = new List + { + sortTerm, + }; + var source = new[] { "foo", "foo bar" }.AsQueryable(); + var strainerOptions = new StrainerOptions(); + Expression> expression = x => x.Length; + var sortExpression = Substitute.For>(); + sortExpression.IsSubsequent.Returns(false); + sortExpression.IsDescending.Returns(true); + sortExpression.Expression.Returns(expression); + + _strainerOptionsProviderMock + .GetStrainerOptions() + .Returns(strainerOptions); + _metadataFacadeMock + .GetMetadata(isSortableRequired: true, isFilterableRequired: false, sortTerm.Name) + .ReturnsNull(); + _customSortingExpressionProviderMock + .TryGetCustomExpression(sortTerm, isSubsequent: false, out _) + .Returns(x => + { + x[2] = sortExpression; + + return true; + }); + + // Act + var result = _applier.TryApplySorting(sortTerms, source, out var sortedSource); + + // Assert + result.Should().BeTrue(); + sortedSource.Should().NotBeNullOrEmpty(); + sortedSource.Should().BeInDescendingOrder(expression); + + _strainerOptionsProviderMock + .Received(1) + .GetStrainerOptions(); + _metadataFacadeMock + .Received(1) + .GetMetadata(isSortableRequired: true, isFilterableRequired: false, sortTerm.Name); + _customSortingExpressionProviderMock + .Received(1) + .TryGetCustomExpression(sortTerm, isSubsequent: false, out _); + } + + [Fact] + public void Should_ApplySorting_FromPropertyMetadata() + { + // Arrange + var sortTerm = Substitute.For(); + sortTerm.Name.Returns(nameof(string.Length)); + var sortTerms = new List + { + sortTerm, + }; + var source = new[] { "foo", "foo bar" }.AsQueryable(); + var strainerOptions = new StrainerOptions(); + var metadata = Substitute.For(); + Expression> expression = x => x.Length; + var sortExpression = Substitute.For>(); + sortExpression.IsSubsequent.Returns(false); + sortExpression.IsDescending.Returns(true); + sortExpression.Expression.Returns(expression); + + _strainerOptionsProviderMock + .GetStrainerOptions() + .Returns(strainerOptions); + _metadataFacadeMock + .GetMetadata(isSortableRequired: true, isFilterableRequired: false, sortTerm.Name) + .Returns(metadata); + _sortExpressionProviderMock + .GetExpression(metadata, sortTerm, isSubsequent: false) + .Returns(sortExpression); + + // Act + var result = _applier.TryApplySorting(sortTerms, source, out var sortedSource); + + // Assert + result.Should().BeTrue(); + sortedSource.Should().NotBeNullOrEmpty(); + sortedSource.Should().BeInDescendingOrder(expression); + + _strainerOptionsProviderMock + .Received(1) + .GetStrainerOptions(); + _metadataFacadeMock + .Received(1) + .GetMetadata(isSortableRequired: true, isFilterableRequired: false, sortTerm.Name); + _sortExpressionProviderMock + .Received(1) + .GetExpression(metadata, sortTerm, isSubsequent: false); + _customSortingExpressionProviderMock + .DidNotReceive() + .TryGetCustomExpression(sortTerm, isSubsequent: false, out _); } private class Post -- GitLab From 96b0e8a625e2a5bd8c8398b30d17d4d6b2c055a8 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sat, 11 Jan 2025 22:28:32 +0100 Subject: [PATCH 19/56] Use TryAdd approach when registering Strainer services Allow adding Strainer multiple times --- .../StrainerServiceCollectionExtensions.cs | 170 +++++++++--------- ...trainerServiceCollectionExtensionsTests.cs | 26 ++- 2 files changed, 108 insertions(+), 88 deletions(-) diff --git a/src/Strainer.AspNetCore/Extensions/DependencyInjection/StrainerServiceCollectionExtensions.cs b/src/Strainer.AspNetCore/Extensions/DependencyInjection/StrainerServiceCollectionExtensions.cs index 7b948d8a..5ad505c6 100644 --- a/src/Strainer.AspNetCore/Extensions/DependencyInjection/StrainerServiceCollectionExtensions.cs +++ b/src/Strainer.AspNetCore/Extensions/DependencyInjection/StrainerServiceCollectionExtensions.cs @@ -100,7 +100,7 @@ public static class StrainerServiceCollectionExtensions var moduleTypes = GetModuleTypesFromAssemblies(assembliesToScan); - services.AddSingleton(new AssemblySourceProvider(assembliesToScan)); + services.TryAddSingleton(new AssemblySourceProvider(assembliesToScan)); return services.AddStrainer(moduleTypes, serviceLifetime); } @@ -280,7 +280,7 @@ public static class StrainerServiceCollectionExtensions Guard.Against.Null(assembliesToScan); services.Configure(configuration); - services.AddSingleton(new AssemblySourceProvider(assembliesToScan)); + services.TryAddSingleton(new AssemblySourceProvider(assembliesToScan)); return services.AddStrainer(assembliesToScan, serviceLifetime); } @@ -420,7 +420,7 @@ public static class StrainerServiceCollectionExtensions Guard.Against.Null(assembliesToScan); services.AddOptions().Configure(configure); - services.AddSingleton(new AssemblySourceProvider(assembliesToScan)); + services.TryAddSingleton(new AssemblySourceProvider(assembliesToScan)); return services.AddStrainer(assembliesToScan, serviceLifetime); } @@ -430,95 +430,89 @@ public static class StrainerServiceCollectionExtensions IReadOnlyCollection moduleTypes, ServiceLifetime serviceLifetime) { - if (services.Any(d => d.ServiceType == typeof(IStrainerProcessor))) - { - throw new InvalidOperationException( - "Unable to registrer Strainer services because they have been registered already."); - } - services.AddOptions(); if (serviceLifetime == ServiceLifetime.Singleton) { - services.Add(serviceLifetime); + services.TryAdd(serviceLifetime); } else { - services.Add(serviceLifetime); + services.TryAdd(serviceLifetime); } - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - - services.Add(serviceLifetime); - services.Add(serviceLifetime); - - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); services.TryAddSingleton(); - services.Add(serviceLifetime); - - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - - services.Add(serviceLifetime); - services.Add(serviceLifetime); - services.Add(serviceLifetime); - - services.AddSingleton(serviceProvider => + services.TryAdd(serviceLifetime); + + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAddEnumerable(serviceLifetime); + services.TryAddEnumerable(serviceLifetime); + services.TryAdd(serviceLifetime); + + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + services.TryAdd(serviceLifetime); + + services.TryAddSingleton(serviceProvider => { using var scope = serviceProvider.CreateScope(); var configurationFactory = scope.ServiceProvider.GetRequiredService(); @@ -538,12 +532,20 @@ public static class StrainerServiceCollectionExtensions }); } - private static void Add( + private static void TryAdd( + this IServiceCollection services, + ServiceLifetime serviceLifetime) + where TImplementationType : TServiceType + { + services.TryAdd(new ServiceDescriptor(typeof(TServiceType), typeof(TImplementationType), serviceLifetime)); + } + + private static void TryAddEnumerable( this IServiceCollection services, ServiceLifetime serviceLifetime) - where TImplementationType : TServiceType + where TImplementationType : TServiceType { - services.Add(new ServiceDescriptor(typeof(TServiceType), typeof(TImplementationType), serviceLifetime)); + services.TryAddEnumerable(new ServiceDescriptor(typeof(TServiceType), typeof(TImplementationType), serviceLifetime)); } private static List GetModuleTypesFromAssemblies(IReadOnlyCollection assemblies) diff --git a/test/Strainer.UnitTests/Extensions/DepedencyInjection/StrainerServiceCollectionExtensionsTests.cs b/test/Strainer.UnitTests/Extensions/DepedencyInjection/StrainerServiceCollectionExtensionsTests.cs index a64bb01c..4647f8ff 100644 --- a/test/Strainer.UnitTests/Extensions/DepedencyInjection/StrainerServiceCollectionExtensionsTests.cs +++ b/test/Strainer.UnitTests/Extensions/DepedencyInjection/StrainerServiceCollectionExtensionsTests.cs @@ -276,7 +276,7 @@ public class StrainerServiceCollectionExtensionsTests } [Fact] - public void ExtensionMethod_Throws_WhenStrainerRegisteredSecondTime() + public void ExtensionMethod_DoesNotThrow_WhenStrainerRegisteredSecondTime() { // Arrange var services = new ServiceCollection(); @@ -286,8 +286,7 @@ public class StrainerServiceCollectionExtensionsTests Action act = () => services.AddStrainer(); // Assert - act.Should().ThrowExactly() - .WithMessage("Unable to registrer Strainer services because they have been registered already."); + act.Should().NotThrow(); } [Fact] @@ -307,7 +306,7 @@ public class StrainerServiceCollectionExtensionsTests } [Fact] - public void ExtensionMethod_AddsStrainer_WithCustomService_FilterTermParser() + public void ExtensionMethod_AddsStrainer_BeforeCustomService_FilterTermParser() { // Arrange var services = new ServiceCollection(); @@ -325,6 +324,25 @@ public class StrainerServiceCollectionExtensionsTests "Because DI container should return service that was registered last."); } + [Fact] + public void ExtensionMethod_AddsStrainer_AfterCustomService_FilterTermParser() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddScoped(); + services.AddStrainer(); + using var serviceProvider = services.BuildServiceProvider(); + var postExtensionFilterTermParser = serviceProvider.GetRequiredService(); + + // Assert + postExtensionFilterTermParser + .Should() + .BeAssignableTo( + "Because DI container should return service that was registered last."); + } + [Fact] public void CustomSortingWayFormatter_Works_When_AddedToServiceCollection() { -- GitLab From 86c611e7dbca75c6c83d2aba2c60257d9a5e038b Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sat, 11 Jan 2025 22:30:26 +0100 Subject: [PATCH 20/56] Add missing test case for sorting applier --- .../Services/Sorting/SortingApplierTests.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/Strainer.UnitTests/Services/Sorting/SortingApplierTests.cs b/test/Strainer.UnitTests/Services/Sorting/SortingApplierTests.cs index ef6987fd..25d22c1d 100644 --- a/test/Strainer.UnitTests/Services/Sorting/SortingApplierTests.cs +++ b/test/Strainer.UnitTests/Services/Sorting/SortingApplierTests.cs @@ -54,6 +54,50 @@ public class SortingApplierTests .GetStrainerOptions(); } + [Fact] + public void Should_Throw_WhenPropertyMetadataIsNotFound_AndExceptionThrowingIsEnabled() + { + // Arrange + var sortTerm = Substitute.For(); + sortTerm.Name.Returns("foo"); + var sortTerms = new List + { + sortTerm, + }; + var source = Array.Empty().AsQueryable(); + var strainerOptions = new StrainerOptions + { + ThrowExceptions = true, + }; + + _strainerOptionsProviderMock + .GetStrainerOptions() + .Returns(strainerOptions); + _metadataFacadeMock + .GetMetadata(isSortableRequired: true, isFilterableRequired: false, sortTerm.Name) + .ReturnsNull(); + _customSortingExpressionProviderMock + .TryGetCustomExpression(sortTerm, isSubsequent: false, out var sortExpression) + .Returns(false); + + // Act + Action act = () => _applier.TryApplySorting(sortTerms, source, out var sortedSource); + + // Assert + act.Should().Throw() + .WithMessage($"Property or custom sorting method '{sortTerm.Name}' was not found."); + + _strainerOptionsProviderMock + .Received(1) + .GetStrainerOptions(); + _metadataFacadeMock + .Received(1) + .GetMetadata(isSortableRequired: true, isFilterableRequired: false, sortTerm.Name); + _customSortingExpressionProviderMock + .Received(1) + .TryGetCustomExpression(sortTerm, isSubsequent: false, out sortExpression); + } + [Fact] public void Should_DoNotApplySorting_WhenPropertyMetadataIsNotFound_AndExceptionThrowingIsDisabled() { -- GitLab From 6eb141defb982df8392dc11966a0bc5019d20882 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sat, 11 Jan 2025 22:31:20 +0100 Subject: [PATCH 21/56] Bump project version --- src/Strainer.AspNetCore/Strainer.AspNetCore.csproj | 2 +- src/Strainer/Strainer.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Strainer.AspNetCore/Strainer.AspNetCore.csproj b/src/Strainer.AspNetCore/Strainer.AspNetCore.csproj index cbf04954..c55db2b9 100644 --- a/src/Strainer.AspNetCore/Strainer.AspNetCore.csproj +++ b/src/Strainer.AspNetCore/Strainer.AspNetCore.csproj @@ -6,7 +6,7 @@ Fluorite.Strainer.AspNetCore Fluorite.Strainer.AspNetCore Fluorite - 4.0.0-preview3 + 4.0.0-preview4 Extensions for using Strainer with ASP.NET Core. diff --git a/src/Strainer/Strainer.csproj b/src/Strainer/Strainer.csproj index 83475235..278c8d8e 100644 --- a/src/Strainer/Strainer.csproj +++ b/src/Strainer/Strainer.csproj @@ -6,7 +6,7 @@ Fluorite.Strainer Fluorite.Strainer Fluorite - 4.0.0-preview3 + 4.0.0-preview4 Strainer is a simple, clean, and extensible framework based on .NET Standard that enables sorting, filtering, and pagination functionality. Documentation available on GitLab: https://gitlab.com/fluorite/strainer/ -- GitLab From 91c079793600d46001775583fe89c9f91e89a42f Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sat, 11 Jan 2025 22:33:00 +0100 Subject: [PATCH 22/56] Bump NuGet packages verion --- src/Strainer.ExampleWebApi/Strainer.ExampleWebApi.csproj | 2 +- .../Strainer.IntegrationTests.csproj | 4 ++-- test/Strainer.UnitTests/Strainer.UnitTests.csproj | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Strainer.ExampleWebApi/Strainer.ExampleWebApi.csproj b/src/Strainer.ExampleWebApi/Strainer.ExampleWebApi.csproj index a9e90afd..1f91954e 100644 --- a/src/Strainer.ExampleWebApi/Strainer.ExampleWebApi.csproj +++ b/src/Strainer.ExampleWebApi/Strainer.ExampleWebApi.csproj @@ -21,7 +21,7 @@ - + diff --git a/test/Strainer.IntegrationTests/Strainer.IntegrationTests.csproj b/test/Strainer.IntegrationTests/Strainer.IntegrationTests.csproj index ebf263ca..91e42b83 100644 --- a/test/Strainer.IntegrationTests/Strainer.IntegrationTests.csproj +++ b/test/Strainer.IntegrationTests/Strainer.IntegrationTests.csproj @@ -18,8 +18,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers diff --git a/test/Strainer.UnitTests/Strainer.UnitTests.csproj b/test/Strainer.UnitTests/Strainer.UnitTests.csproj index ae0da83d..ddec23ac 100644 --- a/test/Strainer.UnitTests/Strainer.UnitTests.csproj +++ b/test/Strainer.UnitTests/Strainer.UnitTests.csproj @@ -12,7 +12,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -23,8 +23,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers -- GitLab From 2a55168ce20234bce1d37a457780ed02e3d1ba79 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Mon, 13 Jan 2025 23:18:44 +0100 Subject: [PATCH 23/56] Shape builders so it forces user to follow correct building path --- .../Filtering/CustomFilterMethodBuilder.cs | 56 +------------ ...CustomFilterMethodBuilderWithExpression.cs | 40 +++++++++ .../CustomFilterMethodBuilderWithName.cs | 29 +++++++ .../Filtering/FilterOperatorBuilder.cs | 71 +--------------- .../FilterOperatorBuilderWithExpression.cs | 45 ++++++++++ .../FilterOperatorBuilderWithName.cs | 22 +++++ .../FilterOperatorBuilderWithSymbol.cs | 18 ++++ .../Filtering/FilterOperatorMapper.cs | 84 +++++++++++-------- .../Filtering/ICustomFilterMethodBuilder.cs | 14 +--- ...CustomFilterMethodBuilderWithExpression.cs | 8 ++ .../ICustomFilterMethodBuilderWithName.cs | 11 +++ .../Filtering/IFilterOperatorBuilder.cs | 17 +--- .../IFilterOperatorBuilderWithExpression.cs | 12 +++ .../IFilterOperatorBuilderWithName.cs | 9 ++ .../IFilterOperatorBuilderWithSymbol.cs | 6 ++ .../Sorting/CustomSortMethodBuilder.cs | 53 +----------- .../CustomSortMethodBuilderWithExpression.cs | 39 +++++++++ .../CustomSortMethodBuilderWithName.cs | 29 +++++++ .../Sorting/ICustomSortMethodBuilder.cs | 14 +--- .../ICustomSortMethodBuilderWithExpression.cs | 8 ++ .../ICustomSortMethodBuilderWithName.cs | 11 +++ .../CustomFilterMethodBuilderTests.cs | 77 +++++++++++++++++ .../Filtering/FilterOperatorBuilderTests.cs | 3 +- .../Sorting/CustomSortMethodBuilderTests.cs | 32 ++++--- 24 files changed, 453 insertions(+), 255 deletions(-) create mode 100644 src/Strainer/Services/Filtering/CustomFilterMethodBuilderWithExpression.cs create mode 100644 src/Strainer/Services/Filtering/CustomFilterMethodBuilderWithName.cs create mode 100644 src/Strainer/Services/Filtering/FilterOperatorBuilderWithExpression.cs create mode 100644 src/Strainer/Services/Filtering/FilterOperatorBuilderWithName.cs create mode 100644 src/Strainer/Services/Filtering/FilterOperatorBuilderWithSymbol.cs create mode 100644 src/Strainer/Services/Filtering/ICustomFilterMethodBuilderWithExpression.cs create mode 100644 src/Strainer/Services/Filtering/ICustomFilterMethodBuilderWithName.cs create mode 100644 src/Strainer/Services/Filtering/IFilterOperatorBuilderWithExpression.cs create mode 100644 src/Strainer/Services/Filtering/IFilterOperatorBuilderWithName.cs create mode 100644 src/Strainer/Services/Filtering/IFilterOperatorBuilderWithSymbol.cs create mode 100644 src/Strainer/Services/Sorting/CustomSortMethodBuilderWithExpression.cs create mode 100644 src/Strainer/Services/Sorting/CustomSortMethodBuilderWithName.cs create mode 100644 src/Strainer/Services/Sorting/ICustomSortMethodBuilderWithExpression.cs create mode 100644 src/Strainer/Services/Sorting/ICustomSortMethodBuilderWithName.cs create mode 100644 test/Strainer.UnitTests/Services/Filtering/CustomFilterMethodBuilderTests.cs diff --git a/src/Strainer/Services/Filtering/CustomFilterMethodBuilder.cs b/src/Strainer/Services/Filtering/CustomFilterMethodBuilder.cs index fb79c632..1dac7c6f 100644 --- a/src/Strainer/Services/Filtering/CustomFilterMethodBuilder.cs +++ b/src/Strainer/Services/Filtering/CustomFilterMethodBuilder.cs @@ -1,8 +1,4 @@ -using Fluorite.Strainer.Models.Filtering; -using Fluorite.Strainer.Models.Filtering.Terms; -using System.Linq.Expressions; - -namespace Fluorite.Strainer.Services.Filtering; +namespace Fluorite.Strainer.Services.Filtering; public class CustomFilterMethodBuilder : ICustomFilterMethodBuilder { @@ -10,54 +6,8 @@ public class CustomFilterMethodBuilder : ICustomFilterMethodBuilder>? Expression { get; set; } - - protected Func>>? FilterTermExpression { get; set; } - - protected string? Name { get; set; } - - public ICustomFilterMethod Build() - { - Guard.Against.NullOrWhiteSpace(Name); - - if (FilterTermExpression is null) - { - Guard.Against.Null(Expression); - - return new CustomFilterMethod(Name, Expression); - } - else - { - Guard.Against.Null(FilterTermExpression); - - return new CustomFilterMethod(Name, FilterTermExpression); - } - } - - public ICustomFilterMethodBuilder HasFunction(Expression> expression) - { - Expression = Guard.Against.Null(expression); - - return this; - } - - public ICustomFilterMethodBuilder HasFunction( - Func>> filterTermExpression) + public ICustomFilterMethodBuilderWithName HasName(string name) { - FilterTermExpression = Guard.Against.Null(filterTermExpression); - - return this; - } - - public ICustomFilterMethodBuilder HasName(string name) - { - Name = Guard.Against.NullOrWhiteSpace(name); - - return this; + return new CustomFilterMethodBuilderWithName(name); } } diff --git a/src/Strainer/Services/Filtering/CustomFilterMethodBuilderWithExpression.cs b/src/Strainer/Services/Filtering/CustomFilterMethodBuilderWithExpression.cs new file mode 100644 index 00000000..f203484e --- /dev/null +++ b/src/Strainer/Services/Filtering/CustomFilterMethodBuilderWithExpression.cs @@ -0,0 +1,40 @@ +using Fluorite.Strainer.Models.Filtering; +using Fluorite.Strainer.Models.Filtering.Terms; +using System.Linq.Expressions; + +namespace Fluorite.Strainer.Services.Filtering; + +public class CustomFilterMethodBuilderWithExpression : CustomFilterMethodBuilderWithName, ICustomFilterMethodBuilderWithExpression +{ + public CustomFilterMethodBuilderWithExpression( + string name, + Expression> expression) + : base(name) + { + Expression = Guard.Against.Null(expression); + } + + public CustomFilterMethodBuilderWithExpression( + string name, + Func>> filterTermExpression) + : base(name) + { + FilterTermExpression = Guard.Against.Null(filterTermExpression); + } + + public Expression>? Expression { get; } + + public Func>>? FilterTermExpression { get; } + + public ICustomFilterMethod Build() + { + if (FilterTermExpression is null) + { + return new CustomFilterMethod(Name, Expression!); + } + else + { + return new CustomFilterMethod(Name, FilterTermExpression); + } + } +} diff --git a/src/Strainer/Services/Filtering/CustomFilterMethodBuilderWithName.cs b/src/Strainer/Services/Filtering/CustomFilterMethodBuilderWithName.cs new file mode 100644 index 00000000..ae273e38 --- /dev/null +++ b/src/Strainer/Services/Filtering/CustomFilterMethodBuilderWithName.cs @@ -0,0 +1,29 @@ +using Fluorite.Strainer.Models.Filtering.Terms; +using System.Linq.Expressions; + +namespace Fluorite.Strainer.Services.Filtering; + +public class CustomFilterMethodBuilderWithName : CustomFilterMethodBuilder, ICustomFilterMethodBuilderWithName +{ + public CustomFilterMethodBuilderWithName(string name) + { + Name = Guard.Against.NullOrWhiteSpace(name); + } + + protected string Name { get; } + + public ICustomFilterMethodBuilderWithExpression HasFunction(Expression> expression) + { + Guard.Against.Null(expression); + + return new CustomFilterMethodBuilderWithExpression(Name, expression); + } + + public ICustomFilterMethodBuilderWithExpression HasFunction( + Func>> filterTermExpression) + { + Guard.Against.Null(filterTermExpression); + + return new CustomFilterMethodBuilderWithExpression(Name, filterTermExpression); + } +} diff --git a/src/Strainer/Services/Filtering/FilterOperatorBuilder.cs b/src/Strainer/Services/Filtering/FilterOperatorBuilder.cs index e286c2bd..e268f9e1 100644 --- a/src/Strainer/Services/Filtering/FilterOperatorBuilder.cs +++ b/src/Strainer/Services/Filtering/FilterOperatorBuilder.cs @@ -1,74 +1,11 @@ -using Fluorite.Strainer.Models.Filtering.Operators; -using System.Linq.Expressions; - -namespace Fluorite.Strainer.Services.Filtering; +namespace Fluorite.Strainer.Services.Filtering; public class FilterOperatorBuilder : IFilterOperatorBuilder { - public FilterOperatorBuilder() - { - } - - public FilterOperatorBuilder(string symbol) - { - Symbol = Guard.Against.NullOrWhiteSpace(symbol); - } - - protected Func? Expression { get; set; } - - protected bool IsCaseInsensitive1 { get; set; } - - protected bool IsStringBased1 { get; set; } - - protected string? Name { get; set; } - - protected string? Symbol { get; set; } - - public IFilterOperator Build() - { - Guard.Against.NullOrWhiteSpace(Name, message: "Missing or invalid filter operator name."); - Guard.Against.NullOrWhiteSpace(Symbol, message: "Missing or invalid filter operator symbol."); - Guard.Against.Null(Expression, message: "Missing filter operator expression."); - - return new FilterOperator(Name, Symbol, Expression) - { - IsCaseInsensitive = IsCaseInsensitive1, - IsStringBased = IsStringBased1, - }; - } - - public IFilterOperatorBuilder HasExpression(Func expression) - { - Expression = Guard.Against.Null(expression); - - return this; - } - - public IFilterOperatorBuilder HasName(string name) - { - Name = Guard.Against.NullOrWhiteSpace(name); - - return this; - } - - public IFilterOperatorBuilder HasSymbol(string symbol) - { - Symbol = Guard.Against.NullOrWhiteSpace(symbol); - - return this; - } - - public IFilterOperatorBuilder IsCaseInsensitive() - { - IsCaseInsensitive1 = true; - - return this; - } - - public IFilterOperatorBuilder IsStringBased() + public IFilterOperatorBuilderWithSymbol HasSymbol(string symbol) { - IsStringBased1 = true; + Guard.Against.NullOrWhiteSpace(symbol); - return this; + return new FilterOperatorBuilderWithSymbol(symbol); } } diff --git a/src/Strainer/Services/Filtering/FilterOperatorBuilderWithExpression.cs b/src/Strainer/Services/Filtering/FilterOperatorBuilderWithExpression.cs new file mode 100644 index 00000000..749a8731 --- /dev/null +++ b/src/Strainer/Services/Filtering/FilterOperatorBuilderWithExpression.cs @@ -0,0 +1,45 @@ +using Fluorite.Strainer.Models.Filtering.Operators; +using System.Linq.Expressions; + +namespace Fluorite.Strainer.Services.Filtering; + +public class FilterOperatorBuilderWithExpression : FilterOperatorBuilderWithName, IFilterOperatorBuilderWithExpression +{ + public FilterOperatorBuilderWithExpression( + string symbol, + string name, + Func expression) + : base(symbol, name) + { + Expression = Guard.Against.Null(expression); + } + + protected Func Expression { get; } + + protected bool IsCaseInsensitiveValue { get; private set; } + + protected bool IsStringBasedValue { get; private set; } + + public IFilterOperator Build() + { + return new FilterOperator(Name, Symbol, Expression) + { + IsCaseInsensitive = IsCaseInsensitiveValue, + IsStringBased = IsStringBasedValue, + }; + } + + public IFilterOperatorBuilderWithExpression IsCaseInsensitive() + { + IsCaseInsensitiveValue = true; + + return this; + } + + public IFilterOperatorBuilderWithExpression IsStringBased() + { + IsStringBasedValue = true; + + return this; + } +} diff --git a/src/Strainer/Services/Filtering/FilterOperatorBuilderWithName.cs b/src/Strainer/Services/Filtering/FilterOperatorBuilderWithName.cs new file mode 100644 index 00000000..6da2bd18 --- /dev/null +++ b/src/Strainer/Services/Filtering/FilterOperatorBuilderWithName.cs @@ -0,0 +1,22 @@ +using Fluorite.Strainer.Models.Filtering.Operators; +using System.Linq.Expressions; + +namespace Fluorite.Strainer.Services.Filtering; + +public class FilterOperatorBuilderWithName : FilterOperatorBuilderWithSymbol, IFilterOperatorBuilderWithName +{ + public FilterOperatorBuilderWithName(string symbol, string name) + : base(symbol) + { + Name = Guard.Against.NullOrWhiteSpace(name); + } + + protected string Name { get; } + + public IFilterOperatorBuilderWithExpression HasExpression(Func expression) + { + Guard.Against.Null(expression); + + return new FilterOperatorBuilderWithExpression(Symbol, Name, expression); + } +} diff --git a/src/Strainer/Services/Filtering/FilterOperatorBuilderWithSymbol.cs b/src/Strainer/Services/Filtering/FilterOperatorBuilderWithSymbol.cs new file mode 100644 index 00000000..4a3205e7 --- /dev/null +++ b/src/Strainer/Services/Filtering/FilterOperatorBuilderWithSymbol.cs @@ -0,0 +1,18 @@ +namespace Fluorite.Strainer.Services.Filtering; + +public class FilterOperatorBuilderWithSymbol : FilterOperatorBuilder, IFilterOperatorBuilderWithSymbol +{ + public FilterOperatorBuilderWithSymbol(string symbol) + { + Symbol = Guard.Against.NullOrWhiteSpace(symbol); + } + + protected string Symbol { get; } + + public IFilterOperatorBuilderWithName HasName(string name) + { + Guard.Against.NullOrWhiteSpace(name); + + return new FilterOperatorBuilderWithName(Symbol, name); + } +} diff --git a/src/Strainer/Services/Filtering/FilterOperatorMapper.cs b/src/Strainer/Services/Filtering/FilterOperatorMapper.cs index f024fdc6..b99c48a0 100644 --- a/src/Strainer/Services/Filtering/FilterOperatorMapper.cs +++ b/src/Strainer/Services/Filtering/FilterOperatorMapper.cs @@ -35,11 +35,13 @@ public static class FilterOperatorMapper { return new List { - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.EqualsSymbol) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.EqualsSymbol) .HasName("equal") .HasExpression((context) => Expression.Equal(context.PropertyValue, context.FilterValue)) .Build(), - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.DoesNotEqual) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.DoesNotEqual) .HasName("does not equal") .HasExpression((context) => Expression.NotEqual(context.PropertyValue, context.FilterValue)) .Build(), @@ -50,11 +52,11 @@ public static class FilterOperatorMapper { return new List { - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.LessThan) + new FilterOperatorBuilder().HasSymbol(FilterOperatorSymbols.LessThan) .HasName("less than") .HasExpression((context) => Expression.LessThan(context.PropertyValue, context.FilterValue)) .Build(), - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.LessThanOrEqualTo) + new FilterOperatorBuilder().HasSymbol(FilterOperatorSymbols.LessThanOrEqualTo) .HasName("less than or equal to") .HasExpression((context) => Expression.LessThanOrEqual(context.PropertyValue, context.FilterValue)) .Build(), @@ -65,11 +67,11 @@ public static class FilterOperatorMapper { return new List { - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.GreaterThan) + new FilterOperatorBuilder().HasSymbol(FilterOperatorSymbols.GreaterThan) .HasName("greater than") .HasExpression((context) => Expression.GreaterThan(context.PropertyValue, context.FilterValue)) .Build(), - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.GreaterThanOrEqualTo) + new FilterOperatorBuilder().HasSymbol(FilterOperatorSymbols.GreaterThanOrEqualTo) .HasName("greater than or equal to") .HasExpression((context) => Expression.GreaterThanOrEqual(context.PropertyValue, context.FilterValue)) .Build(), @@ -80,29 +82,32 @@ public static class FilterOperatorMapper { return new List { - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.Contains) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.Contains) .HasName("contains") - .IsStringBased() .HasExpression((context) => Expression.Call( context.PropertyValue, typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string) }), context.FilterValue)) + .IsStringBased() .Build(), - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.StartsWith) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.StartsWith) .HasName("starts with") - .IsStringBased() .HasExpression((context) => Expression.Call( context.PropertyValue, typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string) }), context.FilterValue)) + .IsStringBased() .Build(), - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.EndsWith) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.EndsWith) .HasName("ends with") - .IsStringBased() .HasExpression((context) => Expression.Call( context.PropertyValue, typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string) }), context.FilterValue)) + .IsStringBased() .Build(), }; } @@ -111,29 +116,32 @@ public static class FilterOperatorMapper { return new List { - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.DoesNotContain) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.DoesNotContain) .HasName("does not contain") - .IsStringBased() .HasExpression((context) => Expression.Not(Expression.Call( context.PropertyValue, typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string) }), context.FilterValue))) + .IsStringBased() .Build(), - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.DoesNotStartWith) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.DoesNotStartWith) .HasName("does not start with") - .IsStringBased() .HasExpression((context) => Expression.Not(Expression.Call( context.PropertyValue, typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string) }), context.FilterValue))) + .IsStringBased() .Build(), - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.DoesNotEndWith) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.DoesNotEndWith) .HasName("does not end with") - .IsStringBased() .HasExpression((context) => Expression.Not(Expression.Call( context.PropertyValue, typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string) }), context.FilterValue))) + .IsStringBased() .Build(), }; } @@ -142,9 +150,9 @@ public static class FilterOperatorMapper { return new List { - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.EqualsCaseInsensitive) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.EqualsCaseInsensitive) .HasName("equal (case insensitive)") - .IsStringBased() .HasExpression((context) => Expression.Call( typeof(string).GetMethod( nameof(string.Equals), @@ -155,11 +163,12 @@ public static class FilterOperatorMapper context.PropertyValue, context.FilterValue, Expression.Constant(StringComparison.OrdinalIgnoreCase))) + .IsStringBased() .IsCaseInsensitive() .Build(), - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.DoesNotEqualCaseInsensitive) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.DoesNotEqualCaseInsensitive) .HasName("does not equal (case insensitive)") - .IsStringBased() .HasExpression((context) => Expression.Not(Expression.Call( typeof(string).GetMethod( nameof(string.Equals), @@ -170,6 +179,7 @@ public static class FilterOperatorMapper context.PropertyValue, context.FilterValue, Expression.Constant(StringComparison.OrdinalIgnoreCase)))) + .IsStringBased() .IsCaseInsensitive() .Build(), }; @@ -179,9 +189,9 @@ public static class FilterOperatorMapper { return new List { - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.ContainsCaseInsensitive) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.ContainsCaseInsensitive) .HasName("contains (case insensitive)") - .IsStringBased() .HasExpression((context) => { #if NETSTANDARD2_0 @@ -194,26 +204,29 @@ public static class FilterOperatorMapper throw new NotSupportedException("Runtime version does not support case-insensitive contains method."); #endif }) + .IsStringBased() .IsCaseInsensitive() .Build(), - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.StartsWithCaseInsensitive) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.StartsWithCaseInsensitive) .HasName("starts with (case insensitive)") - .IsStringBased() .HasExpression((context) => Expression.Call( context.PropertyValue, typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string), typeof(StringComparison) }), context.FilterValue, Expression.Constant(StringComparison.OrdinalIgnoreCase))) + .IsStringBased() .IsCaseInsensitive() .Build(), - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.EndsWithCaseInsensitive) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.EndsWithCaseInsensitive) .HasName("ends with (case insensitive)") - .IsStringBased() .HasExpression((context) => Expression.Call( context.PropertyValue, typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string), typeof(StringComparison) }), context.FilterValue, Expression.Constant(StringComparison.OrdinalIgnoreCase))) + .IsStringBased() .IsCaseInsensitive() .Build(), }; @@ -223,9 +236,9 @@ public static class FilterOperatorMapper { return new List { - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.DoesNotContainCaseInsensitive) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.DoesNotContainCaseInsensitive) .HasName("does not contain (case insensitive)") - .IsStringBased() .HasExpression((context) => { #if NETSTANDARD2_0 @@ -238,26 +251,29 @@ public static class FilterOperatorMapper throw new NotSupportedException("Runtime version does not support case-insensitive contains method."); #endif }) + .IsStringBased() .IsCaseInsensitive() .Build(), - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.DoesNotStartWithCaseInsensitive) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.DoesNotStartWithCaseInsensitive) .HasName("does not start with (case insensitive)") - .IsStringBased() .HasExpression((context) => Expression.Not(Expression.Call( context.PropertyValue, typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string), typeof(StringComparison) }), context.FilterValue, Expression.Constant(StringComparison.OrdinalIgnoreCase)))) + .IsStringBased() .IsCaseInsensitive() .Build(), - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.DoesNotEndWithCaseInsensitive) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.DoesNotEndWithCaseInsensitive) .HasName("does not end with (case insensitive)") - .IsStringBased() .HasExpression((context) => Expression.Not(Expression.Call( context.PropertyValue, typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string), typeof(StringComparison) }), context.FilterValue, Expression.Constant(StringComparison.OrdinalIgnoreCase)))) + .IsStringBased() .IsCaseInsensitive() .Build(), }; diff --git a/src/Strainer/Services/Filtering/ICustomFilterMethodBuilder.cs b/src/Strainer/Services/Filtering/ICustomFilterMethodBuilder.cs index 25ca3fe3..4737ca37 100644 --- a/src/Strainer/Services/Filtering/ICustomFilterMethodBuilder.cs +++ b/src/Strainer/Services/Filtering/ICustomFilterMethodBuilder.cs @@ -1,16 +1,6 @@ -using Fluorite.Strainer.Models.Filtering; -using Fluorite.Strainer.Models.Filtering.Terms; -using System.Linq.Expressions; - -namespace Fluorite.Strainer.Services.Filtering; +namespace Fluorite.Strainer.Services.Filtering; public interface ICustomFilterMethodBuilder { - ICustomFilterMethod Build(); - - ICustomFilterMethodBuilder HasFunction(Expression> expression); - - ICustomFilterMethodBuilder HasFunction(Func>> filterTermExpression); - - ICustomFilterMethodBuilder HasName(string name); + ICustomFilterMethodBuilderWithName HasName(string name); } diff --git a/src/Strainer/Services/Filtering/ICustomFilterMethodBuilderWithExpression.cs b/src/Strainer/Services/Filtering/ICustomFilterMethodBuilderWithExpression.cs new file mode 100644 index 00000000..58e0b6d2 --- /dev/null +++ b/src/Strainer/Services/Filtering/ICustomFilterMethodBuilderWithExpression.cs @@ -0,0 +1,8 @@ +using Fluorite.Strainer.Models.Filtering; + +namespace Fluorite.Strainer.Services.Filtering; + +public interface ICustomFilterMethodBuilderWithExpression : ICustomFilterMethodBuilderWithName +{ + ICustomFilterMethod Build(); +} diff --git a/src/Strainer/Services/Filtering/ICustomFilterMethodBuilderWithName.cs b/src/Strainer/Services/Filtering/ICustomFilterMethodBuilderWithName.cs new file mode 100644 index 00000000..cc89297f --- /dev/null +++ b/src/Strainer/Services/Filtering/ICustomFilterMethodBuilderWithName.cs @@ -0,0 +1,11 @@ +using Fluorite.Strainer.Models.Filtering.Terms; +using System.Linq.Expressions; + +namespace Fluorite.Strainer.Services.Filtering; + +public interface ICustomFilterMethodBuilderWithName : ICustomFilterMethodBuilder +{ + ICustomFilterMethodBuilderWithExpression HasFunction(Expression> expression); + + ICustomFilterMethodBuilderWithExpression HasFunction(Func>> filterTermExpression); +} diff --git a/src/Strainer/Services/Filtering/IFilterOperatorBuilder.cs b/src/Strainer/Services/Filtering/IFilterOperatorBuilder.cs index 7028ddc2..b0258ab2 100644 --- a/src/Strainer/Services/Filtering/IFilterOperatorBuilder.cs +++ b/src/Strainer/Services/Filtering/IFilterOperatorBuilder.cs @@ -1,19 +1,6 @@ -using Fluorite.Strainer.Models.Filtering.Operators; -using System.Linq.Expressions; - -namespace Fluorite.Strainer.Services.Filtering; +namespace Fluorite.Strainer.Services.Filtering; public interface IFilterOperatorBuilder { - IFilterOperator Build(); - - IFilterOperatorBuilder HasExpression(Func expression); - - IFilterOperatorBuilder HasName(string name); - - IFilterOperatorBuilder HasSymbol(string symbol); - - IFilterOperatorBuilder IsCaseInsensitive(); - - IFilterOperatorBuilder IsStringBased(); + IFilterOperatorBuilderWithSymbol HasSymbol(string symbol); } diff --git a/src/Strainer/Services/Filtering/IFilterOperatorBuilderWithExpression.cs b/src/Strainer/Services/Filtering/IFilterOperatorBuilderWithExpression.cs new file mode 100644 index 00000000..564c56ce --- /dev/null +++ b/src/Strainer/Services/Filtering/IFilterOperatorBuilderWithExpression.cs @@ -0,0 +1,12 @@ +using Fluorite.Strainer.Models.Filtering.Operators; + +namespace Fluorite.Strainer.Services.Filtering; + +public interface IFilterOperatorBuilderWithExpression : IFilterOperatorBuilderWithName +{ + IFilterOperator Build(); + + IFilterOperatorBuilderWithExpression IsCaseInsensitive(); + + IFilterOperatorBuilderWithExpression IsStringBased(); +} diff --git a/src/Strainer/Services/Filtering/IFilterOperatorBuilderWithName.cs b/src/Strainer/Services/Filtering/IFilterOperatorBuilderWithName.cs new file mode 100644 index 00000000..785c6438 --- /dev/null +++ b/src/Strainer/Services/Filtering/IFilterOperatorBuilderWithName.cs @@ -0,0 +1,9 @@ +using Fluorite.Strainer.Models.Filtering.Operators; +using System.Linq.Expressions; + +namespace Fluorite.Strainer.Services.Filtering; + +public interface IFilterOperatorBuilderWithName : IFilterOperatorBuilderWithSymbol +{ + IFilterOperatorBuilderWithExpression HasExpression(Func expression); +} diff --git a/src/Strainer/Services/Filtering/IFilterOperatorBuilderWithSymbol.cs b/src/Strainer/Services/Filtering/IFilterOperatorBuilderWithSymbol.cs new file mode 100644 index 00000000..0ef799bd --- /dev/null +++ b/src/Strainer/Services/Filtering/IFilterOperatorBuilderWithSymbol.cs @@ -0,0 +1,6 @@ +namespace Fluorite.Strainer.Services.Filtering; + +public interface IFilterOperatorBuilderWithSymbol : IFilterOperatorBuilder +{ + IFilterOperatorBuilderWithName HasName(string name); +} diff --git a/src/Strainer/Services/Sorting/CustomSortMethodBuilder.cs b/src/Strainer/Services/Sorting/CustomSortMethodBuilder.cs index a8e0865b..8bfb3112 100644 --- a/src/Strainer/Services/Sorting/CustomSortMethodBuilder.cs +++ b/src/Strainer/Services/Sorting/CustomSortMethodBuilder.cs @@ -1,8 +1,4 @@ -using Fluorite.Strainer.Models.Sorting; -using Fluorite.Strainer.Models.Sorting.Terms; -using System.Linq.Expressions; - -namespace Fluorite.Strainer.Services.Sorting; +namespace Fluorite.Strainer.Services.Sorting; public class CustomSortMethodBuilder : ICustomSortMethodBuilder { @@ -10,51 +6,10 @@ public class CustomSortMethodBuilder : ICustomSortMethodBuilder>? Expression { get; set; } - - protected Func>>? ExpressionProvider { get; set; } - - protected string? Name { get; set; } - - public ICustomSortMethod Build() - { - Guard.Against.NullOrWhiteSpace(Name); - - if (ExpressionProvider is null) - { - Guard.Against.Null(Expression); - - return new CustomSortMethod(Name, Expression); - } - else - { - Guard.Against.Null(ExpressionProvider); - - return new CustomSortMethod(Name, ExpressionProvider); - } - } - - public ICustomSortMethodBuilder HasFunction( - Expression> expression) - { - Expression = Guard.Against.Null(expression); - ExpressionProvider = null; - - return this; - } - - public ICustomSortMethodBuilder HasFunction(Func>> expressionProvider) - { - ExpressionProvider = Guard.Against.Null(expressionProvider); - Expression = null; - - return this; - } - - public ICustomSortMethodBuilder HasName(string name) + public ICustomSortMethodBuilderWithName HasName(string name) { - Name = Guard.Against.NullOrWhiteSpace(name); + Guard.Against.NullOrWhiteSpace(name); - return this; + return new CustomSortMethodBuilderWithName(name); } } \ No newline at end of file diff --git a/src/Strainer/Services/Sorting/CustomSortMethodBuilderWithExpression.cs b/src/Strainer/Services/Sorting/CustomSortMethodBuilderWithExpression.cs new file mode 100644 index 00000000..5cb33da1 --- /dev/null +++ b/src/Strainer/Services/Sorting/CustomSortMethodBuilderWithExpression.cs @@ -0,0 +1,39 @@ +using Fluorite.Strainer.Models.Sorting; +using Fluorite.Strainer.Models.Sorting.Terms; +using System.Linq.Expressions; + +namespace Fluorite.Strainer.Services.Sorting; + +public class CustomSortMethodBuilderWithExpression : CustomSortMethodBuilderWithName, ICustomSortMethodBuilderWithExpression +{ + public CustomSortMethodBuilderWithExpression( + string name, Expression> expression) + : base(name) + { + Expression = Guard.Against.Null(expression); + } + + public CustomSortMethodBuilderWithExpression( + string name, + Func>> sortTermExpression) + : base(name) + { + SortTermExpression = Guard.Against.Null(sortTermExpression); + } + + public Expression>? Expression { get; } + + public Func>>? SortTermExpression { get; } + + public ICustomSortMethod Build() + { + if (SortTermExpression is null) + { + return new CustomSortMethod(Name, Expression!); + } + else + { + return new CustomSortMethod(Name, SortTermExpression); + } + } +} diff --git a/src/Strainer/Services/Sorting/CustomSortMethodBuilderWithName.cs b/src/Strainer/Services/Sorting/CustomSortMethodBuilderWithName.cs new file mode 100644 index 00000000..9534c7b7 --- /dev/null +++ b/src/Strainer/Services/Sorting/CustomSortMethodBuilderWithName.cs @@ -0,0 +1,29 @@ +using Fluorite.Strainer.Models.Sorting.Terms; +using System.Linq.Expressions; + +namespace Fluorite.Strainer.Services.Sorting; + +public class CustomSortMethodBuilderWithName : CustomSortMethodBuilder, ICustomSortMethodBuilderWithName +{ + public CustomSortMethodBuilderWithName(string name) + { + Name = Guard.Against.NullOrWhiteSpace(name); + } + + protected string Name { get; } + + public ICustomSortMethodBuilderWithExpression HasFunction(Expression> expression) + { + Guard.Against.Null(expression); + + return new CustomSortMethodBuilderWithExpression(Name, expression); + } + + public ICustomSortMethodBuilderWithExpression HasFunction( + Func>> sortTermExpression) + { + Guard.Against.Null(sortTermExpression); + + return new CustomSortMethodBuilderWithExpression(Name, sortTermExpression); + } +} diff --git a/src/Strainer/Services/Sorting/ICustomSortMethodBuilder.cs b/src/Strainer/Services/Sorting/ICustomSortMethodBuilder.cs index 0fec1ecd..4932d3ca 100644 --- a/src/Strainer/Services/Sorting/ICustomSortMethodBuilder.cs +++ b/src/Strainer/Services/Sorting/ICustomSortMethodBuilder.cs @@ -1,16 +1,6 @@ -using Fluorite.Strainer.Models.Sorting; -using Fluorite.Strainer.Models.Sorting.Terms; -using System.Linq.Expressions; - -namespace Fluorite.Strainer.Services.Sorting; +namespace Fluorite.Strainer.Services.Sorting; public interface ICustomSortMethodBuilder { - ICustomSortMethod Build(); - - ICustomSortMethodBuilder HasFunction(Expression> expression); - - ICustomSortMethodBuilder HasFunction(Func>> expressionProvider); - - ICustomSortMethodBuilder HasName(string name); + ICustomSortMethodBuilderWithName HasName(string name); } diff --git a/src/Strainer/Services/Sorting/ICustomSortMethodBuilderWithExpression.cs b/src/Strainer/Services/Sorting/ICustomSortMethodBuilderWithExpression.cs new file mode 100644 index 00000000..6e26c3e2 --- /dev/null +++ b/src/Strainer/Services/Sorting/ICustomSortMethodBuilderWithExpression.cs @@ -0,0 +1,8 @@ +using Fluorite.Strainer.Models.Sorting; + +namespace Fluorite.Strainer.Services.Sorting; + +public interface ICustomSortMethodBuilderWithExpression : ICustomSortMethodBuilderWithName +{ + ICustomSortMethod Build(); +} diff --git a/src/Strainer/Services/Sorting/ICustomSortMethodBuilderWithName.cs b/src/Strainer/Services/Sorting/ICustomSortMethodBuilderWithName.cs new file mode 100644 index 00000000..2b50adc5 --- /dev/null +++ b/src/Strainer/Services/Sorting/ICustomSortMethodBuilderWithName.cs @@ -0,0 +1,11 @@ +using Fluorite.Strainer.Models.Sorting.Terms; +using System.Linq.Expressions; + +namespace Fluorite.Strainer.Services.Sorting; + +public interface ICustomSortMethodBuilderWithName : ICustomSortMethodBuilder +{ + ICustomSortMethodBuilderWithExpression HasFunction(Expression> expression); + + ICustomSortMethodBuilderWithExpression HasFunction(Func>> sortTermExpression); +} diff --git a/test/Strainer.UnitTests/Services/Filtering/CustomFilterMethodBuilderTests.cs b/test/Strainer.UnitTests/Services/Filtering/CustomFilterMethodBuilderTests.cs new file mode 100644 index 00000000..ce8dfa41 --- /dev/null +++ b/test/Strainer.UnitTests/Services/Filtering/CustomFilterMethodBuilderTests.cs @@ -0,0 +1,77 @@ +using Fluorite.Strainer.Services.Filtering; +using System.Linq.Expressions; + +namespace Fluorite.Strainer.UnitTests.Services.Filtering; + +public class CustomFilterMethodBuilderTests +{ + [Fact] + public void Should_Build_CustomFilterMethod_UsingDirectExpression() + { + // Arrange + var name = "foo"; + Expression> expression1 = x => x.Author != null; + var builder = new CustomFilterMethodBuilder() + .HasName(name) + .HasFunction(expression1); + + // Act + var result = builder.Build(); + + // Assert + result.Should().NotBeNull(); + result.Expression.Should().BeSameAs(expression1); + result.Name.Should().Be(name); + result.FilterTermExpression.Should().BeNull(); + } + + [Fact] + public void Should_Build_CustomFilterMethod_UsingProviderExpression() + { + // Arrange + var name = "foo"; + Expression> expression1 = x => x.Author != null; + var builder = new CustomFilterMethodBuilder() + .HasName(name) + .HasFunction(_ => expression1); + + // Act + var result = builder.Build(); + + // Assert + result.Should().NotBeNull(); + result.Expression.Should().BeNull(); + result.Name.Should().Be(name); + result.FilterTermExpression.Should().NotBeNull(); + result.FilterTermExpression.Invoke(null).Should().BeSameAs(expression1); + } + + [Fact] + public void Should_Build_CustomFilterMethod_UsingDirectExpression_ErasingPreviousExpression() + { + // Arrange + var name = "foo"; + Expression> expression1 = x => x.Author != null; + Expression> expression2 = x => x.Name != null; + var builder = new CustomFilterMethodBuilder() + .HasName(name) + .HasFunction(expression1) + .HasFunction(expression2); + + // Act + var result = builder.Build(); + + // Assert + result.Should().NotBeNull(); + result.Expression.Should().BeSameAs(expression2); + result.Name.Should().BeSameAs(name); + result.FilterTermExpression.Should().BeNull(); + } + + private class Post + { + public string Author { get; set; } + + public string Name { get; set; } + } +} diff --git a/test/Strainer.UnitTests/Services/Filtering/FilterOperatorBuilderTests.cs b/test/Strainer.UnitTests/Services/Filtering/FilterOperatorBuilderTests.cs index b4c5152e..eecd7b73 100644 --- a/test/Strainer.UnitTests/Services/Filtering/FilterOperatorBuilderTests.cs +++ b/test/Strainer.UnitTests/Services/Filtering/FilterOperatorBuilderTests.cs @@ -15,7 +15,8 @@ public class FilterOperatorBuilderTests var expression = Expression.Empty(); // Act - var filterOperator = new FilterOperatorBuilder(symbol) + var filterOperator = new FilterOperatorBuilder() + .HasSymbol(symbol) .HasName(name) .HasExpression(_ => expression) .Build(); diff --git a/test/Strainer.UnitTests/Services/Sorting/CustomSortMethodBuilderTests.cs b/test/Strainer.UnitTests/Services/Sorting/CustomSortMethodBuilderTests.cs index d9efea53..b1016358 100644 --- a/test/Strainer.UnitTests/Services/Sorting/CustomSortMethodBuilderTests.cs +++ b/test/Strainer.UnitTests/Services/Sorting/CustomSortMethodBuilderTests.cs @@ -1,5 +1,5 @@ -using Fluorite.Strainer.Models.Sorting; -using Fluorite.Strainer.Services.Sorting; +using Fluorite.Strainer.Services.Sorting; +using System.Linq.Expressions; namespace Fluorite.Strainer.UnitTests.Services.Sorting; @@ -10,15 +10,17 @@ public class CustomSortMethodBuilderTests { // Arrange var name = "foo"; - var builder = new CustomSortMethodBuilder().HasName(name); + Expression> expression1 = x => x.Author != null; + var builder = new CustomSortMethodBuilder() + .HasName(name) + .HasFunction(expression1); // Act - builder.HasFunction(x => x.Name); var result = builder.Build(); // Assert result.Should().NotBeNull(); - result.Expression.Should().NotBeNull(); + result.Expression.Should().BeSameAs(expression1); result.Name.Should().Be(name); result.ExpressionProvider.Should().BeNull(); } @@ -28,10 +30,12 @@ public class CustomSortMethodBuilderTests { // Arrange var name = "foo"; - var builder = new CustomSortMethodBuilder().HasName(name); + Expression> expression1 = x => x.Author != null; + var builder = new CustomSortMethodBuilder() + .HasName(name) + .HasFunction(_ => expression1); // Act - builder.HasFunction((sortTerm) => sortTerm.IsDescending ? (x => x.Author) : (x => x.Name)); var result = builder.Build(); // Assert @@ -39,6 +43,7 @@ public class CustomSortMethodBuilderTests result.Expression.Should().BeNull(); result.Name.Should().Be(name); result.ExpressionProvider.Should().NotBeNull(); + result.ExpressionProvider.Invoke(null).Should().BeSameAs(expression1); } [Fact] @@ -46,17 +51,20 @@ public class CustomSortMethodBuilderTests { // Arrange var name = "foo"; - var builder = new CustomSortMethodBuilder().HasName(name); + Expression> expression1 = x => x.Author != null; + Expression> expression2 = x => x.Name != null; + var builder = new CustomSortMethodBuilder() + .HasName(name) + .HasFunction(expression1) + .HasFunction(expression2); // Act - builder.HasFunction(term => x => x.Name); - builder.HasFunction(x => x.Name); var result = builder.Build(); // Assert result.Should().NotBeNull(); - result.Expression.Should().NotBeNull(); - result.Name.Should().Be(name); + result.Expression.Should().BeSameAs(expression2); + result.Name.Should().BeSameAs(name); result.ExpressionProvider.Should().BeNull(); } -- GitLab From 6bacb4b5fd7358ea6b6cd41fecc3aec6cc27bade Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Tue, 14 Jan 2025 19:40:14 +0100 Subject: [PATCH 24/56] Add unit tests for generic Strainer module builder --- .../GenericStrainerModuleBuilderTests.cs | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 test/Strainer.UnitTests/Services/Modules/GenericStrainerModuleBuilderTests.cs diff --git a/test/Strainer.UnitTests/Services/Modules/GenericStrainerModuleBuilderTests.cs b/test/Strainer.UnitTests/Services/Modules/GenericStrainerModuleBuilderTests.cs new file mode 100644 index 00000000..7bbb80e4 --- /dev/null +++ b/test/Strainer.UnitTests/Services/Modules/GenericStrainerModuleBuilderTests.cs @@ -0,0 +1,72 @@ +using Fluorite.Strainer.Models; +using Fluorite.Strainer.Models.Metadata; +using Fluorite.Strainer.Services.Filtering; +using Fluorite.Strainer.Services.Metadata; +using Fluorite.Strainer.Services.Modules; + +namespace Fluorite.Strainer.UnitTests.Services.Modules; + +public class GenericStrainerModuleBuilderTests +{ + private readonly IPropertyInfoProvider _propertyInfoProviderMock = Substitute.For(); + + [Fact] + public void Should_Remove_BuiltInFilterOperator_FromModule() + { + // Arrange + var symbol = FilterOperatorSymbols.EqualsSymbol; + var builtInSymbols = new HashSet + { + symbol, + }; + var strainerModuleMock = Substitute.For(); + strainerModuleMock.ExcludedBuiltInFilterOperators.Returns(builtInSymbols); + var strainerOptions = new StrainerOptions(); + var builder = new StrainerModuleBuilder( + _propertyInfoProviderMock, + strainerModuleMock, + strainerOptions); + + // Act + builder.RemoveBuiltInFilterOperator(FilterOperatorSymbols.EqualsSymbol); + + // Assert + _propertyInfoProviderMock.ReceivedCalls().Should().BeEmpty(); + builtInSymbols.Should().BeEmpty(); + _ = strainerModuleMock + .Received(1) + .ExcludedBuiltInFilterOperators; + } + + [Fact] + public void Should_Throw_WhenAddingObjectMetadata_WhileFluentApiIsDisabled() + { + // Arrange + var strainerModuleMock = Substitute.For(); + var strainerOptions = new StrainerOptions + { + MetadataSourceType = MetadataSourceType.None, + }; + var builder = new StrainerModuleBuilder( + _propertyInfoProviderMock, + strainerModuleMock, + strainerOptions); + + // Act + Action act = () => builder.AddObject(x => x.Id); + + // Assert + act.Should().Throw() + .WithMessage( + $"Current {nameof(MetadataSourceType)} setting does not " + + $"support {nameof(MetadataSourceType.FluentApi)}. " + + $"Include {nameof(MetadataSourceType.FluentApi)} option to " + + $"be able to use it."); + _propertyInfoProviderMock.ReceivedCalls().Should().BeEmpty(); + } + + private class Stub + { + public int Id { get; set; } + } +} -- GitLab From f0fe00cfde54d888963cfd70c20800b701ef11a1 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Tue, 14 Jan 2025 21:54:30 +0100 Subject: [PATCH 25/56] Add dependency tests | Fluent Assertions 8.0 rumpus --- test/Strainer.UnitTests/DependencyTests.cs | 24 ++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 test/Strainer.UnitTests/DependencyTests.cs diff --git a/test/Strainer.UnitTests/DependencyTests.cs b/test/Strainer.UnitTests/DependencyTests.cs new file mode 100644 index 00000000..16548118 --- /dev/null +++ b/test/Strainer.UnitTests/DependencyTests.cs @@ -0,0 +1,24 @@ +namespace Fluorite.Strainer.UnitTests; + +public class DependencyTests +{ + private const string Explanation = + "Starting from Version 8.0 Fluent Assertions uses new non-commercial license. " + + "Even though this is an OSS project, so non-commercial license does not change " + + "anything especially when not shipped along the library. " + + "But still, this rugpull was pretty nasty, so let's stay on 7.x.x for as long as we can. " + + "See https://github.com/fluentassertions/fluentassertions/pull/2943 for more."; + + [Fact] + public void Should_NotUse_FluentAssertions_Version8() + { + // Act + var fluentAssertionsAssembly = AppDomain.CurrentDomain.Load("FluentAssertions"); + + // Assert + fluentAssertionsAssembly.Should().NotBeNull(); + fluentAssertionsAssembly.GetName().Should().NotBeNull(); + fluentAssertionsAssembly.GetName().Version.Should().NotBeNull(); + fluentAssertionsAssembly.GetName().Version.Should().BeLessThan(new Version(8, 0), because: Explanation); + } +} -- GitLab From 251bc0dd8d0d79f339873ebe110088e55552731a Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Tue, 14 Jan 2025 21:55:59 +0100 Subject: [PATCH 26/56] Fix bug in generic strainer module builder for built in filter operators --- .../Services/Modules/StrainerModuleBuilder{T}.cs | 2 +- .../Modules/GenericStrainerModuleBuilderTests.cs | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Strainer/Services/Modules/StrainerModuleBuilder{T}.cs b/src/Strainer/Services/Modules/StrainerModuleBuilder{T}.cs index b9b71bbd..5d13060c 100644 --- a/src/Strainer/Services/Modules/StrainerModuleBuilder{T}.cs +++ b/src/Strainer/Services/Modules/StrainerModuleBuilder{T}.cs @@ -201,7 +201,7 @@ public class StrainerModuleBuilder : IStrainerModuleBuilder { Guard.Against.NullOrWhiteSpace(symbol); - Module.ExcludedBuiltInFilterOperators.Remove(symbol); + Module.ExcludedBuiltInFilterOperators.Add(symbol); return this; } diff --git a/test/Strainer.UnitTests/Services/Modules/GenericStrainerModuleBuilderTests.cs b/test/Strainer.UnitTests/Services/Modules/GenericStrainerModuleBuilderTests.cs index 7bbb80e4..a534bd1d 100644 --- a/test/Strainer.UnitTests/Services/Modules/GenericStrainerModuleBuilderTests.cs +++ b/test/Strainer.UnitTests/Services/Modules/GenericStrainerModuleBuilderTests.cs @@ -11,14 +11,11 @@ public class GenericStrainerModuleBuilderTests private readonly IPropertyInfoProvider _propertyInfoProviderMock = Substitute.For(); [Fact] - public void Should_Remove_BuiltInFilterOperator_FromModule() + public void Should_Add_SymbolToExcludedListOfBuiltInFilterOperator() { // Arrange var symbol = FilterOperatorSymbols.EqualsSymbol; - var builtInSymbols = new HashSet - { - symbol, - }; + var builtInSymbols = new HashSet(); var strainerModuleMock = Substitute.For(); strainerModuleMock.ExcludedBuiltInFilterOperators.Returns(builtInSymbols); var strainerOptions = new StrainerOptions(); @@ -32,7 +29,7 @@ public class GenericStrainerModuleBuilderTests // Assert _propertyInfoProviderMock.ReceivedCalls().Should().BeEmpty(); - builtInSymbols.Should().BeEmpty(); + builtInSymbols.Should().BeEquivalentTo(symbol); _ = strainerModuleMock .Received(1) .ExcludedBuiltInFilterOperators; -- GitLab From b371425f5b686b1258961e6b0a7c30e219a13ff6 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Tue, 14 Jan 2025 21:56:17 +0100 Subject: [PATCH 27/56] Add more unit tests for generic Strainer module builder --- .../GenericStrainerModuleBuilderTests.cs | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/test/Strainer.UnitTests/Services/Modules/GenericStrainerModuleBuilderTests.cs b/test/Strainer.UnitTests/Services/Modules/GenericStrainerModuleBuilderTests.cs index a534bd1d..b876bf90 100644 --- a/test/Strainer.UnitTests/Services/Modules/GenericStrainerModuleBuilderTests.cs +++ b/test/Strainer.UnitTests/Services/Modules/GenericStrainerModuleBuilderTests.cs @@ -3,6 +3,8 @@ using Fluorite.Strainer.Models.Metadata; using Fluorite.Strainer.Services.Filtering; using Fluorite.Strainer.Services.Metadata; using Fluorite.Strainer.Services.Modules; +using System.Linq.Expressions; +using System.Reflection; namespace Fluorite.Strainer.UnitTests.Services.Modules; @@ -62,6 +64,117 @@ public class GenericStrainerModuleBuilderTests _propertyInfoProviderMock.ReceivedCalls().Should().BeEmpty(); } + [Fact] + public void Should_Add_ObjectMetadata() + { + // Arrange + var objectMetadata = new Dictionary(); + var strainerModuleMock = Substitute.For(); + strainerModuleMock.ObjectMetadata.Returns(objectMetadata); + var defaultSortingName = "foo"; + var strainerOptions = new StrainerOptions(); + var builder = new StrainerModuleBuilder( + _propertyInfoProviderMock, + strainerModuleMock, + strainerOptions); + Expression> expression = x => x.Id; + var propertyInfo = Substitute.For(); + + _propertyInfoProviderMock + .GetPropertyInfoAndFullName(expression) + .Returns((propertyInfo, defaultSortingName)); + + // Act + builder.AddObject(expression); + + // Assert + objectMetadata.Should().NotBeEmpty(); + objectMetadata.Should().HaveCount(1); + objectMetadata.Keys.First().Should().Be(); + objectMetadata.Values.First().Should().NotBeNull(); + objectMetadata.Values.First().DefaultSortingPropertyName.Should().Be(defaultSortingName); + _propertyInfoProviderMock + .Received(1) + .GetPropertyInfoAndFullName(expression); + _ = strainerModuleMock + .Received(1) + .ObjectMetadata; + } + + [Fact] + public void Should_Throw_WhenAddingPropertyMetadata_WhileFluentApiIsDisabled() + { + // Arrange + var strainerModuleMock = Substitute.For(); + var strainerOptions = new StrainerOptions + { + MetadataSourceType = MetadataSourceType.None, + }; + var builder = new StrainerModuleBuilder( + _propertyInfoProviderMock, + strainerModuleMock, + strainerOptions); + + // Act + Action act = () => builder.AddProperty(x => x.Id); + + // Assert + act.Should().Throw() + .WithMessage( + $"Current {nameof(MetadataSourceType)} setting does not " + + $"support {nameof(MetadataSourceType.FluentApi)}. " + + $"Include {nameof(MetadataSourceType.FluentApi)} option to " + + $"be able to use it."); + _propertyInfoProviderMock.ReceivedCalls().Should().BeEmpty(); + } + + [Fact] + public void Should_Add_PropertyMetadata() + { + // Arrange + var propertyMetadata = new Dictionary>(); + var defaultMetadata = new Dictionary(); + var strainerModuleMock = Substitute.For(); + strainerModuleMock.PropertyMetadata.Returns(propertyMetadata); + strainerModuleMock.DefaultMetadata.Returns(defaultMetadata); + var propertyName = "foo"; + var strainerOptions = new StrainerOptions(); + var builder = new StrainerModuleBuilder( + _propertyInfoProviderMock, + strainerModuleMock, + strainerOptions); + Expression> expression = x => x.Id; + var propertyInfo = Substitute.For(); + + _propertyInfoProviderMock + .GetPropertyInfoAndFullName(expression) + .Returns((propertyInfo, propertyName)); + + // Act + builder.AddProperty(expression); + + // Assert + propertyMetadata.Should().NotBeEmpty(); + propertyMetadata.Should().HaveCount(1); + propertyMetadata.Keys.First().Should().Be(); + propertyMetadata.Values.First().Should().NotBeNullOrEmpty(); + propertyMetadata.Values.First().Should().HaveCount(1); + propertyMetadata.Values.First().Keys.Should().BeEquivalentTo(propertyName); + propertyMetadata.Values.First().Values.First().Should().NotBeNull(); + propertyMetadata.Values.First().Values.First().Name.Should().Be(propertyName); + propertyMetadata.Values.First().Values.First().PropertyInfo.Should().BeSameAs(propertyInfo); + defaultMetadata.Should().BeEmpty(); + _propertyInfoProviderMock + .Received(1) + .GetPropertyInfoAndFullName(expression); + _ = strainerModuleMock + .Received(1) + .PropertyMetadata; + _ = strainerModuleMock + .Received(1) + .DefaultMetadata; + } + private class Stub { public int Id { get; set; } -- GitLab From eb6872e0053d896fa188f688f6faa2adbf354098 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Wed, 15 Jan 2025 01:34:09 +0100 Subject: [PATCH 28/56] Add unit tests for Strainer module builders --- .../GenericStrainerModuleBuilderTests.cs | 140 ++++++++ .../Modules/StrainerModuleBuilderTests.cs | 322 ++++++++++++++++++ 2 files changed, 462 insertions(+) create mode 100644 test/Strainer.UnitTests/Services/Modules/StrainerModuleBuilderTests.cs diff --git a/test/Strainer.UnitTests/Services/Modules/GenericStrainerModuleBuilderTests.cs b/test/Strainer.UnitTests/Services/Modules/GenericStrainerModuleBuilderTests.cs index b876bf90..3dd6a1d5 100644 --- a/test/Strainer.UnitTests/Services/Modules/GenericStrainerModuleBuilderTests.cs +++ b/test/Strainer.UnitTests/Services/Modules/GenericStrainerModuleBuilderTests.cs @@ -1,5 +1,8 @@ using Fluorite.Strainer.Models; +using Fluorite.Strainer.Models.Filtering; +using Fluorite.Strainer.Models.Filtering.Operators; using Fluorite.Strainer.Models.Metadata; +using Fluorite.Strainer.Models.Sorting; using Fluorite.Strainer.Services.Filtering; using Fluorite.Strainer.Services.Metadata; using Fluorite.Strainer.Services.Modules; @@ -175,6 +178,143 @@ public class GenericStrainerModuleBuilderTests .DefaultMetadata; } + [Fact] + public void Should_Throw_WhenAddingFilterOperator_AndFilterSymbolIsAlreadyTaken() + { + // Arrange + var symbol = "foo"; + var operatorName = "name"; + var expression = Expression.Empty(); + var sortOperator = Substitute.For(); + sortOperator.Symbol.Returns(symbol); + var sortOperators = new Dictionary() + { + [symbol] = Substitute.For(), + }; + var strainerModuleMock = Substitute.For(); + strainerModuleMock.FilterOperators.Returns(sortOperators); + var strainerOptions = new StrainerOptions(); + var builder = new StrainerModuleBuilder( + _propertyInfoProviderMock, + strainerModuleMock, + strainerOptions); + + // Act + Action act = () => builder.AddFilterOperator(x => x + .HasSymbol(symbol) + .HasName(operatorName) + .HasExpression(_ => expression) + .Build()); + + // Assert + act.Should().Throw() + .WithMessage( + $"There is an already existing operator with a symbol {sortOperator.Symbol}. " + + $"Please, choose a different symbol."); + _propertyInfoProviderMock.ReceivedCalls().Should().BeEmpty(); + } + + [Fact] + public void Should_Add_FilterOperator() + { + // Arrange + var symbol = "foo"; + var operatorName = "name"; + var expression = Expression.Empty(); + var sortOperator = Substitute.For(); + sortOperator.Symbol.Returns(symbol); + var sortOperators = new Dictionary(); + var strainerModuleMock = Substitute.For(); + strainerModuleMock.FilterOperators.Returns(sortOperators); + var strainerOptions = new StrainerOptions(); + var builder = new StrainerModuleBuilder( + _propertyInfoProviderMock, + strainerModuleMock, + strainerOptions); + + // Act + builder.AddFilterOperator(x => x + .HasSymbol(symbol) + .HasName(operatorName) + .HasExpression(_ => expression) + .Build()); + + // Assert + sortOperators.Should().NotBeEmpty(); + sortOperators.Should().HaveCount(1); + sortOperators.Keys.Should().BeEquivalentTo(symbol); + sortOperators.Values.First().Should().NotBeNull(); + sortOperators.Values.First().Symbol.Should().Be(symbol); + sortOperators.Values.First().Name.Should().Be(operatorName); + _propertyInfoProviderMock.ReceivedCalls().Should().BeEmpty(); + } + + [Fact] + public void Should_Add_CustomFilterMethod() + { + // Arrange + var name = "foo"; + Expression> expression = x => x.Id == 0; + var customFilterMethods = new Dictionary>(); + var strainerModuleMock = Substitute.For(); + strainerModuleMock.CustomFilterMethods.Returns(customFilterMethods); + var strainerOptions = new StrainerOptions(); + var builder = new StrainerModuleBuilder( + _propertyInfoProviderMock, + strainerModuleMock, + strainerOptions); + + // Act + builder.AddCustomFilterMethod(x => x.HasName(name).HasFunction(expression).Build()); + + // Assert + customFilterMethods.Should().NotBeEmpty(); + customFilterMethods.Should().HaveCount(1); + customFilterMethods.Keys.First().Should().Be(); + customFilterMethods.Values.Should().NotBeNullOrEmpty(); + customFilterMethods.Values.Should().HaveCount(1); + customFilterMethods.Values.First().Should().NotBeNull(); + customFilterMethods.Values.First().Keys.Should().BeEquivalentTo(name); + customFilterMethods.Values.First().Values.First().Should().NotBeNull(); + customFilterMethods.Values.First().Values.First().Name.Should().Be(name); + customFilterMethods.Values.First().Values.First().Should().BeAssignableTo>() + .Subject.Expression.Should().Be(expression); + _propertyInfoProviderMock.ReceivedCalls().Should().BeEmpty(); + } + + [Fact] + public void Should_Add_CustomSortMethod() + { + // Arrange + var name = "foo"; + Expression> expression = x => x.Id; + var customSortMethods = new Dictionary>(); + var strainerModuleMock = Substitute.For(); + strainerModuleMock.CustomSortMethods.Returns(customSortMethods); + var strainerOptions = new StrainerOptions(); + var builder = new StrainerModuleBuilder( + _propertyInfoProviderMock, + strainerModuleMock, + strainerOptions); + + // Act + builder.AddCustomSortMethod(x => x.HasName(name).HasFunction(expression).Build()); + + // Assert + customSortMethods.Should().NotBeEmpty(); + customSortMethods.Should().HaveCount(1); + customSortMethods.Keys.First().Should().Be(); + customSortMethods.Values.Should().NotBeNullOrEmpty(); + customSortMethods.Values.Should().HaveCount(1); + customSortMethods.Values.First().Should().NotBeNull(); + customSortMethods.Values.First().Keys.Should().BeEquivalentTo(name); + customSortMethods.Values.First().Values.First().Should().NotBeNull(); + customSortMethods.Values.First().Values.First().Name.Should().Be(name); + customSortMethods.Values.First().Values.First().Should().BeAssignableTo>() + .Subject.Expression.Should().Be(expression); + _propertyInfoProviderMock.ReceivedCalls().Should().BeEmpty(); + } + private class Stub { public int Id { get; set; } diff --git a/test/Strainer.UnitTests/Services/Modules/StrainerModuleBuilderTests.cs b/test/Strainer.UnitTests/Services/Modules/StrainerModuleBuilderTests.cs new file mode 100644 index 00000000..cddf9551 --- /dev/null +++ b/test/Strainer.UnitTests/Services/Modules/StrainerModuleBuilderTests.cs @@ -0,0 +1,322 @@ +using Fluorite.Strainer.Models; +using Fluorite.Strainer.Models.Filtering; +using Fluorite.Strainer.Models.Filtering.Operators; +using Fluorite.Strainer.Models.Metadata; +using Fluorite.Strainer.Models.Sorting; +using Fluorite.Strainer.Services.Filtering; +using Fluorite.Strainer.Services.Metadata; +using Fluorite.Strainer.Services.Modules; +using System.Linq.Expressions; +using System.Reflection; + +namespace Fluorite.Strainer.UnitTests.Services.Modules; + +public class StrainerModuleBuilderTests +{ + private readonly IPropertyInfoProvider _propertyInfoProviderMock = Substitute.For(); + + [Fact] + public void Should_Add_SymbolToExcludedListOfBuiltInFilterOperator() + { + // Arrange + var symbol = FilterOperatorSymbols.EqualsSymbol; + var builtInSymbols = new HashSet(); + var strainerModuleMock = Substitute.For(); + strainerModuleMock.ExcludedBuiltInFilterOperators.Returns(builtInSymbols); + var strainerOptions = new StrainerOptions(); + var builder = new StrainerModuleBuilder( + _propertyInfoProviderMock, + strainerModuleMock, + strainerOptions); + + // Act + builder.RemoveBuiltInFilterOperator(FilterOperatorSymbols.EqualsSymbol); + + // Assert + _propertyInfoProviderMock.ReceivedCalls().Should().BeEmpty(); + builtInSymbols.Should().BeEquivalentTo(symbol); + _ = strainerModuleMock + .Received(1) + .ExcludedBuiltInFilterOperators; + } + + [Fact] + public void Should_Throw_WhenAddingObjectMetadata_WhileFluentApiIsDisabled() + { + // Arrange + var strainerModuleMock = Substitute.For(); + var strainerOptions = new StrainerOptions + { + MetadataSourceType = MetadataSourceType.None, + }; + var builder = new StrainerModuleBuilder( + _propertyInfoProviderMock, + strainerModuleMock, + strainerOptions); + + // Act + Action act = () => builder.AddObject(x => x.Id); + + // Assert + act.Should().Throw() + .WithMessage( + $"Current {nameof(MetadataSourceType)} setting does not " + + $"support {nameof(MetadataSourceType.FluentApi)}. " + + $"Include {nameof(MetadataSourceType.FluentApi)} option to " + + $"be able to use it."); + _propertyInfoProviderMock.ReceivedCalls().Should().BeEmpty(); + } + + [Fact] + public void Should_Add_ObjectMetadata() + { + // Arrange + var objectMetadata = new Dictionary(); + var strainerModuleMock = Substitute.For(); + strainerModuleMock.ObjectMetadata.Returns(objectMetadata); + var defaultSortingName = "foo"; + var strainerOptions = new StrainerOptions(); + var builder = new StrainerModuleBuilder( + _propertyInfoProviderMock, + strainerModuleMock, + strainerOptions); + Expression> expression = x => x.Id; + var propertyInfo = Substitute.For(); + + _propertyInfoProviderMock + .GetPropertyInfoAndFullName(expression) + .Returns((propertyInfo, defaultSortingName)); + + // Act + builder.AddObject(expression); + + // Assert + objectMetadata.Should().NotBeEmpty(); + objectMetadata.Should().HaveCount(1); + objectMetadata.Keys.First().Should().Be(); + objectMetadata.Values.First().Should().NotBeNull(); + objectMetadata.Values.First().DefaultSortingPropertyName.Should().Be(defaultSortingName); + _propertyInfoProviderMock + .Received(1) + .GetPropertyInfoAndFullName(expression); + _ = strainerModuleMock + .Received(1) + .ObjectMetadata; + } + + [Fact] + public void Should_Throw_WhenAddingPropertyMetadata_WhileFluentApiIsDisabled() + { + // Arrange + var strainerModuleMock = Substitute.For(); + var strainerOptions = new StrainerOptions + { + MetadataSourceType = MetadataSourceType.None, + }; + var builder = new StrainerModuleBuilder( + _propertyInfoProviderMock, + strainerModuleMock, + strainerOptions); + + // Act + Action act = () => builder.AddProperty(x => x.Id); + + // Assert + act.Should().Throw() + .WithMessage( + $"Current {nameof(MetadataSourceType)} setting does not " + + $"support {nameof(MetadataSourceType.FluentApi)}. " + + $"Include {nameof(MetadataSourceType.FluentApi)} option to " + + $"be able to use it."); + _propertyInfoProviderMock.ReceivedCalls().Should().BeEmpty(); + } + + [Fact] + public void Should_Add_PropertyMetadata() + { + // Arrange + var propertyMetadata = new Dictionary>(); + var defaultMetadata = new Dictionary(); + var strainerModuleMock = Substitute.For(); + strainerModuleMock.PropertyMetadata.Returns(propertyMetadata); + strainerModuleMock.DefaultMetadata.Returns(defaultMetadata); + var propertyName = "foo"; + var strainerOptions = new StrainerOptions(); + var builder = new StrainerModuleBuilder( + _propertyInfoProviderMock, + strainerModuleMock, + strainerOptions); + Expression> expression = x => x.Id; + var propertyInfo = Substitute.For(); + + _propertyInfoProviderMock + .GetPropertyInfoAndFullName(expression) + .Returns((propertyInfo, propertyName)); + + // Act + builder.AddProperty(expression); + + // Assert + propertyMetadata.Should().NotBeEmpty(); + propertyMetadata.Should().HaveCount(1); + propertyMetadata.Keys.First().Should().Be(); + propertyMetadata.Values.First().Should().NotBeNullOrEmpty(); + propertyMetadata.Values.First().Should().HaveCount(1); + propertyMetadata.Values.First().Keys.Should().BeEquivalentTo(propertyName); + propertyMetadata.Values.First().Values.First().Should().NotBeNull(); + propertyMetadata.Values.First().Values.First().Name.Should().Be(propertyName); + propertyMetadata.Values.First().Values.First().PropertyInfo.Should().BeSameAs(propertyInfo); + defaultMetadata.Should().BeEmpty(); + _propertyInfoProviderMock + .Received(1) + .GetPropertyInfoAndFullName(expression); + _ = strainerModuleMock + .Received(3) + .PropertyMetadata; + _ = strainerModuleMock + .Received(1) + .DefaultMetadata; + } + + [Fact] + public void Should_Throw_WhenAddingFilterOperator_AndFilterSymbolIsAlreadyTaken() + { + // Arrange + var symbol = "foo"; + var operatorName = "name"; + var expression = Expression.Empty(); + var sortOperator = Substitute.For(); + sortOperator.Symbol.Returns(symbol); + var sortOperators = new Dictionary() + { + [symbol] = Substitute.For(), + }; + var strainerModuleMock = Substitute.For(); + strainerModuleMock.FilterOperators.Returns(sortOperators); + var strainerOptions = new StrainerOptions(); + var builder = new StrainerModuleBuilder( + _propertyInfoProviderMock, + strainerModuleMock, + strainerOptions); + + // Act + Action act = () => builder.AddFilterOperator(x => x + .HasSymbol(symbol) + .HasName(operatorName) + .HasExpression(_ => expression) + .Build()); + + // Assert + act.Should().Throw() + .WithMessage( + $"There is an already existing operator with a symbol {sortOperator.Symbol}. " + + $"Please, choose a different symbol."); + _propertyInfoProviderMock.ReceivedCalls().Should().BeEmpty(); + } + + [Fact] + public void Should_Add_FilterOperator() + { + // Arrange + var symbol = "foo"; + var operatorName = "name"; + var expression = Expression.Empty(); + var sortOperator = Substitute.For(); + sortOperator.Symbol.Returns(symbol); + var sortOperators = new Dictionary(); + var strainerModuleMock = Substitute.For(); + strainerModuleMock.FilterOperators.Returns(sortOperators); + var strainerOptions = new StrainerOptions(); + var builder = new StrainerModuleBuilder( + _propertyInfoProviderMock, + strainerModuleMock, + strainerOptions); + + // Act + builder.AddFilterOperator(x => x + .HasSymbol(symbol) + .HasName(operatorName) + .HasExpression(_ => expression) + .Build()); + + // Assert + sortOperators.Should().NotBeEmpty(); + sortOperators.Should().HaveCount(1); + sortOperators.Keys.Should().BeEquivalentTo(symbol); + sortOperators.Values.First().Should().NotBeNull(); + sortOperators.Values.First().Symbol.Should().Be(symbol); + sortOperators.Values.First().Name.Should().Be(operatorName); + _propertyInfoProviderMock.ReceivedCalls().Should().BeEmpty(); + } + + [Fact] + public void Should_Add_CustomFilterMethod() + { + // Arrange + var name = "foo"; + Expression> expression = x => x.Id == 0; + var customFilterMethods = new Dictionary>(); + var strainerModuleMock = Substitute.For(); + strainerModuleMock.CustomFilterMethods.Returns(customFilterMethods); + var strainerOptions = new StrainerOptions(); + var builder = new StrainerModuleBuilder( + _propertyInfoProviderMock, + strainerModuleMock, + strainerOptions); + + // Act + builder.AddCustomFilterMethod(x => x.HasName(name).HasFunction(expression).Build()); + + // Assert + customFilterMethods.Should().NotBeEmpty(); + customFilterMethods.Should().HaveCount(1); + customFilterMethods.Keys.First().Should().Be(); + customFilterMethods.Values.Should().NotBeNullOrEmpty(); + customFilterMethods.Values.Should().HaveCount(1); + customFilterMethods.Values.First().Should().NotBeNull(); + customFilterMethods.Values.First().Keys.Should().BeEquivalentTo(name); + customFilterMethods.Values.First().Values.First().Should().NotBeNull(); + customFilterMethods.Values.First().Values.First().Name.Should().Be(name); + customFilterMethods.Values.First().Values.First().Should().BeAssignableTo>() + .Subject.Expression.Should().Be(expression); + _propertyInfoProviderMock.ReceivedCalls().Should().BeEmpty(); + } + + [Fact] + public void Should_Add_CustomSortMethod() + { + // Arrange + var name = "foo"; + Expression> expression = x => x.Id; + var customSortMethods = new Dictionary>(); + var strainerModuleMock = Substitute.For(); + strainerModuleMock.CustomSortMethods.Returns(customSortMethods); + var strainerOptions = new StrainerOptions(); + var builder = new StrainerModuleBuilder( + _propertyInfoProviderMock, + strainerModuleMock, + strainerOptions); + + // Act + builder.AddCustomSortMethod(x => x.HasName(name).HasFunction(expression).Build()); + + // Assert + customSortMethods.Should().NotBeEmpty(); + customSortMethods.Should().HaveCount(1); + customSortMethods.Keys.First().Should().Be(); + customSortMethods.Values.Should().NotBeNullOrEmpty(); + customSortMethods.Values.Should().HaveCount(1); + customSortMethods.Values.First().Should().NotBeNull(); + customSortMethods.Values.First().Keys.Should().BeEquivalentTo(name); + customSortMethods.Values.First().Values.First().Should().NotBeNull(); + customSortMethods.Values.First().Values.First().Name.Should().Be(name); + customSortMethods.Values.First().Values.First().Should().BeAssignableTo>() + .Subject.Expression.Should().Be(expression); + _propertyInfoProviderMock.ReceivedCalls().Should().BeEmpty(); + } + + private class Stub + { + public int Id { get; set; } + } +} -- GitLab From b850b9ba08d7051f059318da614af49400edb8b2 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Wed, 15 Jan 2025 21:09:01 +0100 Subject: [PATCH 29/56] Disable StyleCope rule about static arrays --- StyleCop.ruleset | 1 + 1 file changed, 1 insertion(+) diff --git a/StyleCop.ruleset b/StyleCop.ruleset index 792caf94..19d66e56 100644 --- a/StyleCop.ruleset +++ b/StyleCop.ruleset @@ -574,6 +574,7 @@ + -- GitLab From 44a78f38fa6bde1fbcea861e44f2e6d6c5711d29 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Wed, 15 Jan 2025 21:09:30 +0100 Subject: [PATCH 30/56] Add missing test case for joining filtering expressions from multiple terms --- .../Pipelines/FilterPipelineOperationTests.cs | 65 ++++++++++++++++++- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/test/Strainer.UnitTests/Services/Pipelines/FilterPipelineOperationTests.cs b/test/Strainer.UnitTests/Services/Pipelines/FilterPipelineOperationTests.cs index 3af0419c..12950d37 100644 --- a/test/Strainer.UnitTests/Services/Pipelines/FilterPipelineOperationTests.cs +++ b/test/Strainer.UnitTests/Services/Pipelines/FilterPipelineOperationTests.cs @@ -102,7 +102,7 @@ public class FilterPipelineOperationTests { // Arrange var source = new[] { "foo" }.AsQueryable(); - var sourceClone = ((string[])new[] { "foo" }.Clone()).AsQueryable(); + var sourceClone = source.ToArray(); var model = new StrainerModel { Filters = "input", @@ -226,7 +226,7 @@ public class FilterPipelineOperationTests { // Arrange var source = new[] { "foo" }.AsQueryable(); - var sourceClone = ((string[])new[] { "foo" }.Clone()).AsQueryable(); + var sourceClone = source.ToArray(); var model = new StrainerModel { Filters = "input", @@ -275,7 +275,7 @@ public class FilterPipelineOperationTests { // Arrange var source = new[] { "foo", "boat", "ID" }.AsQueryable(); - var sourceClone = ((string[])new[] { "foo" }.Clone()).AsQueryable(); + var sourceClone = source.ToArray(); var model = new StrainerModel { Filters = "input", @@ -326,6 +326,65 @@ public class FilterPipelineOperationTests _filterExpressionProvider.Received(1).GetExpression(metadata2, filterTerm, Arg.Any(), Arg.Is(x => x != null)); } + [Fact] + public void Should_Return_CollectionWithMultipleFilteringsApplied_FromTheMultipleTerms() + { + // Arrange + var source = new[] { "foo", "boat", "ID" }.AsQueryable(); + var sourceClone = source.ToArray(); + var model = new StrainerModel + { + Filters = "input1|input2", + }; + var filterTermName1 = "name"; + var filterTermName2 = "second-name"; + var filterTerm1 = Substitute.For(); + filterTerm1.Names.Returns([filterTermName1]); + var filterTerm2 = Substitute.For(); + filterTerm2.Names.Returns([filterTermName2]); + var terms = new List + { + filterTerm1, + filterTerm2, + }; + var metadata1 = Substitute.For(); + var metadata2 = Substitute.For(); + + _strainerOptionsProvider + .GetStrainerOptions() + .Returns(new StrainerOptions()); + _filterTermParser + .GetParsedTerms(model.Filters) + .Returns(terms); + _metadataFacade + .GetMetadata(isSortableRequired: false, isFilterableRequired: true, filterTermName1) + .Returns(metadata1); + _metadataFacade + .GetMetadata(isSortableRequired: false, isFilterableRequired: true, filterTermName2) + .Returns(metadata2); + _filterExpressionProvider + .GetExpression(metadata1, filterTerm1, Arg.Any(), Arg.Any()) + .Returns(x => CreateStringGreaterThanLengthExpression(x[2] as ParameterExpression, length: 2)); + _filterExpressionProvider + .GetExpression(metadata2, filterTerm2, Arg.Any(), Arg.Any()) + .Returns(x => CreateStringGreaterThanLengthExpression(x[2] as ParameterExpression, length: 3)); + + // Act + var result = _operation.Execute(model, source); + + // Assert + result.Should().NotBeSameAs(sourceClone); + result.Should().NotBeNull(); + result.Should().BeEquivalentTo("boat"); + + _strainerOptionsProvider.Received(1).GetStrainerOptions(); + _filterTermParser.Received(1).GetParsedTerms(model.Filters); + _metadataFacade.Received(1).GetMetadata(false, true, filterTermName1); + _metadataFacade.Received(1).GetMetadata(false, true, filterTermName2); + _filterExpressionProvider.Received(1).GetExpression(metadata1, filterTerm1, Arg.Any(), Arg.Is(y => y == null)); + _filterExpressionProvider.Received(1).GetExpression(metadata2, filterTerm2, Arg.Any(), Arg.Any()); + } + private Expression CreateStringGreaterThanLengthExpression(ParameterExpression parameterExpression, int length) { return Expression.GreaterThan( -- GitLab From 4fc21fb326101e4e35a2e180b7bba6551e27dbfa Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Wed, 15 Jan 2025 21:22:01 +0100 Subject: [PATCH 31/56] Do not catch exceptions in processer, pipeline already does that --- src/Strainer/Services/StrainerProcessor.cs | 37 ++------ .../Services/StrainerProcessorTests.cs | 86 +------------------ 2 files changed, 8 insertions(+), 115 deletions(-) diff --git a/src/Strainer/Services/StrainerProcessor.cs b/src/Strainer/Services/StrainerProcessor.cs index 442a2dd6..80e5bd3f 100644 --- a/src/Strainer/Services/StrainerProcessor.cs +++ b/src/Strainer/Services/StrainerProcessor.cs @@ -1,5 +1,4 @@ -using Fluorite.Strainer.Exceptions; -using Fluorite.Strainer.Models; +using Fluorite.Strainer.Models; using Fluorite.Strainer.Services.Pipelines; namespace Fluorite.Strainer.Services; @@ -11,26 +10,19 @@ namespace Fluorite.Strainer.Services; public class StrainerProcessor : IStrainerProcessor { private readonly IStrainerPipelineBuilderFactory _strainerPipelineBuilderFactory; - private readonly IStrainerOptionsProvider _strainerOptionsProvider; /// /// Initializes a new instance of the class /// with specified context. /// /// - /// /// /// is . /// - /// - /// is . - /// public StrainerProcessor( - IStrainerPipelineBuilderFactory strainerPipelineBuilderFactory, - IStrainerOptionsProvider strainerOptionsProvider) + IStrainerPipelineBuilderFactory strainerPipelineBuilderFactory) { _strainerPipelineBuilderFactory = Guard.Against.Null(strainerPipelineBuilderFactory); - _strainerOptionsProvider = Guard.Against.Null(strainerOptionsProvider); } /// @@ -69,7 +61,7 @@ public class StrainerProcessor : IStrainerProcessor var pipeline = builder.Build(); - return RunPipeline(model, source, pipeline); + return pipeline.Run(model, source); } /// @@ -91,7 +83,7 @@ public class StrainerProcessor : IStrainerProcessor .Filter() .Build(); - return RunPipeline(model, source, pipeline); + return pipeline.Run(model, source); } /// @@ -113,7 +105,7 @@ public class StrainerProcessor : IStrainerProcessor .Paginate() .Build(); - return RunPipeline(model, source, pipeline); + return pipeline.Run(model, source); } /// @@ -135,23 +127,6 @@ public class StrainerProcessor : IStrainerProcessor .Sort() .Build(); - return RunPipeline(model, source, pipeline); - } - - private IQueryable RunPipeline( - IStrainerModel model, - IQueryable source, - IStrainerPipeline pipeline) - { - var options = _strainerOptionsProvider.GetStrainerOptions(); - - try - { - return pipeline.Run(model, source); - } - catch (StrainerException) when (!options.ThrowExceptions) - { - return source; - } + return pipeline.Run(model, source); } } diff --git a/test/Strainer.UnitTests/Services/StrainerProcessorTests.cs b/test/Strainer.UnitTests/Services/StrainerProcessorTests.cs index a3664cbd..fdbcac83 100644 --- a/test/Strainer.UnitTests/Services/StrainerProcessorTests.cs +++ b/test/Strainer.UnitTests/Services/StrainerProcessorTests.cs @@ -1,23 +1,19 @@ -using Fluorite.Strainer.Exceptions; -using Fluorite.Strainer.Models; +using Fluorite.Strainer.Models; using Fluorite.Strainer.Services; using Fluorite.Strainer.Services.Pipelines; -using NSubstitute.ExceptionExtensions; namespace Fluorite.Strainer.UnitTests.Services; public class StrainerProcessorTests { private readonly IStrainerPipelineBuilderFactory _strainerPipelineBuilderFactoryMock = Substitute.For(); - private readonly IStrainerOptionsProvider _strainerOptionsProviderMock = Substitute.For(); private readonly StrainerProcessor _processor; public StrainerProcessorTests() { _processor = new StrainerProcessor( - _strainerPipelineBuilderFactoryMock, - _strainerOptionsProviderMock); + _strainerPipelineBuilderFactoryMock); } [Fact] @@ -31,9 +27,6 @@ public class StrainerProcessorTests var pipelineBuilderMock = Substitute.For(); var pipelineMock = Substitute.For(); - _strainerOptionsProviderMock - .GetStrainerOptions() - .Returns(options); _strainerPipelineBuilderFactoryMock .CreateBuilder() .Returns(pipelineBuilderMock); @@ -60,72 +53,6 @@ public class StrainerProcessorTests }); } - [Fact] - public void Processor_DoesNotThrow_DuringProcessing_WhenExceptionThrowingIsDisabled() - { - // Arrange - var model = new StrainerModel(); - var options = new StrainerOptions - { - ThrowExceptions = false, - }; - var source = GetSourceQueryable(); - var pipelineBuilderMock = Substitute.For(); - var pipelineMock = Substitute.For(); - - _strainerOptionsProviderMock - .GetStrainerOptions() - .Returns(options); - _strainerPipelineBuilderFactoryMock - .CreateBuilder() - .Returns(pipelineBuilderMock); - pipelineBuilderMock - .Build() - .Returns(pipelineMock); - pipelineMock - .Run(model, source) - .Throws(); - - // Act - var result = _processor.Apply(model, source); - - // Assert - result.Should().BeSameAs(source); - } - - [Fact] - public void Processor_Throws_DuringProcessing_WhenExceptionThrowingIsEnabled() - { - // Arrange - var model = new StrainerModel(); - var options = new StrainerOptions - { - ThrowExceptions = true, - }; - var source = GetSourceQueryable(); - var pipelineBuilderMock = Substitute.For(); - var pipelineMock = Substitute.For(); - - _strainerOptionsProviderMock - .GetStrainerOptions() - .Returns(options); - _strainerPipelineBuilderFactoryMock - .CreateBuilder() - .Returns(pipelineBuilderMock); - pipelineBuilderMock - .Build() - .Returns(pipelineMock); - pipelineMock - .Run(model, source) - .Throws(); - - // Act - Action act = () => _processor.Apply(model, source); - - // Assert - act.Should().ThrowExactly(); - } - [Fact] public void Processor_Applies_JustFiltering() { @@ -137,9 +64,6 @@ public class StrainerProcessorTests var pipelineBuilderMock = Substitute.For(); var pipelineMock = Substitute.For(); - _strainerOptionsProviderMock - .GetStrainerOptions() - .Returns(options); _strainerPipelineBuilderFactoryMock .CreateBuilder() .Returns(pipelineBuilderMock); @@ -177,9 +101,6 @@ public class StrainerProcessorTests var pipelineBuilderMock = Substitute.For(); var pipelineMock = Substitute.For(); - _strainerOptionsProviderMock - .GetStrainerOptions() - .Returns(options); _strainerPipelineBuilderFactoryMock .CreateBuilder() .Returns(pipelineBuilderMock); @@ -217,9 +138,6 @@ public class StrainerProcessorTests var pipelineBuilderMock = Substitute.For(); var pipelineMock = Substitute.For(); - _strainerOptionsProviderMock - .GetStrainerOptions() - .Returns(options); _strainerPipelineBuilderFactoryMock .CreateBuilder() .Returns(pipelineBuilderMock); -- GitLab From 0ed6927f65a786a3a497806f8df732142c9d3e21 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Wed, 15 Jan 2025 22:19:42 +0100 Subject: [PATCH 32/56] Add missing test cases for property info provider --- .../Metadata/PropertyInfoProviderTests.cs | 46 +++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/test/Strainer.UnitTests/Services/Metadata/PropertyInfoProviderTests.cs b/test/Strainer.UnitTests/Services/Metadata/PropertyInfoProviderTests.cs index cc9cb691..d054c4e0 100644 --- a/test/Strainer.UnitTests/Services/Metadata/PropertyInfoProviderTests.cs +++ b/test/Strainer.UnitTests/Services/Metadata/PropertyInfoProviderTests.cs @@ -1,12 +1,31 @@ using Fluorite.Strainer.Services.Metadata; using System.Linq.Expressions; +using System.Reflection; namespace Fluorite.Strainer.UnitTests.Services.Metadata; public class PropertyInfoProviderTests { [Fact] - public void Provider_Works_For_Property() + public void Provider_Returns_PropertyInfo() + { + // Arrange + var name = "foo"; + var propertyInfoMock = Substitute.For(); + var typeMock = Substitute.For(); + typeMock.GetProperty(name, BindingFlags.Instance | BindingFlags.Public).Returns(propertyInfoMock); + var provider = new PropertyInfoProvider(); + + // Act + var result = provider.GetPropertyInfo(typeMock, name); + + // Assert + result.Should().NotBeNull(); + result.Should().BeSameAs(propertyInfoMock); + } + + [Fact] + public void Provider_Returns_PropertyInfoAndFullName() { // Arrange Expression> expression = s => s.Property; @@ -22,7 +41,7 @@ public class PropertyInfoProviderTests } [Fact] - public void Provider_Works_For_Property_With_Only_Getter() + public void Provider_Returns_PropertyInfoAndFullName_With_Only_Getter() { // Arrange Expression> expression = s => s.PropertyOnlyGetter; @@ -38,7 +57,7 @@ public class PropertyInfoProviderTests } [Fact] - public void Provider_Does_Not_Work_For_Field() + public void Provider_Throws_For_Fields() { // Arrange Expression> expression = (Stub v) => v.Field; @@ -50,7 +69,7 @@ public class PropertyInfoProviderTests } [Fact] - public void Provider_Does_Not_Work_For_Method() + public void Provider_Throws_For_Methods() { // Arrange Expression> expression = (Stub v) => v.Method(); @@ -61,6 +80,25 @@ public class PropertyInfoProviderTests result.Should().ThrowExactly(); } + [Fact] + public void Provider_Returns_PropertyInfos() + { + // Arrange + var propertyInfoMock = Substitute.For(); + var propertyInfos = new[] { propertyInfoMock }; + var typeMock = Substitute.For(); + typeMock.GetProperties(BindingFlags.Instance | BindingFlags.Public).Returns(propertyInfos); + var provider = new PropertyInfoProvider(); + + // Act + var result = provider.GetPropertyInfos(typeMock); + + // Assert + result.Should().NotBeNullOrEmpty(); + result.Should().HaveSameCount(propertyInfos); + result.Should().BeSameAs(propertyInfos); + } + private class Stub { #pragma warning disable SA1401 // Fields should be private -- GitLab From 050b5be17a7f07bc57ce754905dc45c7ec44e482 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Wed, 15 Jan 2025 22:39:04 +0100 Subject: [PATCH 33/56] Remove MetadataMapper and related unit tests --- .../StrainerServiceCollectionExtensions.cs | 1 - .../Services/Metadata/IMetadataMapper.cs | 21 -- .../Services/Metadata/MetadataMapper.cs | 117 ---------- .../Services/Metadata/MetadataMapperTests.cs | 210 ------------------ 4 files changed, 349 deletions(-) delete mode 100644 src/Strainer/Services/Metadata/IMetadataMapper.cs delete mode 100644 src/Strainer/Services/Metadata/MetadataMapper.cs delete mode 100644 test/Strainer.UnitTests/Services/Metadata/MetadataMapperTests.cs diff --git a/src/Strainer.AspNetCore/Extensions/DependencyInjection/StrainerServiceCollectionExtensions.cs b/src/Strainer.AspNetCore/Extensions/DependencyInjection/StrainerServiceCollectionExtensions.cs index 5ad505c6..c3c300d8 100644 --- a/src/Strainer.AspNetCore/Extensions/DependencyInjection/StrainerServiceCollectionExtensions.cs +++ b/src/Strainer.AspNetCore/Extensions/DependencyInjection/StrainerServiceCollectionExtensions.cs @@ -508,7 +508,6 @@ public static class StrainerServiceCollectionExtensions services.TryAdd(serviceLifetime); services.TryAdd(serviceLifetime); - services.TryAdd(serviceLifetime); services.TryAdd(serviceLifetime); services.TryAdd(serviceLifetime); diff --git a/src/Strainer/Services/Metadata/IMetadataMapper.cs b/src/Strainer/Services/Metadata/IMetadataMapper.cs deleted file mode 100644 index e6cae92a..00000000 --- a/src/Strainer/Services/Metadata/IMetadataMapper.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Fluorite.Strainer.Models.Metadata; -using System.Linq.Expressions; - -namespace Fluorite.Strainer.Services.Metadata; - -public interface IMetadataMapper -{ - IDictionary DefaultMetadata { get; } - - IDictionary> PropertyMetadata { get; } - - IDictionary ObjectMetadata { get; } - - void AddObjectMetadata(IObjectMetadata objectMetadata); - - void AddPropertyMetadata(IPropertyMetadata propertyMetadata); - - IObjectMetadataBuilder Object(Expression> defaultSortingPropertyExpression); - - IPropertyMetadataBuilder Property(Expression> propertyExpression); -} diff --git a/src/Strainer/Services/Metadata/MetadataMapper.cs b/src/Strainer/Services/Metadata/MetadataMapper.cs deleted file mode 100644 index 3f85ccd5..00000000 --- a/src/Strainer/Services/Metadata/MetadataMapper.cs +++ /dev/null @@ -1,117 +0,0 @@ -using Fluorite.Strainer.Models.Metadata; -using System.Linq.Expressions; - -namespace Fluorite.Strainer.Services.Metadata; - -public class MetadataMapper : IMetadataMapper -{ - private readonly IStrainerOptionsProvider _strainerOptionsProvider; - private readonly IPropertyInfoProvider _propertyInfoProvider; - - public MetadataMapper( - IStrainerOptionsProvider strainerOptionsProvider, - IPropertyInfoProvider propertyInfoProvider) - { - DefaultMetadata = new Dictionary(); - PropertyMetadata = new Dictionary>(); - ObjectMetadata = new Dictionary(); - _strainerOptionsProvider = Guard.Against.Null(strainerOptionsProvider); - _propertyInfoProvider = Guard.Against.Null(propertyInfoProvider); - } - - public IDictionary DefaultMetadata { get; } - - public IDictionary> PropertyMetadata { get; } - - public IDictionary ObjectMetadata { get; } - - public void AddObjectMetadata(IObjectMetadata objectMetadata) - { - Guard.Against.Null(objectMetadata); - - var options = _strainerOptionsProvider.GetStrainerOptions(); - if (!options.MetadataSourceType.HasFlag(MetadataSourceType.FluentApi)) - { - throw new InvalidOperationException( - $"Current {nameof(MetadataSourceType)} setting does not " + - $"allow support {nameof(MetadataSourceType.FluentApi)}. " + - $"Include {nameof(MetadataSourceType.FluentApi)} option to " + - $"be able to use it."); - } - - ObjectMetadata[typeof(TEntity)] = objectMetadata; - } - - public void AddPropertyMetadata(IPropertyMetadata propertyMetadata) - { - Guard.Against.Null(propertyMetadata); - - var options = _strainerOptionsProvider.GetStrainerOptions(); - if (!options.MetadataSourceType.HasFlag(MetadataSourceType.FluentApi)) - { - throw new InvalidOperationException( - $"Current {nameof(MetadataSourceType)} setting does not " + - $"allow support {nameof(MetadataSourceType.FluentApi)}. " + - $"Include {nameof(MetadataSourceType.FluentApi)} option to " + - $"be able to use it."); - } - - if (!PropertyMetadata.ContainsKey(typeof(TEntity))) - { - PropertyMetadata[typeof(TEntity)] = new Dictionary(); - } - - if (propertyMetadata.IsDefaultSorting) - { - DefaultMetadata[typeof(TEntity)] = propertyMetadata; - } - - var metadataKey = propertyMetadata.DisplayName ?? propertyMetadata.Name; - - PropertyMetadata[typeof(TEntity)][metadataKey] = propertyMetadata; - } - - public IObjectMetadataBuilder Object(Expression> defaultSortingPropertyExpression) - { - Guard.Against.Null(defaultSortingPropertyExpression); - - var options = _strainerOptionsProvider.GetStrainerOptions(); - if (!options.MetadataSourceType.HasFlag(MetadataSourceType.FluentApi)) - { - throw new InvalidOperationException( - $"Current {nameof(MetadataSourceType)} setting does not " + - $"allow support {nameof(MetadataSourceType.FluentApi)}. " + - $"Include {nameof(MetadataSourceType.FluentApi)} option to " + - $"be able to use it."); - } - - return new ObjectMetadataBuilder( - _propertyInfoProvider, - ObjectMetadata, - defaultSortingPropertyExpression); - } - - public IPropertyMetadataBuilder Property(Expression> propertyExpression) - { - Guard.Against.Null(propertyExpression); - - var options = _strainerOptionsProvider.GetStrainerOptions(); - if (!options.MetadataSourceType.HasFlag(MetadataSourceType.FluentApi)) - { - throw new InvalidOperationException( - $"Current {nameof(MetadataSourceType)} setting does not " + - $"allow support {nameof(MetadataSourceType.FluentApi)}. " + - $"Include {nameof(MetadataSourceType.FluentApi)} option to " + - $"be able to use it."); - } - - if (!PropertyMetadata.ContainsKey(typeof(TEntity))) - { - PropertyMetadata[typeof(TEntity)] = new Dictionary(); - } - - var (propertyInfo, fullName) = _propertyInfoProvider.GetPropertyInfoAndFullName(propertyExpression); - - return new PropertyMetadataBuilder(PropertyMetadata, DefaultMetadata, propertyInfo, fullName); - } -} diff --git a/test/Strainer.UnitTests/Services/Metadata/MetadataMapperTests.cs b/test/Strainer.UnitTests/Services/Metadata/MetadataMapperTests.cs deleted file mode 100644 index 19804782..00000000 --- a/test/Strainer.UnitTests/Services/Metadata/MetadataMapperTests.cs +++ /dev/null @@ -1,210 +0,0 @@ -using Fluorite.Strainer.Models; -using Fluorite.Strainer.Models.Metadata; -using Fluorite.Strainer.Services; -using Fluorite.Strainer.Services.Metadata; -using System.Linq.Expressions; - -namespace Fluorite.Strainer.UnitTests.Services.Metadata; - -public class MetadataMapperTests -{ - [Fact] - public void AddPropertyMetadata_Adds_PropertyMetadata_Via_AddPropertyMetadata() - { - // Arrange - var optionsProvider = Substitute.For(); - optionsProvider - .GetStrainerOptions() - .Returns(new StrainerOptions()); - var propertyInfoProviderMock = Substitute.For(); - var mapper = new MetadataMapper(optionsProvider, propertyInfoProviderMock); - var propertyInfo = typeof(Post).GetProperty(nameof(Post.Id)); - var metadata = new PropertyMetadata(propertyInfo.Name, propertyInfo); - - // Act - mapper.AddPropertyMetadata(metadata); - - // Assert - mapper.PropertyMetadata.Should().HaveCount(1); - mapper.PropertyMetadata.First().Value.Should().HaveCount(1); - mapper.PropertyMetadata.First().Value.First().Value.Name.Should().Be(nameof(Post.Id)); - mapper.PropertyMetadata.First().Value.First().Value.PropertyInfo.Should().BeSameAs(typeof(Post).GetProperty(metadata.Name)); - } - - [Fact] - public void AddPropertyMetadata_Adds_Different_PropertyMetadata_For_Already_Existing_PropertyMetadata() - { - // Arrange - var optionsProvider = Substitute.For(); - optionsProvider - .GetStrainerOptions() - .Returns(new StrainerOptions()); - var propertyInfoProviderMock = Substitute.For(); - var mapper = new MetadataMapper(optionsProvider, propertyInfoProviderMock); - var propertyInfo = typeof(Post).GetProperty(nameof(Post.Id)); - var firstMetadata = new PropertyMetadata("first", propertyInfo); - var secondMetadata = new PropertyMetadata("second", propertyInfo) - { - IsFilterable = true, - }; - - // Act - mapper.AddPropertyMetadata(firstMetadata); - mapper.AddPropertyMetadata(secondMetadata); - - // Assert - mapper.PropertyMetadata.Should().HaveCount(1); - mapper.PropertyMetadata.First().Value.Should().HaveCount(2); - mapper.PropertyMetadata.First().Value.First().Value.Name.Should().Be(firstMetadata.Name); - mapper.PropertyMetadata.First().Value.First().Value.PropertyInfo.Should().BeSameAs(firstMetadata.PropertyInfo); - mapper.PropertyMetadata.First().Value.Last().Value.Name.Should().Be(secondMetadata.Name); - mapper.PropertyMetadata.First().Value.Last().Value.PropertyInfo.Should().BeSameAs(secondMetadata.PropertyInfo); - } - - [Fact] - public void AddPropertyMetadata_Adds_AlreadyExistingPropertyMetadata_Via_AddPropertyMetadata_Without_Duplicating() - { - // Arrange - var optionsProvider = Substitute.For(); - optionsProvider - .GetStrainerOptions() - .Returns(new StrainerOptions()); - var propertyInfoProviderMock = Substitute.For(); - var mapper = new MetadataMapper(optionsProvider, propertyInfoProviderMock); - var propertyInfo = typeof(Post).GetProperty(nameof(Post.Id)); - var metadata = new PropertyMetadata(propertyInfo.Name, propertyInfo); - - // Act - mapper.AddPropertyMetadata(metadata); - mapper.AddPropertyMetadata(metadata); - - // Assert - mapper.PropertyMetadata.Should().HaveCount(1); - mapper.PropertyMetadata.First().Value.Should().HaveCount(1); - mapper.PropertyMetadata.First().Value.First().Value.Name.Should().Be(nameof(Post.Id)); - mapper.PropertyMetadata.First().Value.First().Value.PropertyInfo.Should().BeSameAs(typeof(Post).GetProperty(metadata.Name)); - } - - [Fact] - public void Property_Throws_Exception_With_FluentApiMetadataSourceType_Disabled() - { - // Arrange - var optionsProvider = Substitute.For(); - optionsProvider - .GetStrainerOptions() - .Returns(new StrainerOptions { MetadataSourceType = MetadataSourceType.Attributes }); - var propertyInfoProviderMock = Substitute.For(); - var mapper = new MetadataMapper(optionsProvider, propertyInfoProviderMock); - - // Act & Assert - Assert.Throws(() => mapper.Property(p => p.Id)); - } - - [Fact] - public void AddPropertyMetadata_Throws_Exception_With_FluentApiMetadataSourceType_Disabled() - { - // Arrange - var optionsProvider = Substitute.For(); - optionsProvider - .GetStrainerOptions() - .Returns(new StrainerOptions { MetadataSourceType = MetadataSourceType.Attributes }); - var propertyInfoProviderMock = Substitute.For(); - var mapper = new MetadataMapper(optionsProvider, propertyInfoProviderMock); - var metadata = Substitute.For(); - - // Act & Assert - Assert.Throws(() => mapper.AddPropertyMetadata(metadata)); - } - - [Fact] - public void AddObjectMetadata_Adds_ObjectMetadata() - { - // Arrange - var modelType = typeof(Post); - var optionsProvider = Substitute.For(); - optionsProvider - .GetStrainerOptions() - .Returns(new StrainerOptions()); - var propertyInfoProviderMock = Substitute.For(); - var mapper = new MetadataMapper(optionsProvider, propertyInfoProviderMock); - var objectMetadata = Substitute.For(); - - // Act - mapper.AddObjectMetadata(objectMetadata); - - // Assert - mapper.ObjectMetadata.Should().NotBeEmpty(); - mapper.ObjectMetadata.Keys.Should().BeEquivalentTo([modelType]); - mapper.ObjectMetadata[modelType].Should().BeSameAs(objectMetadata); - } - - [Fact] - public void AddObjectMetadata_Throws_Exception_With_FluentApiMetadataSourceType_Disabled() - { - // Arrange - var optionsProvider = Substitute.For(); - optionsProvider - .GetStrainerOptions() - .Returns(new StrainerOptions { MetadataSourceType = MetadataSourceType.Attributes }); - var propertyInfoProviderMock = Substitute.For(); - var mapper = new MetadataMapper(optionsProvider, propertyInfoProviderMock); - var objectMetadata = Substitute.For(); - - // Act & Assert - Assert.Throws(() => mapper.AddObjectMetadata(objectMetadata)); - } - - [Fact] - public void Object_AddObjectMetadata_Adds_ObjectMetadata_And_ReturnsBuilder() - { - // Arrange - var modelType = typeof(Post); - var optionsProvider = Substitute.For(); - optionsProvider - .GetStrainerOptions() - .Returns(new StrainerOptions()); - var defaultSortingPropertyName = nameof(Post.Id); - var defaultSortingPropertyInfo = typeof(Post).GetProperty(defaultSortingPropertyName); - Expression> defaultSortingPropertyExpression = b => b.Id; - var propertyInfoProviderMock = Substitute.For(); - propertyInfoProviderMock - .GetPropertyInfoAndFullName(defaultSortingPropertyExpression) - .Returns((defaultSortingPropertyInfo, defaultSortingPropertyName)); - var mapper = new MetadataMapper(optionsProvider, propertyInfoProviderMock); - - // Act - var builder = mapper.Object(defaultSortingPropertyExpression); - - // Assert - builder.Should().NotBeNull(); - - mapper.ObjectMetadata.Should().NotBeEmpty(); - mapper.ObjectMetadata.Keys.Should().BeEquivalentTo([modelType]); - mapper.ObjectMetadata[modelType].Should().NotBeNull(); - } - - [Fact] - public void Object_Throws_Exception_With_FluentApiMetadataSourceType_Disabled() - { - // Arrange - var optionsProvider = Substitute.For(); - optionsProvider - .GetStrainerOptions() - .Returns(new StrainerOptions { MetadataSourceType = MetadataSourceType.Attributes }); - var propertyInfoProviderMock = Substitute.For(); - var mapper = new MetadataMapper(optionsProvider, propertyInfoProviderMock); - - // Act & Assert - Assert.Throws(() => mapper.Object(x => x.Id)); - } - - private class Post - { - public int Id { get; set; } - } - - private class Comment - { - public int Id { get; set; } - } -} -- GitLab From 9a3f42281d395c29b2abc231c8ca77ffd545a2f0 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Wed, 15 Jan 2025 23:41:46 +0100 Subject: [PATCH 34/56] Remove redundant IEquatable implementations and comparison operators --- .../Models/Filtering/Terms/FilterTerm.cs | 64 +---------------- .../Models/Metadata/PropertyMetadata.cs | 71 +------------------ src/Strainer/Models/Sorting/Terms/SortTerm.cs | 61 +--------------- 3 files changed, 3 insertions(+), 193 deletions(-) diff --git a/src/Strainer/Models/Filtering/Terms/FilterTerm.cs b/src/Strainer/Models/Filtering/Terms/FilterTerm.cs index 663f467f..de586cdf 100644 --- a/src/Strainer/Models/Filtering/Terms/FilterTerm.cs +++ b/src/Strainer/Models/Filtering/Terms/FilterTerm.cs @@ -5,7 +5,7 @@ namespace Fluorite.Strainer.Models.Filtering.Terms; /// /// Holds segregated details about a single filtering term. /// -public class FilterTerm : IFilterTerm, IEquatable +public class FilterTerm : IFilterTerm { /// /// Initializes a new instance of the class. @@ -37,66 +37,4 @@ public class FilterTerm : IFilterTerm, IEquatable /// Gets or sets the list of values. /// public IList Values { get; set; } - - public static bool operator ==(FilterTerm term1, FilterTerm term2) - { - return EqualityComparer.Default.Equals(term1, term2); - } - - public static bool operator !=(FilterTerm term1, FilterTerm term2) - { - return !(term1 == term2); - } - - /// - /// Checks if current instance of - /// is equal to other instance. - /// - /// - /// Other instance. - /// - /// - /// if provided other - /// instance is equal to the current one; otherwise . - /// - public override bool Equals(object obj) - { - return Equals(obj as FilterTerm); - } - - /// - /// Checks if current instance of - /// is equal to other instance. - /// - /// - /// Other instance. - /// - /// - /// if provided other - /// instance is equal to the current one; otherwise . - /// - public bool Equals(FilterTerm? other) - { - return other is not null && - Names.SequenceEqual(other.Names) && - EqualityComparer.Default.Equals(Operator, other.Operator) && - Values.SequenceEqual(other.Values); - } - - /// - /// Gets hash code representation of current - /// . - /// - /// - /// A hash code for the current . - /// - public override int GetHashCode() - { - var hashCode = 215681951; - hashCode = (hashCode * -1521134295) + EqualityComparer>.Default.GetHashCode(Names); - hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(Operator); - hashCode = (hashCode * -1521134295) + EqualityComparer>.Default.GetHashCode(Values); - - return hashCode; - } } diff --git a/src/Strainer/Models/Metadata/PropertyMetadata.cs b/src/Strainer/Models/Metadata/PropertyMetadata.cs index 1252435a..77f3ca7b 100644 --- a/src/Strainer/Models/Metadata/PropertyMetadata.cs +++ b/src/Strainer/Models/Metadata/PropertyMetadata.cs @@ -7,7 +7,7 @@ namespace Fluorite.Strainer.Models.Metadata; /// Represents default property metadata model. /// [DebuggerDisplay("\\{" + nameof(Name) + " = " + "{" + nameof(Name) + "} \\}")] -public class PropertyMetadata : IPropertyMetadata, IEquatable +public class PropertyMetadata : IPropertyMetadata { /// /// Initializes a new instance of the class. @@ -67,73 +67,4 @@ public class PropertyMetadata : IPropertyMetadata, IEquatable /// property. /// public PropertyInfo PropertyInfo { get; } - - public static bool operator ==(PropertyMetadata metadata1, PropertyMetadata metadata2) - { - return EqualityComparer.Default.Equals(metadata1, metadata2); - } - - public static bool operator !=(PropertyMetadata metadata1, PropertyMetadata metadata2) - { - return !(metadata1 == metadata2); - } - - /// - /// Checks if current instance of - /// is equal to other instance. - /// - /// - /// Other instance. - /// - /// - /// if provided other - /// instance is equal to the current one; otherwise . - /// - public override bool Equals(object obj) - { - return Equals(obj as PropertyMetadata); - } - - /// - /// Checks if current instance of - /// is equal to other instance. - /// - /// - /// Other instance. - /// - /// - /// if provided other - /// instance is equal to the current one; otherwise . - /// - public bool Equals(PropertyMetadata? other) - { - return other is not null && - DisplayName == other.DisplayName && - IsDefaultSorting == other.IsDefaultSorting && - IsDefaultSortingDescending == other.IsDefaultSortingDescending && - IsFilterable == other.IsFilterable && - IsSortable == other.IsSortable && - Name == other.Name && - EqualityComparer.Default.Equals(PropertyInfo, other.PropertyInfo); - } - - /// - /// Gets hash code representation of current - /// . - /// - /// - /// A hash code for the current . - /// - public override int GetHashCode() - { - var hashCode = -1500598692; - hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(DisplayName); - hashCode = (hashCode * -1521134295) + IsDefaultSorting.GetHashCode(); - hashCode = (hashCode * -1521134295) + IsDefaultSortingDescending.GetHashCode(); - hashCode = (hashCode * -1521134295) + IsFilterable.GetHashCode(); - hashCode = (hashCode * -1521134295) + IsSortable.GetHashCode(); - hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(Name); - hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(PropertyInfo); - return hashCode; - } } diff --git a/src/Strainer/Models/Sorting/Terms/SortTerm.cs b/src/Strainer/Models/Sorting/Terms/SortTerm.cs index 20b74439..2a562f64 100644 --- a/src/Strainer/Models/Sorting/Terms/SortTerm.cs +++ b/src/Strainer/Models/Sorting/Terms/SortTerm.cs @@ -8,7 +8,7 @@ namespace Fluorite.Strainer.Models.Sorting.Terms; [DebuggerDisplay("\\{" + nameof(Name) + " = " + "{" + nameof(Name) + "}, " + nameof(IsDescending) + " = " + "{" + nameof(IsDescending) + "}" + "\\}")] -public class SortTerm : ISortTerm, IEquatable +public class SortTerm : ISortTerm { /// /// Initializes a new instance of the class. @@ -36,63 +36,4 @@ public class SortTerm : ISortTerm, IEquatable /// Gets the name within sorting term. /// public string Name { get; } - - public static bool operator ==(SortTerm term1, SortTerm term2) - { - return EqualityComparer.Default.Equals(term1, term2); - } - - public static bool operator !=(SortTerm term1, SortTerm term2) - { - return !(term1 == term2); - } - - /// - /// Checks if current instance of - /// is equal to other instance. - /// - /// - /// Other instance. - /// - /// - /// if provided other - /// instance is equal to the current one; otherwise . - /// - public override bool Equals(object obj) - { - return Equals(obj as SortTerm); - } - - /// - /// Checks if current instance of is equal - /// to other instance. - /// - /// - /// Other instance. - /// - /// - /// if provided other - /// instance is equal to the current one; otherwise . - /// - public bool Equals(SortTerm? other) - { - return other is not null && - IsDescending == other.IsDescending && - Name == other.Name; - } - - /// - /// Gets hash code representation of current - /// . - /// - /// - /// A hash code for the current . - /// - public override int GetHashCode() - { - var hashCode = 1436560617; - hashCode = (hashCode * -1521134295) + IsDescending.GetHashCode(); - hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(Name); - return hashCode; - } } -- GitLab From 64d9371f41a226a51d9b0c9e466f1d435fe3a574 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sat, 18 Jan 2025 16:31:25 +0100 Subject: [PATCH 35/56] Improve debugger display for read only hashset --- src/Strainer/Collections/ReadOnlyHashSet.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Strainer/Collections/ReadOnlyHashSet.cs b/src/Strainer/Collections/ReadOnlyHashSet.cs index af2c5d7b..01b9de81 100644 --- a/src/Strainer/Collections/ReadOnlyHashSet.cs +++ b/src/Strainer/Collections/ReadOnlyHashSet.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Diagnostics; namespace Fluorite.Strainer.Collections; @@ -8,6 +9,7 @@ namespace Fluorite.Strainer.Collections; /// /// The type of elements in the hash set. /// +[DebuggerDisplay(nameof(Count) + " = {" + nameof(Count) + ",nq}")] public class ReadOnlyHashSet : IReadOnlySet, IReadOnlyCollection, IEnumerable, IEnumerable { private readonly HashSet _set; -- GitLab From 93c9715ba57e02b72176456a6d9638804a234727 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sun, 19 Jan 2025 18:59:43 +0100 Subject: [PATCH 36/56] Rework sorting way Remove unknown enum member from sorting wy. Update metadata models, make default sorting way nullable, always fall back to default sorting way from Strainer options to unify behaviour. Update tests. --- .../Attributes/StrainerObjectAttribute.cs | 17 ++- .../Attributes/StrainerPropertyAttribute.cs | 13 +-- .../Models/Metadata/IObjectMetadata.cs | 11 +- .../Models/Metadata/IPropertyMetadata.cs | 11 +- .../Models/Metadata/ObjectMetadata.cs | 25 ++-- .../Models/Metadata/PropertyMetadata.cs | 20 ++-- src/Strainer/Models/Sorting/SortingWay.cs | 7 +- .../Attributes/AttributeMetadataRetriever.cs | 6 +- .../AttributePropertyMetadataBuilder.cs | 20 +++- .../IAttributePropertyMetadataBuilder.cs | 4 +- .../PropertyMetadataDictionaryProvider.cs | 2 +- .../Attributes/StrainerAttributeProvider.cs | 17 ++- .../FluentApi/FluentApiMetadataProvider.cs | 8 +- .../FluentApiPropertyMetadataBuilder.cs | 26 +++-- .../IFluentApiPropertyMetadataBuilder.cs | 4 +- .../Metadata/ObjectMetadataBuilder.cs | 9 +- .../Metadata/PropertyMetadataBuilder.cs | 5 +- .../DescendingPrefixSortingWayFormatter.cs | 24 +--- .../Services/Sorting/ISortingWayFormatter.cs | 4 +- .../Sorting/SortExpressionProvider.cs | 14 ++- .../Sorting/SortPropertyMetadataBuilder.cs | 5 +- .../Services/Sorting/SortTermParser.cs | 7 +- .../Sorting/SuffixSortingWayFormatter.cs | 19 +-- ...trainerServiceCollectionExtensionsTests.cs | 2 +- .../CustomFilteringExpressionProviderTests.cs | 1 - .../FilterExpressionProviderTests.cs | 1 - .../Filtering/FilterOperatorValidatorTests.cs | 1 - .../Steps/ChangeTypeOfFilterValueStepTests.cs | 1 - .../AttributeMetadataProviderTests.cs | 1 - .../AttributeMetadataRetrieverTests.cs | 3 +- .../AttributePropertyMetadataBuilderTests.cs | 9 +- .../StrainerAttributeProviderTests.cs | 17 ++- .../FluentApiMetadataProviderTests.cs | 8 +- .../FluentApiPropertyMetadataBuilderTests.cs | 109 ++++++++++++++++++ .../Services/Metadata/MetadataFacadeTests.cs | 1 - .../Metadata/ObjectMetadataBuilderTests.cs | 11 +- .../Metadata/PropertyMetadataBuilderTests.cs | 11 +- .../SortPropertyMetadataBuilderTests.cs | 7 +- .../Pipelines/SortPipelineOperationTests.cs | 1 - .../CustomSortingExpressionProviderTests.cs | 1 - ...escendingPrefixSortingWayFormatterTests.cs | 36 +----- .../Sorting/SortExpressionProviderTests.cs | 60 +++++++++- .../Services/Sorting/SortTermParserTests.cs | 5 +- .../Services/Sorting/SortingApplierTests.cs | 1 - .../Sorting/SuffixSortingWayFormatterTests.cs | 42 +------ test/Strainer.UnitTests/_GlobalUsings.cs | 1 + 46 files changed, 345 insertions(+), 263 deletions(-) create mode 100644 test/Strainer.UnitTests/Services/Metadata/FluentApi/FluentApiPropertyMetadataBuilderTests.cs diff --git a/src/Strainer/Attributes/StrainerObjectAttribute.cs b/src/Strainer/Attributes/StrainerObjectAttribute.cs index 2c4d8589..4cdc7eb4 100644 --- a/src/Strainer/Attributes/StrainerObjectAttribute.cs +++ b/src/Strainer/Attributes/StrainerObjectAttribute.cs @@ -1,4 +1,5 @@ using Fluorite.Strainer.Models.Metadata; +using Fluorite.Strainer.Models.Sorting; using System.Reflection; namespace Fluorite.Strainer.Attributes; @@ -35,29 +36,27 @@ public class StrainerObjectAttribute : Attribute, IObjectMetadata /// /// Property name being default sorting property for marked object. /// - /// - /// A value indicating whether default sorting way - /// for marked object is descending. + /// + /// A default sorting way for marked object. /// /// /// is , /// empty or contains only whitespace characters. /// - public StrainerObjectAttribute(string defaultSortingPropertyName, bool isDefaultSortingDescending) : this(defaultSortingPropertyName) + public StrainerObjectAttribute(string defaultSortingPropertyName, SortingWay defaultSortingWay) : this(defaultSortingPropertyName) { - IsDefaultSortingDescending = isDefaultSortingDescending; + DefaultSortingWay = defaultSortingWay; } /// public string DefaultSortingPropertyName { get; } /// - /// Gets a value indicating whether default - /// sorting way for marked object is descending. + /// Gets default sorting way used when applying sorting using default sorting property. /// - /// Defaults to . + /// Defaults to . /// - public bool IsDefaultSortingDescending { get; } = false; + public SortingWay? DefaultSortingWay { get; } /// /// Gets or sets a value indicating whether marked diff --git a/src/Strainer/Attributes/StrainerPropertyAttribute.cs b/src/Strainer/Attributes/StrainerPropertyAttribute.cs index 971fd8a4..493bbd29 100644 --- a/src/Strainer/Attributes/StrainerPropertyAttribute.cs +++ b/src/Strainer/Attributes/StrainerPropertyAttribute.cs @@ -1,4 +1,5 @@ using Fluorite.Strainer.Models.Metadata; +using Fluorite.Strainer.Models.Sorting; using System.Reflection; namespace Fluorite.Strainer.Attributes; @@ -35,12 +36,12 @@ public class StrainerPropertyAttribute : Attribute, IPropertyMetadata public bool IsDefaultSorting { get; set; } = false; /// - /// Gets or sets a value indicating whether default - /// sorting should be performed in a descending way. + /// Gets or sets default sorting way used when related property is marked + /// as a default sorting property. /// - /// Defaults to . + /// Defaults to . /// - public bool IsDefaultSortingDescending { get; set; } = false; + public SortingWay? DefaultSortingWay { get; set; } = null; /// /// Gets or sets a value indicating whether related @@ -58,9 +59,7 @@ public class StrainerPropertyAttribute : Attribute, IPropertyMetadata /// public bool IsSortable { get; set; } = true; - /// - /// Gets the real name of related property. - /// + /// public string Name => PropertyInfo!.Name; /// diff --git a/src/Strainer/Models/Metadata/IObjectMetadata.cs b/src/Strainer/Models/Metadata/IObjectMetadata.cs index b7b7c083..1d71e494 100644 --- a/src/Strainer/Models/Metadata/IObjectMetadata.cs +++ b/src/Strainer/Models/Metadata/IObjectMetadata.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using Fluorite.Strainer.Models.Sorting; +using System.Reflection; namespace Fluorite.Strainer.Models.Metadata; @@ -9,15 +10,15 @@ namespace Fluorite.Strainer.Models.Metadata; public interface IObjectMetadata { /// - /// Gets a property name being default sorting property for marked object. + /// Gets a property name that will be used as a default (fallback) property + /// when no sorting information is available but sorting needs to be applied. /// string DefaultSortingPropertyName { get; } /// - /// Gets a value indicating whether default - /// sorting way for marked object is descending. + /// Gets default sorting way used when applying sorting using default sorting property. /// - bool IsDefaultSortingDescending { get; } + SortingWay? DefaultSortingWay { get; } /// /// Gets a value indicating whether related diff --git a/src/Strainer/Models/Metadata/IPropertyMetadata.cs b/src/Strainer/Models/Metadata/IPropertyMetadata.cs index d6e5bf97..3508b0f4 100644 --- a/src/Strainer/Models/Metadata/IPropertyMetadata.cs +++ b/src/Strainer/Models/Metadata/IPropertyMetadata.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using Fluorite.Strainer.Models.Sorting; +using System.Reflection; namespace Fluorite.Strainer.Models.Metadata; @@ -17,16 +18,16 @@ public interface IPropertyMetadata /// property should be used as a default (fallback) property when /// no sorting information was provided but sorting was still requested. /// - /// Default sorting is not perfomed when sorting information was not + /// Default sorting is not perfomed when sorting information is not /// properly recognized. /// bool IsDefaultSorting { get; } /// - /// Gets a value indicating whether default - /// sorting should be performed in a descending way. + /// Gets default sorting way used when related property is marked + /// as a default sorting property. /// - bool IsDefaultSortingDescending { get; } + SortingWay? DefaultSortingWay { get; } /// /// Gets a value indicating whether related diff --git a/src/Strainer/Models/Metadata/ObjectMetadata.cs b/src/Strainer/Models/Metadata/ObjectMetadata.cs index 6053cda0..2c2dd0be 100644 --- a/src/Strainer/Models/Metadata/ObjectMetadata.cs +++ b/src/Strainer/Models/Metadata/ObjectMetadata.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using Fluorite.Strainer.Models.Sorting; +using System.Reflection; namespace Fluorite.Strainer.Models.Metadata; @@ -13,25 +14,19 @@ public class ObjectMetadata : IObjectMetadata /// public ObjectMetadata( string defaultSortingPropertyName, - bool isDefaultSortingDescending, - PropertyInfo defaultSortingPropertyInfo) + PropertyInfo defaultSortingPropertyInfo, + SortingWay? defaultSortingWay) { DefaultSortingPropertyName = Guard.Against.NullOrWhiteSpace(defaultSortingPropertyName); - IsDefaultSortingDescending = isDefaultSortingDescending; DefaultSortingPropertyInfo = Guard.Against.Null(defaultSortingPropertyInfo); + DefaultSortingWay = defaultSortingWay; } - /// - /// Gets a property name being default sorting property - /// for marked object. - /// + /// public string DefaultSortingPropertyName { get; } - /// - /// Gets a value indicating whether default - /// sorting way for marked object is descending. - /// - public bool IsDefaultSortingDescending { get; } + /// + public SortingWay? DefaultSortingWay { get; } /// /// Gets or sets a value indicating whether related @@ -45,8 +40,6 @@ public class ObjectMetadata : IObjectMetadata /// public bool IsSortable { get; set; } - /// - /// Gets the for default sorting property. - /// + /// public PropertyInfo DefaultSortingPropertyInfo { get; } } diff --git a/src/Strainer/Models/Metadata/PropertyMetadata.cs b/src/Strainer/Models/Metadata/PropertyMetadata.cs index 77f3ca7b..10fc2ca2 100644 --- a/src/Strainer/Models/Metadata/PropertyMetadata.cs +++ b/src/Strainer/Models/Metadata/PropertyMetadata.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using Fluorite.Strainer.Models.Sorting; +using System.Diagnostics; using System.Reflection; namespace Fluorite.Strainer.Models.Metadata; @@ -34,16 +35,16 @@ public class PropertyMetadata : IPropertyMetadata /// property should be used as a default (fallback) property when /// no sorting information was provided but sorting was still requested. /// - /// Default sorting is not perfomed when sorting information was not + /// Default sorting is not perfomed when sorting information is not /// properly recognized. /// public bool IsDefaultSorting { get; set; } /// - /// Gets or sets a value indicating whether default - /// sorting should be performed in a descending way. + /// Gets or sets default sorting way used when related property is marked + /// as a default sorting property. /// - public bool IsDefaultSortingDescending { get; set; } + public SortingWay? DefaultSortingWay { get; set; } /// /// Gets or sets a value indicating whether related @@ -57,14 +58,9 @@ public class PropertyMetadata : IPropertyMetadata /// public bool IsSortable { get; set; } - /// - /// Gets the name under which property was marked. - /// + /// public string Name { get; } - /// - /// Gets the for related - /// property. - /// + /// public PropertyInfo PropertyInfo { get; } } diff --git a/src/Strainer/Models/Sorting/SortingWay.cs b/src/Strainer/Models/Sorting/SortingWay.cs index 04d8ce2d..4671300d 100644 --- a/src/Strainer/Models/Sorting/SortingWay.cs +++ b/src/Strainer/Models/Sorting/SortingWay.cs @@ -5,15 +5,10 @@ /// public enum SortingWay { - /// - /// Unknown sorting way, that could not be established. - /// - Unknown = 0, - /// /// Ascending sorting way. /// - Ascending, + Ascending = 1, /// /// Descending sorting way. diff --git a/src/Strainer/Services/Metadata/Attributes/AttributeMetadataRetriever.cs b/src/Strainer/Services/Metadata/Attributes/AttributeMetadataRetriever.cs index ccd60e13..b6f8dc1d 100644 --- a/src/Strainer/Services/Metadata/Attributes/AttributeMetadataRetriever.cs +++ b/src/Strainer/Services/Metadata/Attributes/AttributeMetadataRetriever.cs @@ -55,7 +55,7 @@ public class AttributeMetadataRetriever : IAttributeMetadataRetriever $"{modelType.FullName} and it's accessible."); } - return _attributePropertyMetadataBuilder.BuildDefaultPropertyMetadata(attribute, propertyInfo); + return _attributePropertyMetadataBuilder.BuildDefaultMetadata(attribute, propertyInfo); } currentType = currentType.BaseType; @@ -172,7 +172,7 @@ public class AttributeMetadataRetriever : IAttributeMetadataRetriever if (isMatching && attribute is not null && propertyInfo is not null) { - return _attributePropertyMetadataBuilder.BuildPropertyMetadata(attribute, propertyInfo); + return _attributePropertyMetadataBuilder.BuildDefaultMetadataForProperty(attribute, propertyInfo); } currentType = currentType.BaseType; @@ -237,7 +237,7 @@ public class AttributeMetadataRetriever : IAttributeMetadataRetriever if (attribute != null) { return _propertyInfoProvider.GetPropertyInfos(currentType) - .Select(propertyInfo => _attributePropertyMetadataBuilder.BuildPropertyMetadata(attribute, propertyInfo)) + .Select(propertyInfo => _attributePropertyMetadataBuilder.BuildDefaultMetadataForProperty(attribute, propertyInfo)) .ToList() .AsReadOnly(); } diff --git a/src/Strainer/Services/Metadata/Attributes/AttributePropertyMetadataBuilder.cs b/src/Strainer/Services/Metadata/Attributes/AttributePropertyMetadataBuilder.cs index 1864cb01..f9ee9420 100644 --- a/src/Strainer/Services/Metadata/Attributes/AttributePropertyMetadataBuilder.cs +++ b/src/Strainer/Services/Metadata/Attributes/AttributePropertyMetadataBuilder.cs @@ -6,7 +6,14 @@ namespace Fluorite.Strainer.Services.Metadata.Attributes; public class AttributePropertyMetadataBuilder : IAttributePropertyMetadataBuilder { - public IPropertyMetadata BuildDefaultPropertyMetadata(StrainerObjectAttribute attribute, PropertyInfo propertyInfo) + private readonly IStrainerOptionsProvider _strainerOptionsProvider; + + public AttributePropertyMetadataBuilder(IStrainerOptionsProvider strainerOptionsProvider) + { + _strainerOptionsProvider = Guard.Against.Null(strainerOptionsProvider); + } + + public IPropertyMetadata BuildDefaultMetadata(StrainerObjectAttribute attribute, PropertyInfo propertyInfo) { Guard.Against.Null(attribute); Guard.Against.Null(propertyInfo); @@ -14,23 +21,26 @@ public class AttributePropertyMetadataBuilder : IAttributePropertyMetadataBuilde return new PropertyMetadata(propertyInfo.Name, propertyInfo) { IsDefaultSorting = true, - IsDefaultSortingDescending = attribute.IsDefaultSortingDescending, + DefaultSortingWay = attribute.DefaultSortingWay, IsFilterable = attribute.IsFilterable, IsSortable = attribute.IsSortable, }; } - public IPropertyMetadata BuildPropertyMetadata(StrainerObjectAttribute attribute, PropertyInfo propertyInfo) + public IPropertyMetadata BuildDefaultMetadataForProperty(StrainerObjectAttribute attribute, PropertyInfo propertyInfo) { Guard.Against.Null(attribute); Guard.Against.Null(propertyInfo); - var isDefaultSorting = attribute.DefaultSortingPropertyName == propertyInfo.Name; + var isDefaultSorting = attribute.DefaultSortingPropertyInfo == propertyInfo; + var defaultSortingWay = isDefaultSorting + ? attribute.DefaultSortingWay + : _strainerOptionsProvider.GetStrainerOptions().DefaultSortingWay; return new PropertyMetadata(propertyInfo.Name, propertyInfo) { IsDefaultSorting = isDefaultSorting, - IsDefaultSortingDescending = isDefaultSorting && attribute.IsDefaultSortingDescending, + DefaultSortingWay = defaultSortingWay, IsFilterable = attribute.IsFilterable, IsSortable = attribute.IsSortable, }; diff --git a/src/Strainer/Services/Metadata/Attributes/IAttributePropertyMetadataBuilder.cs b/src/Strainer/Services/Metadata/Attributes/IAttributePropertyMetadataBuilder.cs index 90988610..a34220be 100644 --- a/src/Strainer/Services/Metadata/Attributes/IAttributePropertyMetadataBuilder.cs +++ b/src/Strainer/Services/Metadata/Attributes/IAttributePropertyMetadataBuilder.cs @@ -6,7 +6,7 @@ namespace Fluorite.Strainer.Services.Metadata.Attributes; public interface IAttributePropertyMetadataBuilder { - IPropertyMetadata BuildDefaultPropertyMetadata(StrainerObjectAttribute attribute, PropertyInfo propertyInfo); + IPropertyMetadata BuildDefaultMetadata(StrainerObjectAttribute attribute, PropertyInfo propertyInfo); - IPropertyMetadata BuildPropertyMetadata(StrainerObjectAttribute attribute, PropertyInfo propertyInfo); + IPropertyMetadata BuildDefaultMetadataForProperty(StrainerObjectAttribute attribute, PropertyInfo propertyInfo); } \ No newline at end of file diff --git a/src/Strainer/Services/Metadata/Attributes/PropertyMetadataDictionaryProvider.cs b/src/Strainer/Services/Metadata/Attributes/PropertyMetadataDictionaryProvider.cs index c8c2f592..7459febf 100644 --- a/src/Strainer/Services/Metadata/Attributes/PropertyMetadataDictionaryProvider.cs +++ b/src/Strainer/Services/Metadata/Attributes/PropertyMetadataDictionaryProvider.cs @@ -38,7 +38,7 @@ public class PropertyMetadataDictionaryProvider : IPropertyMetadataDictionaryPro Guard.Against.Null(strainerObjectAttribute); return _propertyInfoProvider.GetPropertyInfos(type) - .Select(propertyInfo => _attributePropertyMetadataBuilder.BuildPropertyMetadata(strainerObjectAttribute, propertyInfo)) + .Select(propertyInfo => _attributePropertyMetadataBuilder.BuildDefaultMetadataForProperty(strainerObjectAttribute, propertyInfo)) .ToDictionary(metadata => metadata.Name, metadata => metadata) .ToReadOnly(); } diff --git a/src/Strainer/Services/Metadata/Attributes/StrainerAttributeProvider.cs b/src/Strainer/Services/Metadata/Attributes/StrainerAttributeProvider.cs index 25d541b0..674c62f0 100644 --- a/src/Strainer/Services/Metadata/Attributes/StrainerAttributeProvider.cs +++ b/src/Strainer/Services/Metadata/Attributes/StrainerAttributeProvider.cs @@ -5,11 +5,24 @@ namespace Fluorite.Strainer.Services.Metadata.Attributes; public class StrainerAttributeProvider : IStrainerAttributeProvider { + private readonly IPropertyInfoProvider _propertyInfoProvider; + + public StrainerAttributeProvider(IPropertyInfoProvider propertyInfoProvider) + { + _propertyInfoProvider = Guard.Against.Null(propertyInfoProvider); + } + public StrainerObjectAttribute? GetObjectAttribute(Type type) { Guard.Against.Null(type); - return type.GetCustomAttribute(inherit: false); + var attribute = type.GetCustomAttribute(inherit: false); + if (attribute != null) + { + attribute.DefaultSortingPropertyInfo = _propertyInfoProvider.GetPropertyInfo(type, attribute.DefaultSortingPropertyName); + } + + return attribute; } public StrainerPropertyAttribute? GetPropertyAttribute(PropertyInfo propertyInfo) @@ -17,7 +30,7 @@ public class StrainerAttributeProvider : IStrainerAttributeProvider Guard.Against.Null(propertyInfo); var attribute = propertyInfo.GetCustomAttribute(inherit: false); - if (attribute != null && attribute.PropertyInfo == null) + if (attribute != null) { attribute.PropertyInfo = propertyInfo; } diff --git a/src/Strainer/Services/Metadata/FluentApi/FluentApiMetadataProvider.cs b/src/Strainer/Services/Metadata/FluentApi/FluentApiMetadataProvider.cs index e25fb48c..74edf004 100644 --- a/src/Strainer/Services/Metadata/FluentApi/FluentApiMetadataProvider.cs +++ b/src/Strainer/Services/Metadata/FluentApi/FluentApiMetadataProvider.cs @@ -56,7 +56,7 @@ public class FluentApiMetadataProvider : IMetadataProvider { if (_metadataProvider.GetObjectMetadata().TryGetValue(modelType, out var objectMetadata)) { - propertyMetadata = _propertyMetadataBuilder.BuildPropertyMetadata(objectMetadata); + propertyMetadata = _propertyMetadataBuilder.BuildDefaultMetadata(objectMetadata); } } @@ -99,7 +99,7 @@ public class FluentApiMetadataProvider : IMetadataProvider var propertyInfo = _propertyInfoProvider.GetPropertyInfo(modelType, name); if (propertyInfo is not null) { - return _propertyMetadataBuilder.BuildPropertyMetadataFromPropertyInfo(objectMetadata, propertyInfo); + return _propertyMetadataBuilder.BuildDefaultMetadataForProperty(objectMetadata, propertyInfo); } } } @@ -124,7 +124,7 @@ public class FluentApiMetadataProvider : IMetadataProvider if (_metadataProvider.GetObjectMetadata().TryGetValue(modelType, out var objectMetadata)) { return _propertyInfoProvider.GetPropertyInfos(modelType) - .Select(p => _propertyMetadataBuilder.BuildPropertyMetadataFromPropertyInfo(objectMetadata, p)) + .Select(p => _propertyMetadataBuilder.BuildDefaultMetadataForProperty(objectMetadata, p)) .ToList(); } @@ -150,7 +150,7 @@ public class FluentApiMetadataProvider : IMetadataProvider { return _propertyInfoProvider .GetPropertyInfos(type) - .Select(propertyInfo => _propertyMetadataBuilder.BuildPropertyMetadataFromPropertyInfo(objectMetadata, propertyInfo)) + .Select(propertyInfo => _propertyMetadataBuilder.BuildDefaultMetadataForProperty(objectMetadata, propertyInfo)) .ToDictionary(p => p.Name, p => p) .ToReadOnlyDictionary(); } diff --git a/src/Strainer/Services/Metadata/FluentApi/FluentApiPropertyMetadataBuilder.cs b/src/Strainer/Services/Metadata/FluentApi/FluentApiPropertyMetadataBuilder.cs index ad461bda..297d3290 100644 --- a/src/Strainer/Services/Metadata/FluentApi/FluentApiPropertyMetadataBuilder.cs +++ b/src/Strainer/Services/Metadata/FluentApi/FluentApiPropertyMetadataBuilder.cs @@ -5,38 +5,44 @@ namespace Fluorite.Strainer.Services.Metadata.FluentApi; public class FluentApiPropertyMetadataBuilder : IFluentApiPropertyMetadataBuilder { - public IPropertyMetadata BuildPropertyMetadata(IObjectMetadata objectMetadata) + private readonly IStrainerOptionsProvider _strainerOptionsProvider; + + public FluentApiPropertyMetadataBuilder(IStrainerOptionsProvider strainerOptionsProvider) { - Guard.Against.Null(objectMetadata); + _strainerOptionsProvider = Guard.Against.Null(strainerOptionsProvider); + } - if (objectMetadata.DefaultSortingPropertyInfo is null) - { - throw new ArgumentException("Missing PropertyInfo in passed object metadata.", nameof(objectMetadata)); - } + public IPropertyMetadata BuildDefaultMetadata(IObjectMetadata objectMetadata) + { + Guard.Against.Null(objectMetadata); + Guard.Against.Null(objectMetadata.DefaultSortingPropertyInfo); return new PropertyMetadata(objectMetadata.DefaultSortingPropertyName, objectMetadata.DefaultSortingPropertyInfo) { IsDefaultSorting = true, - IsDefaultSortingDescending = objectMetadata.IsDefaultSortingDescending, + DefaultSortingWay = objectMetadata.DefaultSortingWay, IsFilterable = objectMetadata.IsFilterable, IsSortable = objectMetadata.IsSortable, }; } - public IPropertyMetadata BuildPropertyMetadataFromPropertyInfo(IObjectMetadata objectMetadata, PropertyInfo propertyInfo) + public IPropertyMetadata BuildDefaultMetadataForProperty(IObjectMetadata objectMetadata, PropertyInfo propertyInfo) { Guard.Against.Null(objectMetadata); + Guard.Against.Null(objectMetadata.DefaultSortingPropertyInfo); Guard.Against.Null(propertyInfo); var isDefaultSorting = objectMetadata.DefaultSortingPropertyInfo == propertyInfo; - var isDefaultSortingAscending = isDefaultSorting && objectMetadata.IsDefaultSortingDescending; + var defaultSortingWay = isDefaultSorting + ? objectMetadata.DefaultSortingWay + : _strainerOptionsProvider.GetStrainerOptions().DefaultSortingWay; return new PropertyMetadata(propertyInfo.Name, propertyInfo) { IsFilterable = objectMetadata.IsFilterable, IsSortable = objectMetadata.IsSortable, IsDefaultSorting = isDefaultSorting, - IsDefaultSortingDescending = isDefaultSortingAscending, + DefaultSortingWay = defaultSortingWay, }; } } diff --git a/src/Strainer/Services/Metadata/FluentApi/IFluentApiPropertyMetadataBuilder.cs b/src/Strainer/Services/Metadata/FluentApi/IFluentApiPropertyMetadataBuilder.cs index ba2f058a..c23c76d9 100644 --- a/src/Strainer/Services/Metadata/FluentApi/IFluentApiPropertyMetadataBuilder.cs +++ b/src/Strainer/Services/Metadata/FluentApi/IFluentApiPropertyMetadataBuilder.cs @@ -5,7 +5,7 @@ namespace Fluorite.Strainer.Services.Metadata.FluentApi; public interface IFluentApiPropertyMetadataBuilder { - IPropertyMetadata BuildPropertyMetadata(IObjectMetadata objectMetadata); + IPropertyMetadata BuildDefaultMetadata(IObjectMetadata objectMetadata); - IPropertyMetadata BuildPropertyMetadataFromPropertyInfo(IObjectMetadata objectMetadata, PropertyInfo propertyInfo); + IPropertyMetadata BuildDefaultMetadataForProperty(IObjectMetadata objectMetadata, PropertyInfo propertyInfo); } diff --git a/src/Strainer/Services/Metadata/ObjectMetadataBuilder.cs b/src/Strainer/Services/Metadata/ObjectMetadataBuilder.cs index 0ca4bc9c..00bce8c9 100644 --- a/src/Strainer/Services/Metadata/ObjectMetadataBuilder.cs +++ b/src/Strainer/Services/Metadata/ObjectMetadataBuilder.cs @@ -1,4 +1,5 @@ using Fluorite.Strainer.Models.Metadata; +using Fluorite.Strainer.Models.Sorting; using System.Linq.Expressions; using System.Reflection; @@ -24,7 +25,7 @@ public class ObjectMetadataBuilder : IObjectMetadataBuilder Save(Build()); } - protected bool IsDefaultSortingDescendingValue { get; set; } + protected SortingWay? DefaultSortingWay { get; set; } protected bool IsFilterableValue { get; set; } @@ -32,7 +33,7 @@ public class ObjectMetadataBuilder : IObjectMetadataBuilder public IObjectMetadata Build() { - return new ObjectMetadata(_defaultSortingPropertyName, IsDefaultSortingDescendingValue, _defaultSortingPropertyInfo) + return new ObjectMetadata(_defaultSortingPropertyName, _defaultSortingPropertyInfo, DefaultSortingWay) { IsFilterable = IsFilterableValue, IsSortable = IsSortableValue, @@ -57,7 +58,7 @@ public class ObjectMetadataBuilder : IObjectMetadataBuilder public IObjectMetadataBuilder IsDefaultSortingAscending() { - IsDefaultSortingDescendingValue = false; + DefaultSortingWay = SortingWay.Ascending; Save(Build()); return this; @@ -65,7 +66,7 @@ public class ObjectMetadataBuilder : IObjectMetadataBuilder public IObjectMetadataBuilder IsDefaultSortingDescending() { - IsDefaultSortingDescendingValue = true; + DefaultSortingWay = SortingWay.Descending; Save(Build()); return this; diff --git a/src/Strainer/Services/Metadata/PropertyMetadataBuilder.cs b/src/Strainer/Services/Metadata/PropertyMetadataBuilder.cs index 581a3964..2c1a8e94 100644 --- a/src/Strainer/Services/Metadata/PropertyMetadataBuilder.cs +++ b/src/Strainer/Services/Metadata/PropertyMetadataBuilder.cs @@ -1,5 +1,6 @@ using Fluorite.Strainer.Exceptions; using Fluorite.Strainer.Models.Metadata; +using Fluorite.Strainer.Models.Sorting; using Fluorite.Strainer.Services.Sorting; using System.Reflection; @@ -30,7 +31,7 @@ public class PropertyMetadataBuilder : IPropertyMetadataBuilder : IPropertyMetadataBuilder /// is . /// - /// - /// is . - /// public string Format(string input, SortingWay sortingWay) { Guard.Against.Null(input); - if (sortingWay == SortingWay.Unknown) - { - throw new ArgumentException( - $"{nameof(sortingWay)} cannot be {nameof(SortingWay.Unknown)}.", - nameof(sortingWay)); - } - if (string.IsNullOrWhiteSpace(input)) { return input; @@ -56,13 +46,13 @@ public class DescendingPrefixSortingWayFormatter : ISortingWayFormatter /// /// is . /// - public SortingWay GetSortingWay(string input) + public SortingWay? GetSortingWay(string input) { Guard.Against.Null(input); if (string.IsNullOrWhiteSpace(input)) { - return SortingWay.Unknown; + return null; } if (input.StartsWith(DescendingPrefix)) @@ -77,20 +67,10 @@ public class DescendingPrefixSortingWayFormatter : ISortingWayFormatter /// /// is . /// - /// - /// is . - /// public string Unformat(string input, SortingWay sortingWay) { Guard.Against.Null(input); - if (sortingWay == SortingWay.Unknown) - { - throw new ArgumentException( - $"{nameof(sortingWay)} cannot be {nameof(SortingWay.Unknown)}.", - nameof(sortingWay)); - } - if (string.IsNullOrEmpty(input)) { return input; diff --git a/src/Strainer/Services/Sorting/ISortingWayFormatter.cs b/src/Strainer/Services/Sorting/ISortingWayFormatter.cs index 8038b4ca..4e6f290a 100644 --- a/src/Strainer/Services/Sorting/ISortingWayFormatter.cs +++ b/src/Strainer/Services/Sorting/ISortingWayFormatter.cs @@ -32,11 +32,11 @@ public interface ISortingWayFormatter /// /// if the input is formatted in /// ascending way; if the input - /// is formatted in descending way; + /// is formatted in descending way; /// if the sorting way cannot be established (e.g. the input was /// ). /// - SortingWay GetSortingWay(string input); + SortingWay? GetSortingWay(string input); /// /// Removes sorting way formatting from provided input value. diff --git a/src/Strainer/Services/Sorting/SortExpressionProvider.cs b/src/Strainer/Services/Sorting/SortExpressionProvider.cs index c9b71254..b0a64511 100644 --- a/src/Strainer/Services/Sorting/SortExpressionProvider.cs +++ b/src/Strainer/Services/Sorting/SortExpressionProvider.cs @@ -1,5 +1,4 @@ -using Fluorite.Strainer.Exceptions; -using Fluorite.Strainer.Models.Metadata; +using Fluorite.Strainer.Models.Metadata; using Fluorite.Strainer.Models.Sorting; using Fluorite.Strainer.Models.Sorting.Terms; using Fluorite.Strainer.Services.Metadata; @@ -17,13 +16,17 @@ namespace Fluorite.Strainer.Services.Sorting; public class SortExpressionProvider : ISortExpressionProvider { private readonly IMetadataFacade _metadataProvidersFacade; + private readonly IStrainerOptionsProvider _strainerOptionsProvider; /// /// Initializes a new instance of the class. /// - public SortExpressionProvider(IMetadataFacade metadataProvidersFacade) + public SortExpressionProvider( + IMetadataFacade metadataProvidersFacade, + IStrainerOptionsProvider strainerOptionsProvider) { _metadataProvidersFacade = Guard.Against.Null(metadataProvidersFacade); + _strainerOptionsProvider = Guard.Against.Null(strainerOptionsProvider); } /// @@ -36,9 +39,12 @@ public class SortExpressionProvider : ISortExpressionProvider } var name = propertyMetadata.DisplayName ?? propertyMetadata.Name; + var defaultSortingWay = propertyMetadata.DefaultSortingWay + ?? _strainerOptionsProvider.GetStrainerOptions().DefaultSortingWay; + var isDescending = defaultSortingWay == SortingWay.Descending; var sortTerm = new SortTerm(name) { - IsDescending = propertyMetadata.IsDefaultSortingDescending, + IsDescending = isDescending, }; return GetExpression(propertyMetadata, sortTerm, isSubsequent: false); diff --git a/src/Strainer/Services/Sorting/SortPropertyMetadataBuilder.cs b/src/Strainer/Services/Sorting/SortPropertyMetadataBuilder.cs index e35dff67..63e6a06b 100644 --- a/src/Strainer/Services/Sorting/SortPropertyMetadataBuilder.cs +++ b/src/Strainer/Services/Sorting/SortPropertyMetadataBuilder.cs @@ -1,4 +1,5 @@ using Fluorite.Strainer.Models.Metadata; +using Fluorite.Strainer.Models.Sorting; using Fluorite.Strainer.Services.Metadata; using System.Reflection; @@ -18,7 +19,7 @@ public class SortPropertyMetadataBuilder : PropertyMetadataBuilder : PropertyMetadataBuilder IsDefaultSort(bool isDescending = false) { IsDefaultSorting = true; - IsDefaultSortingDescending = isDescending; + DefaultSortingWay = isDescending ? SortingWay.Descending : SortingWay.Ascending; Save(Build()); return this; diff --git a/src/Strainer/Services/Sorting/SortTermParser.cs b/src/Strainer/Services/Sorting/SortTermParser.cs index fffac126..a06e629b 100644 --- a/src/Strainer/Services/Sorting/SortTermParser.cs +++ b/src/Strainer/Services/Sorting/SortTermParser.cs @@ -42,12 +42,7 @@ public class SortTermParser : ISortTermParser continue; } - var sortingWay = _formatter.GetSortingWay(value); - if (sortingWay == SortingWay.Unknown) - { - sortingWay = options.DefaultSortingWay; - } - + var sortingWay = _formatter.GetSortingWay(value) ?? options.DefaultSortingWay; var name = _formatter.Unformat(value, sortingWay); var sortTerm = new SortTerm(name) { diff --git a/src/Strainer/Services/Sorting/SuffixSortingWayFormatter.cs b/src/Strainer/Services/Sorting/SuffixSortingWayFormatter.cs index aa2f0a5a..c0a1ead3 100644 --- a/src/Strainer/Services/Sorting/SuffixSortingWayFormatter.cs +++ b/src/Strainer/Services/Sorting/SuffixSortingWayFormatter.cs @@ -32,9 +32,6 @@ public class SuffixSortingWayFormatter : ISortingWayFormatter } /// - /// - /// is . - /// /// /// is . /// @@ -64,15 +61,10 @@ public class SuffixSortingWayFormatter : ISortingWayFormatter /// /// is . /// - public SortingWay GetSortingWay(string input) + public SortingWay? GetSortingWay(string input) { Guard.Against.Null(input); - if (string.IsNullOrWhiteSpace(input)) - { - return SortingWay.Unknown; - } - if (input.EndsWith(DescendingSuffix)) { return SortingWay.Descending; @@ -83,7 +75,7 @@ public class SuffixSortingWayFormatter : ISortingWayFormatter return SortingWay.Ascending; } - return SortingWay.Unknown; + return null; } /// @@ -97,13 +89,6 @@ public class SuffixSortingWayFormatter : ISortingWayFormatter { Guard.Against.Null(input); - if (sortingWay == SortingWay.Unknown) - { - throw new ArgumentException( - $"{nameof(sortingWay)} cannot be {nameof(SortingWay.Unknown)}.", - nameof(sortingWay)); - } - if (string.IsNullOrEmpty(input)) { return input; diff --git a/test/Strainer.UnitTests/Extensions/DepedencyInjection/StrainerServiceCollectionExtensionsTests.cs b/test/Strainer.UnitTests/Extensions/DepedencyInjection/StrainerServiceCollectionExtensionsTests.cs index 4647f8ff..18fc5315 100644 --- a/test/Strainer.UnitTests/Extensions/DepedencyInjection/StrainerServiceCollectionExtensionsTests.cs +++ b/test/Strainer.UnitTests/Extensions/DepedencyInjection/StrainerServiceCollectionExtensionsTests.cs @@ -405,7 +405,7 @@ public class StrainerServiceCollectionExtensionsTests throw new NotImplementedException(); } - public SortingWay GetSortingWay(string input) + public SortingWay? GetSortingWay(string input) { throw new NotImplementedException(); } diff --git a/test/Strainer.UnitTests/Services/Filtering/CustomFilteringExpressionProviderTests.cs b/test/Strainer.UnitTests/Services/Filtering/CustomFilteringExpressionProviderTests.cs index b3a1150c..1a284cfc 100644 --- a/test/Strainer.UnitTests/Services/Filtering/CustomFilteringExpressionProviderTests.cs +++ b/test/Strainer.UnitTests/Services/Filtering/CustomFilteringExpressionProviderTests.cs @@ -3,7 +3,6 @@ using Fluorite.Strainer.Models.Filtering; using Fluorite.Strainer.Models.Filtering.Terms; using Fluorite.Strainer.Services.Configuration; using Fluorite.Strainer.Services.Filtering; -using NSubstitute.ReturnsExtensions; using System.Linq.Expressions; namespace Fluorite.Strainer.UnitTests.Services.Filtering; diff --git a/test/Strainer.UnitTests/Services/Filtering/FilterExpressionProviderTests.cs b/test/Strainer.UnitTests/Services/Filtering/FilterExpressionProviderTests.cs index c561d78e..f08bd91a 100644 --- a/test/Strainer.UnitTests/Services/Filtering/FilterExpressionProviderTests.cs +++ b/test/Strainer.UnitTests/Services/Filtering/FilterExpressionProviderTests.cs @@ -2,7 +2,6 @@ using Fluorite.Strainer.Models.Metadata; using Fluorite.Strainer.Services.Conversion; using Fluorite.Strainer.Services.Filtering; -using NSubstitute.ReturnsExtensions; using System.Linq.Expressions; using System.Reflection; diff --git a/test/Strainer.UnitTests/Services/Filtering/FilterOperatorValidatorTests.cs b/test/Strainer.UnitTests/Services/Filtering/FilterOperatorValidatorTests.cs index b9837768..ec4408cd 100644 --- a/test/Strainer.UnitTests/Services/Filtering/FilterOperatorValidatorTests.cs +++ b/test/Strainer.UnitTests/Services/Filtering/FilterOperatorValidatorTests.cs @@ -2,7 +2,6 @@ using Fluorite.Strainer.Models.Filtering.Operators; using Fluorite.Strainer.Services.Filtering; using Fluorite.Strainer.Services.Validation; -using NSubstitute.ReturnsExtensions; using System.Linq.Expressions; namespace Fluorite.Strainer.UnitTests.Services.Filtering; diff --git a/test/Strainer.UnitTests/Services/Filtering/Steps/ChangeTypeOfFilterValueStepTests.cs b/test/Strainer.UnitTests/Services/Filtering/Steps/ChangeTypeOfFilterValueStepTests.cs index 9f0f2b63..a512d16a 100644 --- a/test/Strainer.UnitTests/Services/Filtering/Steps/ChangeTypeOfFilterValueStepTests.cs +++ b/test/Strainer.UnitTests/Services/Filtering/Steps/ChangeTypeOfFilterValueStepTests.cs @@ -4,7 +4,6 @@ using Fluorite.Strainer.Models.Metadata; using Fluorite.Strainer.Services.Conversion; using Fluorite.Strainer.Services.Filtering; using Fluorite.Strainer.Services.Filtering.Steps; -using System.ComponentModel; using System.Linq.Expressions; using System.Reflection; diff --git a/test/Strainer.UnitTests/Services/Metadata/Attributes/AttributeMetadataProviderTests.cs b/test/Strainer.UnitTests/Services/Metadata/Attributes/AttributeMetadataProviderTests.cs index b3bc3205..91585b56 100644 --- a/test/Strainer.UnitTests/Services/Metadata/Attributes/AttributeMetadataProviderTests.cs +++ b/test/Strainer.UnitTests/Services/Metadata/Attributes/AttributeMetadataProviderTests.cs @@ -2,7 +2,6 @@ using Fluorite.Strainer.Models.Metadata; using Fluorite.Strainer.Services.Metadata; using Fluorite.Strainer.Services.Metadata.Attributes; -using NSubstitute.ReturnsExtensions; namespace Fluorite.Strainer.UnitTests.Services.Metadata.Attributes; diff --git a/test/Strainer.UnitTests/Services/Metadata/Attributes/AttributeMetadataRetrieverTests.cs b/test/Strainer.UnitTests/Services/Metadata/Attributes/AttributeMetadataRetrieverTests.cs index 40104ffe..6b95454d 100644 --- a/test/Strainer.UnitTests/Services/Metadata/Attributes/AttributeMetadataRetrieverTests.cs +++ b/test/Strainer.UnitTests/Services/Metadata/Attributes/AttributeMetadataRetrieverTests.cs @@ -2,7 +2,6 @@ using Fluorite.Strainer.Models.Metadata; using Fluorite.Strainer.Services.Metadata; using Fluorite.Strainer.Services.Metadata.Attributes; -using NSubstitute.ReturnsExtensions; using System.Reflection; namespace Fluorite.Strainer.UnitTests.Services.Metadata.Attributes; @@ -91,7 +90,7 @@ public class AttributeMetadataRetrieverTests .GetPropertyInfo(modelType, objectAttribute.DefaultSortingPropertyName) .Returns(propertyInfo); _attributePropertyMetadataBuilderMock - .BuildDefaultPropertyMetadata(objectAttribute, propertyInfo) + .BuildDefaultMetadata(objectAttribute, propertyInfo) .Returns(defaultPropertyMetadata); // Act diff --git a/test/Strainer.UnitTests/Services/Metadata/Attributes/AttributePropertyMetadataBuilderTests.cs b/test/Strainer.UnitTests/Services/Metadata/Attributes/AttributePropertyMetadataBuilderTests.cs index 67cf1b7f..ad4cfe7d 100644 --- a/test/Strainer.UnitTests/Services/Metadata/Attributes/AttributePropertyMetadataBuilderTests.cs +++ b/test/Strainer.UnitTests/Services/Metadata/Attributes/AttributePropertyMetadataBuilderTests.cs @@ -1,4 +1,5 @@ using Fluorite.Strainer.Attributes; +using Fluorite.Strainer.Services; using Fluorite.Strainer.Services.Metadata.Attributes; using System.Reflection; @@ -6,11 +7,13 @@ namespace Fluorite.Strainer.UnitTests.Services.Metadata.Attributes; public class AttributePropertyMetadataBuilderTests { + private readonly IStrainerOptionsProvider _strainerOptionsProviderMock = Substitute.For(); + private readonly AttributePropertyMetadataBuilder _builder; public AttributePropertyMetadataBuilderTests() { - _builder = new(); + _builder = new(_strainerOptionsProviderMock); } [Fact] @@ -24,13 +27,13 @@ public class AttributePropertyMetadataBuilderTests propertyInfo.Name.Returns(propertyName); // Act - var result = _builder.BuildDefaultPropertyMetadata(attribute, propertyInfo); + var result = _builder.BuildDefaultMetadata(attribute, propertyInfo); // Assert result.Should().NotBeNull(); result.DisplayName.Should().BeNull(); result.IsDefaultSorting.Should().BeTrue(); - result.IsDefaultSortingDescending.Should().Be(attribute.IsDefaultSortingDescending); + result.DefaultSortingWay.Should().Be(attribute.DefaultSortingWay); result.IsFilterable.Should().Be(attribute.IsFilterable); result.IsSortable.Should().Be(attribute.IsSortable); result.Name.Should().Be(propertyName); diff --git a/test/Strainer.UnitTests/Services/Metadata/Attributes/StrainerAttributeProviderTests.cs b/test/Strainer.UnitTests/Services/Metadata/Attributes/StrainerAttributeProviderTests.cs index 07f2bb65..dbdadc7c 100644 --- a/test/Strainer.UnitTests/Services/Metadata/Attributes/StrainerAttributeProviderTests.cs +++ b/test/Strainer.UnitTests/Services/Metadata/Attributes/StrainerAttributeProviderTests.cs @@ -1,17 +1,24 @@ using Fluorite.Strainer.Attributes; +using Fluorite.Strainer.Services.Metadata; using Fluorite.Strainer.Services.Metadata.Attributes; -using NSubstitute.ReturnsExtensions; using System.Reflection; namespace Fluorite.Strainer.UnitTests.Services.Metadata.Attributes; public class StrainerAttributeProviderTests { + private readonly IPropertyInfoProvider _propertyInfoProviderMock = Substitute.For(); + private readonly StrainerAttributeProvider _provider; public StrainerAttributeProviderTests() { - _provider = new StrainerAttributeProvider(); + _provider = new StrainerAttributeProvider(_propertyInfoProviderMock); + } + + public interface ITypeFormatter + { + string GetPrettyTypeName(Type type); } [Fact] @@ -43,9 +50,13 @@ public class StrainerAttributeProviderTests // Arrange var objectAttribute = new StrainerObjectAttribute("foo"); var typeMock = Substitute.For(); + var defaultSortingPropertyInfo = Substitute.For(); typeMock .GetCustomAttributes(typeof(StrainerObjectAttribute), false) .Returns(new[] { objectAttribute }); + _propertyInfoProviderMock + .GetPropertyInfo(Arg.Is(x => ReferenceEquals(x, typeMock)), objectAttribute.DefaultSortingPropertyName) + .Returns(defaultSortingPropertyInfo); // Act var result = _provider.GetObjectAttribute(typeMock); @@ -53,6 +64,8 @@ public class StrainerAttributeProviderTests // Assert result.Should().NotBeNull(); result.Should().BeSameAs(objectAttribute); + result.DefaultSortingPropertyInfo.Should().NotBeNull(); + result.DefaultSortingPropertyInfo.Should().BeSameAs(defaultSortingPropertyInfo); } [Fact] diff --git a/test/Strainer.UnitTests/Services/Metadata/FluentApi/FluentApiMetadataProviderTests.cs b/test/Strainer.UnitTests/Services/Metadata/FluentApi/FluentApiMetadataProviderTests.cs index 45a488fd..c3c8dac0 100644 --- a/test/Strainer.UnitTests/Services/Metadata/FluentApi/FluentApiMetadataProviderTests.cs +++ b/test/Strainer.UnitTests/Services/Metadata/FluentApi/FluentApiMetadataProviderTests.cs @@ -67,7 +67,7 @@ public class FluentApiMetadataProviderTests .GetObjectMetadata() .Returns(objectMetadataDictionary); _propertyMetadataBuilderMock - .BuildPropertyMetadata(objectMetadata) + .BuildDefaultMetadata(objectMetadata) .Returns(propertyMetadata); // Act @@ -277,7 +277,7 @@ public class FluentApiMetadataProviderTests .GetPropertyInfo(typeof(Post), name) .Returns(propertyInfo); _propertyMetadataBuilderMock - .BuildPropertyMetadataFromPropertyInfo(objectMetadata, propertyInfo) + .BuildDefaultMetadataForProperty(objectMetadata, propertyInfo) .Returns(propertyMetadata); // Act @@ -327,7 +327,7 @@ public class FluentApiMetadataProviderTests .GetPropertyInfo(typeof(Post), name) .Returns(propertyInfo); _propertyMetadataBuilderMock - .BuildPropertyMetadataFromPropertyInfo(objectMetadata, propertyInfo) + .BuildDefaultMetadataForProperty(objectMetadata, propertyInfo) .Returns(propertyMetadata); // Act @@ -482,7 +482,7 @@ public class FluentApiMetadataProviderTests .GetPropertyInfos(typeof(Post)) .Returns(propertyInfos); _propertyMetadataBuilderMock - .BuildPropertyMetadataFromPropertyInfo(objectMetadata, propertyInfo) + .BuildDefaultMetadataForProperty(objectMetadata, propertyInfo) .Returns(propertyMetadata); // Act diff --git a/test/Strainer.UnitTests/Services/Metadata/FluentApi/FluentApiPropertyMetadataBuilderTests.cs b/test/Strainer.UnitTests/Services/Metadata/FluentApi/FluentApiPropertyMetadataBuilderTests.cs new file mode 100644 index 00000000..f90c2c5f --- /dev/null +++ b/test/Strainer.UnitTests/Services/Metadata/FluentApi/FluentApiPropertyMetadataBuilderTests.cs @@ -0,0 +1,109 @@ +using Fluorite.Strainer.Models; +using Fluorite.Strainer.Models.Metadata; +using Fluorite.Strainer.Models.Sorting; +using Fluorite.Strainer.Services; +using Fluorite.Strainer.Services.Metadata.FluentApi; +using System.Reflection; + +namespace Fluorite.Strainer.UnitTests.Services.Metadata.FluentApi; + +public class FluentApiPropertyMetadataBuilderTests +{ + private readonly IStrainerOptionsProvider _strainerOptionsProviderMock = Substitute.For(); + + private readonly FluentApiPropertyMetadataBuilder _builder; + + public FluentApiPropertyMetadataBuilderTests() + { + _builder = new FluentApiPropertyMetadataBuilder(_strainerOptionsProviderMock); + } + + [Fact] + public void Should_Return_DefaultMetadata() + { + // Arrange + var name = "foo"; + var propertyInfo = Substitute.For(); + var objectMetadata = Substitute.For(); + objectMetadata.DefaultSortingPropertyName.Returns(name); + objectMetadata.DefaultSortingPropertyInfo.Returns(propertyInfo); + + // Act + var result = _builder.BuildDefaultMetadata(objectMetadata); + + // Assert + result.Should().NotBeNull(); + result.IsDefaultSorting.Should().BeTrue(); + result.Should().BeEquivalentTo( + objectMetadata, + options => options + .WithMapping(e => e.DefaultSortingPropertyInfo, s => s.PropertyInfo) + .WithMapping(e => e.DefaultSortingPropertyName, s => s.Name)); + } + + [Theory] + [InlineData(SortingWay.Ascending)] + [InlineData(SortingWay.Descending)] + public void Should_Return_DefaultMetadataForProperty_UsingTheSamePropertyInfoAsDefaultProperty(SortingWay defaultSortingWay) + { + // Arrange + var name = "foo"; + var propertyInfo = Substitute.For(); + propertyInfo.Name.Returns(name); + var objectMetadata = Substitute.For(); + objectMetadata.DefaultSortingWay.Returns(defaultSortingWay); + objectMetadata.DefaultSortingPropertyName.Returns(name); + objectMetadata.DefaultSortingPropertyInfo.Returns(propertyInfo); + + // Act + var result = _builder.BuildDefaultMetadataForProperty(objectMetadata, propertyInfo); + + // Assert + result.Should().NotBeNull(); + result.IsDefaultSorting.Should().BeTrue(); + result.Should().BeEquivalentTo( + objectMetadata, + options => options + .WithMapping(e => e.DefaultSortingPropertyInfo, s => s.PropertyInfo) + .WithMapping(e => e.DefaultSortingPropertyName, s => s.Name)); + } + + [Theory] + [InlineData(SortingWay.Ascending)] + [InlineData(SortingWay.Descending)] + public void Should_Return_DefaultMetadataForProperty_UsingDifferentPropertyInfoAsDefaultProperty(SortingWay defaultSortingWay) + { + // Arrange + var name = "foo"; + var propertyInfo = Substitute.For(); + var differentPropertyInfo = Substitute.For(); + differentPropertyInfo.Name.Returns(name); + var objectMetadata = Substitute.For(); + objectMetadata.DefaultSortingPropertyName.Returns(name); + objectMetadata.DefaultSortingPropertyInfo.Returns(propertyInfo); + + var strainerOptions = new StrainerOptions + { + DefaultSortingWay = defaultSortingWay, + }; + + _strainerOptionsProviderMock + .GetStrainerOptions() + .Returns(strainerOptions); + + // Act + var result = _builder.BuildDefaultMetadataForProperty(objectMetadata, differentPropertyInfo); + + // Assert + result.Should().NotBeNull(); + result.IsDefaultSorting.Should().BeFalse(); + result.PropertyInfo.Should().BeSameAs(differentPropertyInfo); + result.DefaultSortingWay.Should().Be(strainerOptions.DefaultSortingWay); + result.Should().BeEquivalentTo( + objectMetadata, + options => options + .Excluding(e => e.DefaultSortingWay) + .Excluding(e => e.DefaultSortingPropertyInfo) + .WithMapping(e => e.DefaultSortingPropertyName, s => s.Name)); + } +} diff --git a/test/Strainer.UnitTests/Services/Metadata/MetadataFacadeTests.cs b/test/Strainer.UnitTests/Services/Metadata/MetadataFacadeTests.cs index 26054934..3e7ebfcf 100644 --- a/test/Strainer.UnitTests/Services/Metadata/MetadataFacadeTests.cs +++ b/test/Strainer.UnitTests/Services/Metadata/MetadataFacadeTests.cs @@ -1,7 +1,6 @@ using Fluorite.Extensions; using Fluorite.Strainer.Models.Metadata; using Fluorite.Strainer.Services.Metadata; -using NSubstitute.ReturnsExtensions; namespace Fluorite.Strainer.UnitTests.Services.Metadata; diff --git a/test/Strainer.UnitTests/Services/Metadata/ObjectMetadataBuilderTests.cs b/test/Strainer.UnitTests/Services/Metadata/ObjectMetadataBuilderTests.cs index 039d95b3..8cb910af 100644 --- a/test/Strainer.UnitTests/Services/Metadata/ObjectMetadataBuilderTests.cs +++ b/test/Strainer.UnitTests/Services/Metadata/ObjectMetadataBuilderTests.cs @@ -1,4 +1,5 @@ using Fluorite.Strainer.Models.Metadata; +using Fluorite.Strainer.Models.Sorting; using Fluorite.Strainer.Services.Metadata; using System.Linq.Expressions; @@ -31,7 +32,7 @@ public class ObjectMetadataBuilderTests objectMetadata.Keys.Should().BeEquivalentTo([typeof(Blog)]); objectMetadata.Values.First().DefaultSortingPropertyName.Should().Be(defaultSortingPropertyName); objectMetadata.Values.First().DefaultSortingPropertyInfo.Should().BeSameAs(defaultSortingPropertyInfo); - objectMetadata.Values.First().IsDefaultSortingDescending.Should().BeFalse(); + objectMetadata.Values.First().DefaultSortingWay.Should().BeNull(); objectMetadata.Values.First().IsFilterable.Should().BeFalse(); objectMetadata.Values.First().IsSortable.Should().BeFalse(); } @@ -62,7 +63,7 @@ public class ObjectMetadataBuilderTests objectMetadata.Keys.Should().BeEquivalentTo([typeof(Blog)]); objectMetadata.Values.First().DefaultSortingPropertyName.Should().Be(defaultSortingPropertyName); objectMetadata.Values.First().DefaultSortingPropertyInfo.Should().BeSameAs(defaultSortingPropertyInfo); - objectMetadata.Values.First().IsDefaultSortingDescending.Should().BeFalse(); + objectMetadata.Values.First().DefaultSortingWay.Should().BeNull(); objectMetadata.Values.First().IsFilterable.Should().BeTrue(); objectMetadata.Values.First().IsSortable.Should().BeFalse(); } @@ -93,7 +94,7 @@ public class ObjectMetadataBuilderTests objectMetadata.Keys.Should().BeEquivalentTo([typeof(Blog)]); objectMetadata.Values.First().DefaultSortingPropertyName.Should().Be(defaultSortingPropertyName); objectMetadata.Values.First().DefaultSortingPropertyInfo.Should().BeSameAs(defaultSortingPropertyInfo); - objectMetadata.Values.First().IsDefaultSortingDescending.Should().BeFalse(); + objectMetadata.Values.First().DefaultSortingWay.Should().BeNull(); objectMetadata.Values.First().IsFilterable.Should().BeFalse(); objectMetadata.Values.First().IsSortable.Should().BeTrue(); } @@ -124,7 +125,7 @@ public class ObjectMetadataBuilderTests objectMetadata.Keys.Should().BeEquivalentTo([typeof(Blog)]); objectMetadata.Values.First().DefaultSortingPropertyName.Should().Be(defaultSortingPropertyName); objectMetadata.Values.First().DefaultSortingPropertyInfo.Should().BeSameAs(defaultSortingPropertyInfo); - objectMetadata.Values.First().IsDefaultSortingDescending.Should().BeFalse(); + objectMetadata.Values.First().DefaultSortingWay.Should().Be(SortingWay.Ascending); objectMetadata.Values.First().IsFilterable.Should().BeFalse(); objectMetadata.Values.First().IsSortable.Should().BeFalse(); } @@ -155,7 +156,7 @@ public class ObjectMetadataBuilderTests objectMetadata.Keys.Should().BeEquivalentTo([typeof(Blog)]); objectMetadata.Values.First().DefaultSortingPropertyName.Should().Be(defaultSortingPropertyName); objectMetadata.Values.First().DefaultSortingPropertyInfo.Should().BeSameAs(defaultSortingPropertyInfo); - objectMetadata.Values.First().IsDefaultSortingDescending.Should().BeTrue(); + objectMetadata.Values.First().DefaultSortingWay.Should().Be(SortingWay.Descending); objectMetadata.Values.First().IsFilterable.Should().BeFalse(); objectMetadata.Values.First().IsSortable.Should().BeFalse(); } diff --git a/test/Strainer.UnitTests/Services/Metadata/PropertyMetadataBuilderTests.cs b/test/Strainer.UnitTests/Services/Metadata/PropertyMetadataBuilderTests.cs index d81d8dcf..319a110a 100644 --- a/test/Strainer.UnitTests/Services/Metadata/PropertyMetadataBuilderTests.cs +++ b/test/Strainer.UnitTests/Services/Metadata/PropertyMetadataBuilderTests.cs @@ -1,7 +1,6 @@ using Fluorite.Strainer.Exceptions; using Fluorite.Strainer.Models.Metadata; using Fluorite.Strainer.Services.Metadata; -using NSubstitute.ReturnsExtensions; namespace Fluorite.Strainer.UnitTests.Services.Metadata; @@ -31,7 +30,7 @@ public class PropertyMetadataBuilderTests propertyMetadata[type].Keys.Should().BeEquivalentTo(propertyName); propertyMetadata[type][propertyName].DisplayName.Should().BeNull(); propertyMetadata[type][propertyName].IsDefaultSorting.Should().BeFalse(); - propertyMetadata[type][propertyName].IsDefaultSortingDescending.Should().BeFalse(); + propertyMetadata[type][propertyName].DefaultSortingWay.Should().BeNull(); propertyMetadata[type][propertyName].IsFilterable.Should().BeFalse(); propertyMetadata[type][propertyName].IsSortable.Should().BeFalse(); propertyMetadata[type][propertyName].Name.Should().Be(propertyName); @@ -63,7 +62,7 @@ public class PropertyMetadataBuilderTests propertyMetadata[type].Keys.Should().BeEquivalentTo(propertyName); propertyMetadata[type][propertyName].DisplayName.Should().BeNull(); propertyMetadata[type][propertyName].IsDefaultSorting.Should().BeFalse(); - propertyMetadata[type][propertyName].IsDefaultSortingDescending.Should().BeFalse(); + propertyMetadata[type][propertyName].DefaultSortingWay.Should().BeNull(); propertyMetadata[type][propertyName].IsFilterable.Should().BeTrue(); propertyMetadata[type][propertyName].IsSortable.Should().BeFalse(); propertyMetadata[type][propertyName].Name.Should().Be(propertyName); @@ -95,7 +94,7 @@ public class PropertyMetadataBuilderTests propertyMetadata[type].Keys.Should().BeEquivalentTo(propertyName); propertyMetadata[type][propertyName].DisplayName.Should().BeNull(); propertyMetadata[type][propertyName].IsDefaultSorting.Should().BeFalse(); - propertyMetadata[type][propertyName].IsDefaultSortingDescending.Should().BeFalse(); + propertyMetadata[type][propertyName].DefaultSortingWay.Should().BeNull(); propertyMetadata[type][propertyName].IsFilterable.Should().BeFalse(); propertyMetadata[type][propertyName].IsSortable.Should().BeTrue(); propertyMetadata[type][propertyName].Name.Should().Be(propertyName); @@ -130,7 +129,7 @@ public class PropertyMetadataBuilderTests propertyMetadata[type].Keys.Should().BeEquivalentTo(displayName); propertyMetadata[type][displayName].DisplayName.Should().Be(displayName); propertyMetadata[type][displayName].IsDefaultSorting.Should().BeFalse(); - propertyMetadata[type][displayName].IsDefaultSortingDescending.Should().BeFalse(); + propertyMetadata[type][displayName].DefaultSortingWay.Should().BeNull(); propertyMetadata[type][displayName].IsFilterable.Should().BeFalse(); propertyMetadata[type][displayName].IsSortable.Should().BeFalse(); propertyMetadata[type][displayName].Name.Should().Be(propertyName); @@ -167,7 +166,7 @@ public class PropertyMetadataBuilderTests propertyMetadata[type].Keys.Should().BeEquivalentTo(displayName3); propertyMetadata[type][displayName3].DisplayName.Should().Be(displayName3); propertyMetadata[type][displayName3].IsDefaultSorting.Should().BeFalse(); - propertyMetadata[type][displayName3].IsDefaultSortingDescending.Should().BeFalse(); + propertyMetadata[type][displayName3].DefaultSortingWay.Should().BeNull(); propertyMetadata[type][displayName3].IsFilterable.Should().BeFalse(); propertyMetadata[type][displayName3].IsSortable.Should().BeFalse(); propertyMetadata[type][displayName3].Name.Should().Be(propertyName); diff --git a/test/Strainer.UnitTests/Services/Metadata/SortPropertyMetadataBuilderTests.cs b/test/Strainer.UnitTests/Services/Metadata/SortPropertyMetadataBuilderTests.cs index 15415b98..451e1788 100644 --- a/test/Strainer.UnitTests/Services/Metadata/SortPropertyMetadataBuilderTests.cs +++ b/test/Strainer.UnitTests/Services/Metadata/SortPropertyMetadataBuilderTests.cs @@ -1,6 +1,6 @@ using Fluorite.Strainer.Models.Metadata; +using Fluorite.Strainer.Models.Sorting; using Fluorite.Strainer.Services.Sorting; -using NSubstitute.ReturnsExtensions; namespace Fluorite.Strainer.UnitTests.Services.Metadata; @@ -33,7 +33,7 @@ public class SortPropertyMetadataBuilderTests propertyMetadata[type].Keys.Should().BeEquivalentTo(propertyName); propertyMetadata[type][propertyName].DisplayName.Should().BeNull(); propertyMetadata[type][propertyName].IsDefaultSorting.Should().BeFalse(); - propertyMetadata[type][propertyName].IsDefaultSortingDescending.Should().BeFalse(); + propertyMetadata[type][propertyName].DefaultSortingWay.Should().BeNull(); propertyMetadata[type][propertyName].IsFilterable.Should().BeFalse(); propertyMetadata[type][propertyName].IsSortable.Should().BeFalse(); propertyMetadata[type][propertyName].Name.Should().Be(propertyName); @@ -47,6 +47,7 @@ public class SortPropertyMetadataBuilderTests public void Should_Save_PropertyMetadata_WithDefaultSortingOption(bool isDescending) { // Arrange + var sortingWay = isDescending ? SortingWay.Descending : SortingWay.Ascending; var propertyMetadata = new Dictionary>(); var defaultMetadata = new Dictionary(); var type = typeof(Blog); @@ -71,7 +72,7 @@ public class SortPropertyMetadataBuilderTests propertyMetadata[type].Keys.Should().BeEquivalentTo(propertyName); propertyMetadata[type][propertyName].DisplayName.Should().BeNull(); propertyMetadata[type][propertyName].IsDefaultSorting.Should().BeTrue(); - propertyMetadata[type][propertyName].IsDefaultSortingDescending.Should().Be(isDescending); + propertyMetadata[type][propertyName].DefaultSortingWay.Should().Be(sortingWay); propertyMetadata[type][propertyName].IsFilterable.Should().BeFalse(); propertyMetadata[type][propertyName].IsSortable.Should().BeFalse(); propertyMetadata[type][propertyName].Name.Should().Be(propertyName); diff --git a/test/Strainer.UnitTests/Services/Pipelines/SortPipelineOperationTests.cs b/test/Strainer.UnitTests/Services/Pipelines/SortPipelineOperationTests.cs index 5e434d04..a9ce326d 100644 --- a/test/Strainer.UnitTests/Services/Pipelines/SortPipelineOperationTests.cs +++ b/test/Strainer.UnitTests/Services/Pipelines/SortPipelineOperationTests.cs @@ -3,7 +3,6 @@ using Fluorite.Strainer.Models.Sorting; using Fluorite.Strainer.Models.Sorting.Terms; using Fluorite.Strainer.Services.Pipelines; using Fluorite.Strainer.Services.Sorting; -using NSubstitute.ReturnsExtensions; namespace Fluorite.Strainer.UnitTests.Services.Pipelines; diff --git a/test/Strainer.UnitTests/Services/Sorting/CustomSortingExpressionProviderTests.cs b/test/Strainer.UnitTests/Services/Sorting/CustomSortingExpressionProviderTests.cs index 0c65df08..f3ed2d59 100644 --- a/test/Strainer.UnitTests/Services/Sorting/CustomSortingExpressionProviderTests.cs +++ b/test/Strainer.UnitTests/Services/Sorting/CustomSortingExpressionProviderTests.cs @@ -3,7 +3,6 @@ using Fluorite.Strainer.Models.Sorting; using Fluorite.Strainer.Models.Sorting.Terms; using Fluorite.Strainer.Services.Configuration; using Fluorite.Strainer.Services.Sorting; -using NSubstitute.ReturnsExtensions; using System.Linq.Expressions; namespace Fluorite.Strainer.UnitTests.Services.Sorting; diff --git a/test/Strainer.UnitTests/Services/Sorting/DescendingPrefixSortingWayFormatterTests.cs b/test/Strainer.UnitTests/Services/Sorting/DescendingPrefixSortingWayFormatterTests.cs index 1f55f521..0362aafc 100644 --- a/test/Strainer.UnitTests/Services/Sorting/DescendingPrefixSortingWayFormatterTests.cs +++ b/test/Strainer.UnitTests/Services/Sorting/DescendingPrefixSortingWayFormatterTests.cs @@ -28,21 +28,6 @@ public class DescendingPrefixSortingWayFormatterTests act.Should().ThrowExactly(); } - [Fact] - public void Formatter_Throws_ForUnkownSortingWay_WhenFormatting() - { - // Arrange - var input = string.Empty; - var sortingWay = SortingWay.Unknown; - - // Act - Action act = () => _formatter.Format(input, sortingWay); - - // Assert - act.Should().ThrowExactly() - .WithMessage($"{nameof(sortingWay)} cannot be {nameof(SortingWay.Unknown)}.*"); - } - [Theory] [InlineData("", SortingWay.Ascending, "")] [InlineData(" ", SortingWay.Ascending, " ")] @@ -70,11 +55,11 @@ public class DescendingPrefixSortingWayFormatterTests } [Theory] - [InlineData("", SortingWay.Unknown)] - [InlineData(" ", SortingWay.Unknown)] + [InlineData("", null)] + [InlineData(" ", null)] [InlineData("foo", SortingWay.Ascending)] [InlineData(DescendingPrefix + "foo", SortingWay.Descending)] - public void Formatter_Returns_CorrectSortingWay(string input, SortingWay sortingWay) + public void Formatter_Returns_CorrectSortingWay(string input, SortingWay? sortingWay) { // Act var result = _formatter.GetSortingWay(input); @@ -97,21 +82,6 @@ public class DescendingPrefixSortingWayFormatterTests act.Should().ThrowExactly(); } - [Fact] - public void Formatter_Throws_ForUnkownSortingWay_WhenUnformatting() - { - // Arrange - var input = string.Empty; - var sortingWay = SortingWay.Unknown; - - // Act - Action act = () => _formatter.Unformat(input, sortingWay); - - // Assert - act.Should().ThrowExactly() - .WithMessage($"{nameof(sortingWay)} cannot be {nameof(SortingWay.Unknown)}.*"); - } - [Theory] [InlineData("", "")] [InlineData(" ", " ")] diff --git a/test/Strainer.UnitTests/Services/Sorting/SortExpressionProviderTests.cs b/test/Strainer.UnitTests/Services/Sorting/SortExpressionProviderTests.cs index 775a1b87..99de9e5f 100644 --- a/test/Strainer.UnitTests/Services/Sorting/SortExpressionProviderTests.cs +++ b/test/Strainer.UnitTests/Services/Sorting/SortExpressionProviderTests.cs @@ -1,11 +1,11 @@ -using Fluorite.Strainer.Exceptions; +using Fluorite.Strainer.Models; using Fluorite.Strainer.Models.Metadata; +using Fluorite.Strainer.Models.Sorting; using Fluorite.Strainer.Models.Sorting.Terms; +using Fluorite.Strainer.Services; using Fluorite.Strainer.Services.Metadata; -using Fluorite.Strainer.Services.Metadata.Attributes; using Fluorite.Strainer.Services.Sorting; using NSubstitute.ReceivedExtensions; -using NSubstitute.ReturnsExtensions; using System.Reflection; namespace Fluorite.Strainer.UnitTests.Services.Sorting; @@ -13,12 +13,15 @@ namespace Fluorite.Strainer.UnitTests.Services.Sorting; public class SortExpressionProviderTests { private readonly IMetadataFacade _metadataProvidersFacadeMock = Substitute.For(); + private readonly IStrainerOptionsProvider _strainerOptionsProviderMock = Substitute.For(); private readonly SortExpressionProvider _provider; public SortExpressionProviderTests() { - _provider = new SortExpressionProvider(_metadataProvidersFacadeMock); + _provider = new SortExpressionProvider( + _metadataProvidersFacadeMock, + _strainerOptionsProviderMock); } [Fact] @@ -46,11 +49,53 @@ public class SortExpressionProviderTests // Arrange var name = nameof(Comment.Id); var propertyInfo = typeof(Comment).GetProperty(name); + var sortingWay = SortingWay.Descending; + var defaultMetadata = Substitute.For(); + defaultMetadata.PropertyInfo.Returns(propertyInfo); + defaultMetadata.DisplayName.ReturnsNull(); + defaultMetadata.IsDefaultSorting.Returns(false); + defaultMetadata.DefaultSortingWay.ReturnsNull(); + defaultMetadata.Name.Returns(name); + + var strainerOptions = new StrainerOptions + { + DefaultSortingWay = sortingWay, + }; + + _metadataProvidersFacadeMock + .GetDefaultMetadata() + .Returns(defaultMetadata); + _strainerOptionsProviderMock + .GetStrainerOptions() + .Returns(strainerOptions); + + // Act + var result = _provider.GetDefaultExpression(); + + // Assert + result.Should().NotBeNull(); + result.Expression.Should().NotBeNull(); + result.IsDefault.Should().BeFalse(); + result.IsDescending.Should().Be(sortingWay == SortingWay.Descending); + result.IsSubsequent.Should().BeFalse(); + + _metadataProvidersFacadeMock + .Received(1) + .GetDefaultMetadata(); + } + + [Fact] + public void Provider_Returns_DefaultExpression_WithSortingWayFromProperty() + { + // Arrange + var name = nameof(Comment.Id); + var propertyInfo = typeof(Comment).GetProperty(name); + var sortingWay = SortingWay.Ascending; var defaultMetadata = Substitute.For(); defaultMetadata.PropertyInfo.Returns(propertyInfo); defaultMetadata.DisplayName.ReturnsNull(); defaultMetadata.IsDefaultSorting.Returns(true); - defaultMetadata.IsDefaultSortingDescending.Returns(true); + defaultMetadata.DefaultSortingWay.Returns(sortingWay); defaultMetadata.Name.Returns(name); _metadataProvidersFacadeMock @@ -64,12 +109,15 @@ public class SortExpressionProviderTests result.Should().NotBeNull(); result.Expression.Should().NotBeNull(); result.IsDefault.Should().BeTrue(); - result.IsDescending.Should().Be(defaultMetadata.IsDefaultSortingDescending); + result.IsDescending.Should().Be(sortingWay == SortingWay.Descending); result.IsSubsequent.Should().BeFalse(); _metadataProvidersFacadeMock .Received(1) .GetDefaultMetadata(); + _strainerOptionsProviderMock + .DidNotReceive() + .GetStrainerOptions(); } [Fact] diff --git a/test/Strainer.UnitTests/Services/Sorting/SortTermParserTests.cs b/test/Strainer.UnitTests/Services/Sorting/SortTermParserTests.cs index 17ee0195..66e780df 100644 --- a/test/Strainer.UnitTests/Services/Sorting/SortTermParserTests.cs +++ b/test/Strainer.UnitTests/Services/Sorting/SortTermParserTests.cs @@ -93,7 +93,6 @@ public class SortTermParserTests var input = "foo"; var parsedInput = "parsed"; var formattedValue = "bar"; - var sortingWay = SortingWay.Unknown; var defaultSortingWay = SortingWay.Ascending; var options = new StrainerOptions { @@ -105,7 +104,7 @@ public class SortTermParserTests .Returns([parsedInput]); _sortingWayFormatterMock .GetSortingWay(parsedInput) - .Returns(sortingWay); + .ReturnsNull(); _sortingWayFormatterMock .Unformat(parsedInput, defaultSortingWay) .Returns(formattedValue); @@ -121,7 +120,7 @@ public class SortTermParserTests sortTermList.Should().HaveCount(1); sortTermList[0].Should().NotBeNull(); sortTermList[0].Input.Should().Be(parsedInput); - sortTermList[0].IsDescending.Should().Be(sortingWay == SortingWay.Descending); + sortTermList[0].IsDescending.Should().Be(defaultSortingWay == SortingWay.Descending); sortTermList[0].Name.Should().Be(formattedValue); } } diff --git a/test/Strainer.UnitTests/Services/Sorting/SortingApplierTests.cs b/test/Strainer.UnitTests/Services/Sorting/SortingApplierTests.cs index 25d22c1d..48137978 100644 --- a/test/Strainer.UnitTests/Services/Sorting/SortingApplierTests.cs +++ b/test/Strainer.UnitTests/Services/Sorting/SortingApplierTests.cs @@ -7,7 +7,6 @@ using Fluorite.Strainer.Services; using Fluorite.Strainer.Services.Metadata; using Fluorite.Strainer.Services.Sorting; using NSubstitute.ReceivedExtensions; -using NSubstitute.ReturnsExtensions; using System.Linq.Expressions; namespace Fluorite.Strainer.UnitTests.Services.Sorting; diff --git a/test/Strainer.UnitTests/Services/Sorting/SuffixSortingWayFormatterTests.cs b/test/Strainer.UnitTests/Services/Sorting/SuffixSortingWayFormatterTests.cs index 46436abd..59fa606b 100644 --- a/test/Strainer.UnitTests/Services/Sorting/SuffixSortingWayFormatterTests.cs +++ b/test/Strainer.UnitTests/Services/Sorting/SuffixSortingWayFormatterTests.cs @@ -29,21 +29,6 @@ public class SuffixSortingWayFormatterTests act.Should().ThrowExactly(); } - [Fact] - public void Formatter_Throws_ForUnkownSortingWay_WhenFormatting() - { - // Arrange - var input = "foo"; - var sortingWay = SortingWay.Unknown; - - // Act - Action act = () => _formatter.Format(input, sortingWay); - - // Assert - act.Should().ThrowExactly() - .WithMessage($"{nameof(sortingWay)} with value '{sortingWay}' is not supported."); - } - [Theory] [InlineData("", SortingWay.Ascending, "")] [InlineData(" ", SortingWay.Ascending, " ")] @@ -63,23 +48,19 @@ public class SuffixSortingWayFormatterTests [Fact] public void Formatter_Throws_ForNullInput_WhenGettingSortingWay() { - // Arrange - string input = null; - // Act - Action act = () => _formatter.GetSortingWay(input); + Action act = () => _formatter.GetSortingWay(input: null); // Assert act.Should().ThrowExactly(); } [Theory] - [InlineData("", SortingWay.Unknown)] - [InlineData(" ", SortingWay.Unknown)] - [InlineData("foo", SortingWay.Unknown)] + [InlineData("", null)] + [InlineData(" ", null)] [InlineData("foo" + AscendingSuffix, SortingWay.Ascending)] [InlineData("foo" + DescendingSuffix, SortingWay.Descending)] - public void Formatter_Returns_CorrectSortingWay(string input, SortingWay sortingWay) + public void Formatter_Returns_CorrectSortingWay(string input, SortingWay? sortingWay) { // Act var result = _formatter.GetSortingWay(input); @@ -102,21 +83,6 @@ public class SuffixSortingWayFormatterTests act.Should().ThrowExactly(); } - [Fact] - public void Formatter_Throws_ForUnkownSortingWay_WhenUnformatting() - { - // Arrange - var input = string.Empty; - var sortingWay = SortingWay.Unknown; - - // Act - Action act = () => _formatter.Unformat(input, sortingWay); - - // Assert - act.Should().ThrowExactly() - .WithMessage($"{nameof(sortingWay)} cannot be {nameof(SortingWay.Unknown)}.*"); - } - [Theory] [InlineData("", SortingWay.Ascending, "")] [InlineData(" ", SortingWay.Ascending, " ")] diff --git a/test/Strainer.UnitTests/_GlobalUsings.cs b/test/Strainer.UnitTests/_GlobalUsings.cs index 9987e03a..a58c13f6 100644 --- a/test/Strainer.UnitTests/_GlobalUsings.cs +++ b/test/Strainer.UnitTests/_GlobalUsings.cs @@ -1,5 +1,6 @@ global using FluentAssertions; global using NSubstitute; +global using NSubstitute.ReturnsExtensions; global using System; global using System.Collections.Generic; global using System.Linq; -- GitLab From b9bd3831dc32839727037253ee8d05853785d525 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sun, 19 Jan 2025 18:59:53 +0100 Subject: [PATCH 37/56] Add NSubstitute.Analyzers package --- test/Strainer.UnitTests/Strainer.UnitTests.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/Strainer.UnitTests/Strainer.UnitTests.csproj b/test/Strainer.UnitTests/Strainer.UnitTests.csproj index ddec23ac..1011c22a 100644 --- a/test/Strainer.UnitTests/Strainer.UnitTests.csproj +++ b/test/Strainer.UnitTests/Strainer.UnitTests.csproj @@ -19,6 +19,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive -- GitLab From 214ce3cfd258dd383389759d1c65bbe6a63db1be Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Mon, 27 Jan 2025 02:20:38 +0100 Subject: [PATCH 38/56] Conditionally support overloads accepting string comparison depending on context --- StyleCop.ruleset | 29 ++ .../StrainerServiceCollectionExtensions.cs | 2 + .../Operators/FilterExpressionContext.cs | 30 +- .../Operators/IFilterExpressionContext.cs | 21 + src/Strainer/Models/StrainerOptions.cs | 2 +- .../Filtering/FilterExpressionProvider.cs | 15 +- .../FilterExpressionWorkflowContext.cs | 2 + .../Filtering/FilterOperatorMapper.cs | 390 ++++++++++++++---- .../Filtering/IFilterExpressionProvider.cs | 3 +- .../Steps/ApplyFilterOperatorStep.cs | 16 +- .../Steps/MitigateCaseInsensitivityStep.cs | 5 + .../Services/Linq/IQueryableEvaluator.cs | 6 + .../Services/Linq/QueryableEvaluator.cs | 24 ++ .../Pipelines/FilterPipelineOperation.cs | 7 +- .../ContainsCaseInsensitiveOperatorTests.cs | 2 +- .../FilterExpressionProviderTests.cs | 21 +- .../Filtering/FilterOperatorMapperTests.cs | 15 +- .../Steps/ApplyFilterOperatorStepTests.cs | 19 +- .../Services/Linq/QueryableEvaluatorTests.cs | 52 +++ .../Metadata/PropertyInfoProviderTests.cs | 10 +- .../Pipelines/FilterPipelineOperationTests.cs | 72 +++- 21 files changed, 608 insertions(+), 135 deletions(-) create mode 100644 src/Strainer/Services/Linq/IQueryableEvaluator.cs create mode 100644 src/Strainer/Services/Linq/QueryableEvaluator.cs create mode 100644 test/Strainer.UnitTests/Services/Linq/QueryableEvaluatorTests.cs diff --git a/StyleCop.ruleset b/StyleCop.ruleset index 19d66e56..7f3c0343 100644 --- a/StyleCop.ruleset +++ b/StyleCop.ruleset @@ -576,6 +576,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Strainer.AspNetCore/Extensions/DependencyInjection/StrainerServiceCollectionExtensions.cs b/src/Strainer.AspNetCore/Extensions/DependencyInjection/StrainerServiceCollectionExtensions.cs index c3c300d8..4c368a2e 100644 --- a/src/Strainer.AspNetCore/Extensions/DependencyInjection/StrainerServiceCollectionExtensions.cs +++ b/src/Strainer.AspNetCore/Extensions/DependencyInjection/StrainerServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ using Fluorite.Strainer.Services.Configuration; using Fluorite.Strainer.Services.Conversion; using Fluorite.Strainer.Services.Filtering; using Fluorite.Strainer.Services.Filtering.Steps; +using Fluorite.Strainer.Services.Linq; using Fluorite.Strainer.Services.Metadata; using Fluorite.Strainer.Services.Metadata.Attributes; using Fluorite.Strainer.Services.Metadata.FluentApi; @@ -441,6 +442,7 @@ public static class StrainerServiceCollectionExtensions services.TryAdd(serviceLifetime); } + services.TryAdd(serviceLifetime); services.TryAdd(serviceLifetime); services.TryAdd(serviceLifetime); services.TryAdd(serviceLifetime); diff --git a/src/Strainer/Models/Filtering/Operators/FilterExpressionContext.cs b/src/Strainer/Models/Filtering/Operators/FilterExpressionContext.cs index 76c874e2..156de9cb 100644 --- a/src/Strainer/Models/Filtering/Operators/FilterExpressionContext.cs +++ b/src/Strainer/Models/Filtering/Operators/FilterExpressionContext.cs @@ -17,21 +17,49 @@ public class FilterExpressionContext : IFilterExpressionContext /// /// The property access expression. /// + /// + /// Gets a value indicating whether Strainer should operate in case insensitive mode + /// when comparing values. + /// + /// + /// A value indicating whether associated + /// is in fact a already materialized collection. + /// + /// + /// Gets a value indicating whether targeted property is string based. + /// /// /// is . /// /// /// is . /// - public FilterExpressionContext(Expression filterValue, Expression propertyValue) + public FilterExpressionContext( + Expression filterValue, + Expression propertyValue, + bool isCaseInsensitiveForValues, + bool isMaterializedQueryable, + bool isStringBasedProperty) { FilterValue = Guard.Against.Null(filterValue); PropertyValue = Guard.Against.Null(propertyValue); + IsCaseInsensitiveForValues = isCaseInsensitiveForValues; + IsMaterializedQueryable = isMaterializedQueryable; + IsStringBasedProperty = isStringBasedProperty; } /// public Expression FilterValue { get; } + /// + public bool IsCaseInsensitiveForValues { get; } + + /// + public bool IsMaterializedQueryable { get; } + + /// + public bool IsStringBasedProperty { get; } + /// public Expression PropertyValue { get; } } diff --git a/src/Strainer/Models/Filtering/Operators/IFilterExpressionContext.cs b/src/Strainer/Models/Filtering/Operators/IFilterExpressionContext.cs index ae62c8c3..c0c56046 100644 --- a/src/Strainer/Models/Filtering/Operators/IFilterExpressionContext.cs +++ b/src/Strainer/Models/Filtering/Operators/IFilterExpressionContext.cs @@ -12,6 +12,27 @@ public interface IFilterExpressionContext /// Expression FilterValue { get; } + /// + /// Gets a value indicating whether Strainer should operate in case insensitive mode + /// when comparing values. + /// + /// Value taken from global . + /// + public bool IsCaseInsensitiveForValues { get; } + + /// + /// Gets a value indicating whether associated + /// is in fact an already materialized collection. In such case database + /// collation will not work and other provider-specific + /// features, such as EF functions. + /// + bool IsMaterializedQueryable { get; } + + /// + /// Gets a value indicating whether targeted property is string based. + /// + bool IsStringBasedProperty { get; } + /// /// Gets the expression for property value access. /// diff --git a/src/Strainer/Models/StrainerOptions.cs b/src/Strainer/Models/StrainerOptions.cs index 6b6c85a5..a67d7b2b 100644 --- a/src/Strainer/Models/StrainerOptions.cs +++ b/src/Strainer/Models/StrainerOptions.cs @@ -40,7 +40,7 @@ public class StrainerOptions /// /// Gets or sets a value indicating whether - /// Strainer should operatre in case insensitive mode when comparing values. + /// Strainer should operate in case insensitive mode when comparing values. /// /// This for example affects the way of comparing value of incoming filter /// with actual property value. diff --git a/src/Strainer/Services/Filtering/FilterExpressionProvider.cs b/src/Strainer/Services/Filtering/FilterExpressionProvider.cs index 37c9d174..299b6da3 100644 --- a/src/Strainer/Services/Filtering/FilterExpressionProvider.cs +++ b/src/Strainer/Services/Filtering/FilterExpressionProvider.cs @@ -1,20 +1,16 @@ using Fluorite.Strainer.Models.Filtering.Terms; using Fluorite.Strainer.Models.Metadata; -using Fluorite.Strainer.Services.Conversion; using System.Linq.Expressions; namespace Fluorite.Strainer.Services.Filtering; public class FilterExpressionProvider : IFilterExpressionProvider { - private readonly ITypeConverterProvider _typeConverterProvider; private readonly IFilterExpressionWorkflowBuilder _filterExpressionWorkflowBuilder; public FilterExpressionProvider( - ITypeConverterProvider typeConverterProvider, IFilterExpressionWorkflowBuilder filterExpressionWorkflowBuilder) { - _typeConverterProvider = Guard.Against.Null(typeConverterProvider); _filterExpressionWorkflowBuilder = Guard.Against.Null(filterExpressionWorkflowBuilder); } @@ -22,7 +18,8 @@ public class FilterExpressionProvider : IFilterExpressionProvider IPropertyMetadata metadata, IFilterTerm filterTerm, ParameterExpression parameterExpression, - Expression? innerExpression) + Expression? innerExpression, + bool isMaterializedQueryable) { Guard.Against.Null(metadata); Guard.Against.Null(filterTerm); @@ -50,16 +47,17 @@ public class FilterExpressionProvider : IFilterExpressionProvider metadata, filterTerm, innerExpression, - propertyExpresssion); + propertyExpresssion, + isMaterializedQueryable); } private Expression? CreateInnerExpression( IPropertyMetadata metadata, IFilterTerm filterTerm, Expression? innerExpression, - MemberExpression? propertyExpression) + MemberExpression? propertyExpression, + bool isMaterializedQueryable) { - var typeConverter = _typeConverterProvider.GetTypeConverter(metadata.PropertyInfo!.PropertyType); var workflow = _filterExpressionWorkflowBuilder.BuildDefaultWorkflow(); foreach (var filterTermValue in filterTerm.Values) @@ -69,6 +67,7 @@ public class FilterExpressionProvider : IFilterExpressionProvider FilterTermConstant = filterTermValue, FilterTermValue = filterTermValue, FinalExpression = null, + IsMaterializedQueryable = isMaterializedQueryable, PropertyMetadata = metadata, PropertyValue = propertyExpression, Term = filterTerm, diff --git a/src/Strainer/Services/Filtering/FilterExpressionWorkflowContext.cs b/src/Strainer/Services/Filtering/FilterExpressionWorkflowContext.cs index 2f0d1c20..dd8f4e04 100644 --- a/src/Strainer/Services/Filtering/FilterExpressionWorkflowContext.cs +++ b/src/Strainer/Services/Filtering/FilterExpressionWorkflowContext.cs @@ -15,6 +15,8 @@ public class FilterExpressionWorkflowContext public Expression? FinalExpression { get; set; } + public bool IsMaterializedQueryable { get; set; } + public IPropertyMetadata? PropertyMetadata { get; set; } public Expression? PropertyValue { get; set; } diff --git a/src/Strainer/Services/Filtering/FilterOperatorMapper.cs b/src/Strainer/Services/Filtering/FilterOperatorMapper.cs index b99c48a0..971aca39 100644 --- a/src/Strainer/Services/Filtering/FilterOperatorMapper.cs +++ b/src/Strainer/Services/Filtering/FilterOperatorMapper.cs @@ -38,12 +38,50 @@ public static class FilterOperatorMapper new FilterOperatorBuilder() .HasSymbol(FilterOperatorSymbols.EqualsSymbol) .HasName("equal") - .HasExpression((context) => Expression.Equal(context.PropertyValue, context.FilterValue)) + .HasExpression((context) => + { + if (context.IsMaterializedQueryable && context.IsCaseInsensitiveForValues && context.IsStringBasedProperty) + { + return Expression.Call( + typeof(string).GetMethod( + nameof(string.Equals), + BindingFlags.Static | BindingFlags.Public, + null, + new Type[] { typeof(string), typeof(string), typeof(StringComparison) }, + null), + context.PropertyValue, + context.FilterValue, + Expression.Constant(StringComparison.OrdinalIgnoreCase)); + } + else + { + return Expression.Equal(context.PropertyValue, context.FilterValue); + } + }) .Build(), new FilterOperatorBuilder() .HasSymbol(FilterOperatorSymbols.DoesNotEqual) .HasName("does not equal") - .HasExpression((context) => Expression.NotEqual(context.PropertyValue, context.FilterValue)) + .HasExpression((context) => + { + if (context.IsMaterializedQueryable && context.IsCaseInsensitiveForValues && context.IsStringBasedProperty) + { + return Expression.Not(Expression.Call( + typeof(string).GetMethod( + nameof(string.Equals), + BindingFlags.Static | BindingFlags.Public, + null, + new Type[] { typeof(string), typeof(string), typeof(StringComparison) }, + null), + context.PropertyValue, + context.FilterValue, + Expression.Constant(StringComparison.OrdinalIgnoreCase))); + } + else + { + return Expression.NotEqual(context.PropertyValue, context.FilterValue); + } + }) .Build(), }; } @@ -85,28 +123,70 @@ public static class FilterOperatorMapper new FilterOperatorBuilder() .HasSymbol(FilterOperatorSymbols.Contains) .HasName("contains") - .HasExpression((context) => Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string) }), - context.FilterValue)) + .HasExpression((context) => + { + if (context.IsMaterializedQueryable && context.IsCaseInsensitiveForValues) + { + return Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string), typeof(StringComparison) }), + context.FilterValue, + Expression.Constant(StringComparison.OrdinalIgnoreCase)); + } + else + { + return Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string) }), + context.FilterValue); + } + }) .IsStringBased() .Build(), new FilterOperatorBuilder() .HasSymbol(FilterOperatorSymbols.StartsWith) .HasName("starts with") - .HasExpression((context) => Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string) }), - context.FilterValue)) + .HasExpression((context) => + { + if (context.IsMaterializedQueryable && context.IsCaseInsensitiveForValues) + { + return Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string), typeof(StringComparison) }), + context.FilterValue, + Expression.Constant(StringComparison.OrdinalIgnoreCase)); + } + else + { + return Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string) }), + context.FilterValue); + } + }) .IsStringBased() .Build(), new FilterOperatorBuilder() .HasSymbol(FilterOperatorSymbols.EndsWith) .HasName("ends with") - .HasExpression((context) => Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string) }), - context.FilterValue)) + .HasExpression((context) => + { + if (context.IsMaterializedQueryable && context.IsCaseInsensitiveForValues) + { + return Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string), typeof(StringComparison) }), + context.FilterValue, + Expression.Constant(StringComparison.OrdinalIgnoreCase)); + } + else + { + return Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string) }), + context.FilterValue); + } + }) .IsStringBased() .Build(), }; @@ -119,28 +199,70 @@ public static class FilterOperatorMapper new FilterOperatorBuilder() .HasSymbol(FilterOperatorSymbols.DoesNotContain) .HasName("does not contain") - .HasExpression((context) => Expression.Not(Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string) }), - context.FilterValue))) + .HasExpression((context) => + { + if (context.IsMaterializedQueryable && context.IsCaseInsensitiveForValues) + { + return Expression.Not(Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string), typeof(StringComparison) }), + context.FilterValue, + Expression.Constant(StringComparison.OrdinalIgnoreCase))); + } + else + { + return Expression.Not(Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string) }), + context.FilterValue)); + } + }) .IsStringBased() .Build(), new FilterOperatorBuilder() .HasSymbol(FilterOperatorSymbols.DoesNotStartWith) .HasName("does not start with") - .HasExpression((context) => Expression.Not(Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string) }), - context.FilterValue))) + .HasExpression((context) => + { + if (context.IsMaterializedQueryable && context.IsCaseInsensitiveForValues) + { + return Expression.Not(Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string), typeof(StringComparison) }), + context.FilterValue, + Expression.Constant(StringComparison.OrdinalIgnoreCase))); + } + else + { + return Expression.Not(Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string) }), + context.FilterValue)); + } + }) .IsStringBased() .Build(), new FilterOperatorBuilder() .HasSymbol(FilterOperatorSymbols.DoesNotEndWith) .HasName("does not end with") - .HasExpression((context) => Expression.Not(Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string) }), - context.FilterValue))) + .HasExpression((context) => + { + if (context.IsMaterializedQueryable && context.IsCaseInsensitiveForValues) + { + return Expression.Not(Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string), typeof(StringComparison) }), + context.FilterValue, + Expression.Constant(StringComparison.OrdinalIgnoreCase))); + } + else + { + return Expression.Not(Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string) }), + context.FilterValue)); + } + }) .IsStringBased() .Build(), }; @@ -153,32 +275,68 @@ public static class FilterOperatorMapper new FilterOperatorBuilder() .HasSymbol(FilterOperatorSymbols.EqualsCaseInsensitive) .HasName("equal (case insensitive)") - .HasExpression((context) => Expression.Call( - typeof(string).GetMethod( - nameof(string.Equals), - BindingFlags.Static | BindingFlags.Public, - null, - new Type[] { typeof(string), typeof(string), typeof(StringComparison) }, - null), - context.PropertyValue, - context.FilterValue, - Expression.Constant(StringComparison.OrdinalIgnoreCase))) + .HasExpression((context) => + { + if (context.IsMaterializedQueryable) + { + return Expression.Call( + typeof(string).GetMethod( + nameof(string.Equals), + BindingFlags.Static | BindingFlags.Public, + null, + new Type[] { typeof(string), typeof(string), typeof(StringComparison) }, + null), + context.PropertyValue, + context.FilterValue, + Expression.Constant(StringComparison.OrdinalIgnoreCase)); + } + else + { + return Expression.Call( + typeof(string).GetMethod( + nameof(string.Equals), + BindingFlags.Static | BindingFlags.Public, + null, + new Type[] { typeof(string), typeof(string) }, + null), + context.PropertyValue, + context.FilterValue); + } + }) .IsStringBased() .IsCaseInsensitive() .Build(), new FilterOperatorBuilder() .HasSymbol(FilterOperatorSymbols.DoesNotEqualCaseInsensitive) .HasName("does not equal (case insensitive)") - .HasExpression((context) => Expression.Not(Expression.Call( - typeof(string).GetMethod( - nameof(string.Equals), - BindingFlags.Static | BindingFlags.Public, - null, - new Type[] { typeof(string), typeof(string), typeof(StringComparison) }, - null), - context.PropertyValue, - context.FilterValue, - Expression.Constant(StringComparison.OrdinalIgnoreCase)))) + .HasExpression((context) => + { + if (context.IsMaterializedQueryable) + { + return Expression.Not(Expression.Call( + typeof(string).GetMethod( + nameof(string.Equals), + BindingFlags.Static | BindingFlags.Public, + null, + new Type[] { typeof(string), typeof(string), typeof(StringComparison) }, + null), + context.PropertyValue, + context.FilterValue, + Expression.Constant(StringComparison.OrdinalIgnoreCase))); + } + else + { + return Expression.Not(Expression.Call( + typeof(string).GetMethod( + nameof(string.Equals), + BindingFlags.Static | BindingFlags.Public, + null, + new Type[] { typeof(string), typeof(string) }, + null), + context.PropertyValue, + context.FilterValue)); + } + }) .IsStringBased() .IsCaseInsensitive() .Build(), @@ -194,15 +352,21 @@ public static class FilterOperatorMapper .HasName("contains (case insensitive)") .HasExpression((context) => { -#if NETSTANDARD2_0 - return Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string), typeof(StringComparison) }), - context.FilterValue, - Expression.Constant(StringComparison.OrdinalIgnoreCase)); -#else - throw new NotSupportedException("Runtime version does not support case-insensitive contains method."); -#endif + if (context.IsMaterializedQueryable) + { + return Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string), typeof(StringComparison) }), + context.FilterValue, + Expression.Constant(StringComparison.OrdinalIgnoreCase)); + } + else + { + return Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string), typeof(StringComparison) }), + context.FilterValue); + } }) .IsStringBased() .IsCaseInsensitive() @@ -210,22 +374,48 @@ public static class FilterOperatorMapper new FilterOperatorBuilder() .HasSymbol(FilterOperatorSymbols.StartsWithCaseInsensitive) .HasName("starts with (case insensitive)") - .HasExpression((context) => Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string), typeof(StringComparison) }), - context.FilterValue, - Expression.Constant(StringComparison.OrdinalIgnoreCase))) + .HasExpression((context) => + { + if (context.IsMaterializedQueryable) + { + return Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string), typeof(StringComparison) }), + context.FilterValue, + Expression.Constant(StringComparison.OrdinalIgnoreCase)); + } + else + { + return Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string), typeof(StringComparison) }), + context.FilterValue); + } + }) .IsStringBased() .IsCaseInsensitive() .Build(), new FilterOperatorBuilder() .HasSymbol(FilterOperatorSymbols.EndsWithCaseInsensitive) .HasName("ends with (case insensitive)") - .HasExpression((context) => Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string), typeof(StringComparison) }), - context.FilterValue, - Expression.Constant(StringComparison.OrdinalIgnoreCase))) + .HasExpression((context) => + { + if (context.IsMaterializedQueryable) + { + return Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string), typeof(StringComparison) }), + context.FilterValue, + Expression.Constant(StringComparison.OrdinalIgnoreCase)); + } + else + { + return Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string), typeof(StringComparison) }), + context.FilterValue); + } + }) .IsStringBased() .IsCaseInsensitive() .Build(), @@ -241,15 +431,21 @@ public static class FilterOperatorMapper .HasName("does not contain (case insensitive)") .HasExpression((context) => { -#if NETSTANDARD2_0 - return Expression.Not(Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string), typeof(StringComparison) }), - context.FilterValue, - Expression.Constant(StringComparison.OrdinalIgnoreCase))); -#else - throw new NotSupportedException("Runtime version does not support case-insensitive contains method."); -#endif + if (context.IsMaterializedQueryable) + { + return Expression.Not(Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string), typeof(StringComparison) }), + context.FilterValue, + Expression.Constant(StringComparison.OrdinalIgnoreCase))); + } + else + { + return Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string), typeof(StringComparison) }), + context.FilterValue); + } }) .IsStringBased() .IsCaseInsensitive() @@ -257,22 +453,48 @@ public static class FilterOperatorMapper new FilterOperatorBuilder() .HasSymbol(FilterOperatorSymbols.DoesNotStartWithCaseInsensitive) .HasName("does not start with (case insensitive)") - .HasExpression((context) => Expression.Not(Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string), typeof(StringComparison) }), - context.FilterValue, - Expression.Constant(StringComparison.OrdinalIgnoreCase)))) + .HasExpression((context) => + { + if (context.IsMaterializedQueryable) + { + return Expression.Not(Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string), typeof(StringComparison) }), + context.FilterValue, + Expression.Constant(StringComparison.OrdinalIgnoreCase))); + } + else + { + return Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string), typeof(StringComparison) }), + context.FilterValue); + } + }) .IsStringBased() .IsCaseInsensitive() .Build(), new FilterOperatorBuilder() .HasSymbol(FilterOperatorSymbols.DoesNotEndWithCaseInsensitive) .HasName("does not end with (case insensitive)") - .HasExpression((context) => Expression.Not(Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string), typeof(StringComparison) }), - context.FilterValue, - Expression.Constant(StringComparison.OrdinalIgnoreCase)))) + .HasExpression((context) => + { + if (context.IsMaterializedQueryable) + { + return Expression.Not(Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string), typeof(StringComparison) }), + context.FilterValue, + Expression.Constant(StringComparison.OrdinalIgnoreCase))); + } + else + { + return Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string), typeof(StringComparison) }), + context.FilterValue); + } + }) .IsStringBased() .IsCaseInsensitive() .Build(), diff --git a/src/Strainer/Services/Filtering/IFilterExpressionProvider.cs b/src/Strainer/Services/Filtering/IFilterExpressionProvider.cs index 252f6bcd..5f645f93 100644 --- a/src/Strainer/Services/Filtering/IFilterExpressionProvider.cs +++ b/src/Strainer/Services/Filtering/IFilterExpressionProvider.cs @@ -10,5 +10,6 @@ public interface IFilterExpressionProvider IPropertyMetadata metadata, IFilterTerm filterTerm, ParameterExpression parameterExpression, - Expression? innerExpression); + Expression? innerExpression, + bool isMaterializedQueryable); } diff --git a/src/Strainer/Services/Filtering/Steps/ApplyFilterOperatorStep.cs b/src/Strainer/Services/Filtering/Steps/ApplyFilterOperatorStep.cs index eb6e0c49..5234a8ac 100644 --- a/src/Strainer/Services/Filtering/Steps/ApplyFilterOperatorStep.cs +++ b/src/Strainer/Services/Filtering/Steps/ApplyFilterOperatorStep.cs @@ -5,6 +5,13 @@ namespace Fluorite.Strainer.Services.Filtering.Steps; public class ApplyFilterOperatorStep : IApplyFilterOperatorStep { + private readonly IStrainerOptionsProvider _strainerOptionsProvider; + + public ApplyFilterOperatorStep(IStrainerOptionsProvider strainerOptionsProvider) + { + _strainerOptionsProvider = Guard.Against.Null(strainerOptionsProvider); + } + public void Execute(FilterExpressionWorkflowContext context) { Guard.Against.Null(context); @@ -16,7 +23,14 @@ public class ApplyFilterOperatorStep : IApplyFilterOperatorStep Guard.Against.Null(context.PropertyMetadata.PropertyInfo); Guard.Against.Null(context.FilterTermValue); - var filterOperatorContext = new FilterExpressionContext(context.FinalExpression, context.PropertyValue); + var strainerOptions = _strainerOptionsProvider.GetStrainerOptions(); + var isStringBasedProperty = context.PropertyMetadata.PropertyInfo.PropertyType == typeof(string); + var filterOperatorContext = new FilterExpressionContext( + context.FinalExpression, + context.PropertyValue, + strainerOptions.IsCaseInsensitiveForValues, + context.IsMaterializedQueryable, + isStringBasedProperty); try { diff --git a/src/Strainer/Services/Filtering/Steps/MitigateCaseInsensitivityStep.cs b/src/Strainer/Services/Filtering/Steps/MitigateCaseInsensitivityStep.cs index 013d5b8b..c435808c 100644 --- a/src/Strainer/Services/Filtering/Steps/MitigateCaseInsensitivityStep.cs +++ b/src/Strainer/Services/Filtering/Steps/MitigateCaseInsensitivityStep.cs @@ -19,6 +19,11 @@ public class MitigateCaseInsensitivityStep : IMitigateCaseInsensitivityStep Guard.Against.Null(context.PropertyMetadata); Guard.Against.Null(context.PropertyMetadata.PropertyInfo); + if (context.IsMaterializedQueryable) + { + return; + } + var options = _strainerOptionsProvider.GetStrainerOptions(); if ((context.Term.Operator.IsCaseInsensitive diff --git a/src/Strainer/Services/Linq/IQueryableEvaluator.cs b/src/Strainer/Services/Linq/IQueryableEvaluator.cs new file mode 100644 index 00000000..9dc45796 --- /dev/null +++ b/src/Strainer/Services/Linq/IQueryableEvaluator.cs @@ -0,0 +1,6 @@ +namespace Fluorite.Strainer.Services.Linq; + +public interface IQueryableEvaluator +{ + bool IsMaterialized(IQueryable queryable); +} diff --git a/src/Strainer/Services/Linq/QueryableEvaluator.cs b/src/Strainer/Services/Linq/QueryableEvaluator.cs new file mode 100644 index 00000000..4d715582 --- /dev/null +++ b/src/Strainer/Services/Linq/QueryableEvaluator.cs @@ -0,0 +1,24 @@ +using System.Reflection; + +namespace Fluorite.Strainer.Services.Linq; + +public class QueryableEvaluator : IQueryableEvaluator +{ + public bool IsMaterialized(IQueryable queryable) + { + Guard.Against.Null(queryable); + + if (queryable is not EnumerableQuery enumerableQuery) + { + return false; + } + + var type = enumerableQuery.GetType(); + var property = type.GetProperty("Enumerable", BindingFlags.NonPublic | BindingFlags.Instance); + var propertyValue = property.GetValue(queryable, null); + + return propertyValue is Array + || propertyValue is ICollection + || propertyValue is IReadOnlyCollection; + } +} diff --git a/src/Strainer/Services/Pipelines/FilterPipelineOperation.cs b/src/Strainer/Services/Pipelines/FilterPipelineOperation.cs index 90a24126..4b2eb68a 100644 --- a/src/Strainer/Services/Pipelines/FilterPipelineOperation.cs +++ b/src/Strainer/Services/Pipelines/FilterPipelineOperation.cs @@ -1,6 +1,7 @@ using Fluorite.Strainer.Exceptions; using Fluorite.Strainer.Models; using Fluorite.Strainer.Services.Filtering; +using Fluorite.Strainer.Services.Linq; using Fluorite.Strainer.Services.Metadata; using System.Linq.Expressions; @@ -10,6 +11,7 @@ public class FilterPipelineOperation : IFilterPipelineOperation, IStrainerPipeli { private readonly ICustomFilteringExpressionProvider _customFilteringExpressionProvider; private readonly IFilterExpressionProvider _filterExpressionProvider; + private readonly IQueryableEvaluator _queryableEvaluator; private readonly IFilterTermParser _filterTermParser; private readonly IMetadataFacade _metadataFacade; private readonly IStrainerOptionsProvider _strainerOptionsProvider; @@ -17,12 +19,14 @@ public class FilterPipelineOperation : IFilterPipelineOperation, IStrainerPipeli public FilterPipelineOperation( ICustomFilteringExpressionProvider customFilteringExpressionProvider, IFilterExpressionProvider filterExpressionProvider, + IQueryableEvaluator queryableEvaluator, IFilterTermParser filterTermParser, IMetadataFacade metadataFacade, IStrainerOptionsProvider strainerOptionsProvider) { _customFilteringExpressionProvider = Guard.Against.Null(customFilteringExpressionProvider); _filterExpressionProvider = Guard.Against.Null(filterExpressionProvider); + _queryableEvaluator = Guard.Against.Null(queryableEvaluator); _filterTermParser = Guard.Against.Null(filterTermParser); _metadataFacade = Guard.Against.Null(metadataFacade); _strainerOptionsProvider = Guard.Against.Null(strainerOptionsProvider); @@ -40,6 +44,7 @@ public class FilterPipelineOperation : IFilterPipelineOperation, IStrainerPipeli return source; } + var isMaterializedQueryable = _queryableEvaluator.IsMaterialized(source); Expression? outerExpression = null; var parameterExpression = Expression.Parameter(typeof(T), "e"); foreach (var filterTerm in parsedTerms) @@ -56,7 +61,7 @@ public class FilterPipelineOperation : IFilterPipelineOperation, IStrainerPipeli { if (metadata is not null) { - termExpression = _filterExpressionProvider.GetExpression(metadata, filterTerm, parameterExpression, termExpression); + termExpression = _filterExpressionProvider.GetExpression(metadata, filterTerm, parameterExpression, termExpression, isMaterializedQueryable); } else { diff --git a/test/Strainer.IntegrationTests/Filtering/Operators/ContainsCaseInsensitiveOperatorTests.cs b/test/Strainer.IntegrationTests/Filtering/Operators/ContainsCaseInsensitiveOperatorTests.cs index f1183234..c5343988 100644 --- a/test/Strainer.IntegrationTests/Filtering/Operators/ContainsCaseInsensitiveOperatorTests.cs +++ b/test/Strainer.IntegrationTests/Filtering/Operators/ContainsCaseInsensitiveOperatorTests.cs @@ -40,7 +40,7 @@ public class ContainsCaseInsensitiveOperatorTests : StrainerFixtureBase var result = processor.Apply(model, queryable); // Assert - result.Should().OnlyContain(p => p.Title.Contains("a", StringComparison.OrdinalIgnoreCase)); + result.Should().OnlyContain(p => p.Title.Contains('a', StringComparison.OrdinalIgnoreCase)); } private class Post diff --git a/test/Strainer.UnitTests/Services/Filtering/FilterExpressionProviderTests.cs b/test/Strainer.UnitTests/Services/Filtering/FilterExpressionProviderTests.cs index f08bd91a..490d5e48 100644 --- a/test/Strainer.UnitTests/Services/Filtering/FilterExpressionProviderTests.cs +++ b/test/Strainer.UnitTests/Services/Filtering/FilterExpressionProviderTests.cs @@ -1,6 +1,5 @@ using Fluorite.Strainer.Models.Filtering.Terms; using Fluorite.Strainer.Models.Metadata; -using Fluorite.Strainer.Services.Conversion; using Fluorite.Strainer.Services.Filtering; using System.Linq.Expressions; using System.Reflection; @@ -9,7 +8,6 @@ namespace Fluorite.Strainer.UnitTests.Services.Filtering; public class FilterExpressionProviderTests { - private readonly ITypeConverterProvider _typeConverterProviderMock = Substitute.For(); private readonly IFilterExpressionWorkflowBuilder _filterExpressionWorkflowBuilderMock = Substitute.For(); private readonly FilterExpressionProvider _provider; @@ -17,7 +15,6 @@ public class FilterExpressionProviderTests public FilterExpressionProviderTests() { _provider = new FilterExpressionProvider( - _typeConverterProviderMock, _filterExpressionWorkflowBuilderMock); } @@ -32,7 +29,7 @@ public class FilterExpressionProviderTests filterTerm.Values.ReturnsNull(); // Act - var result = _provider.GetExpression(metadata, filterTerm, parameterExpression, innerExpression: null); + var result = _provider.GetExpression(metadata, filterTerm, parameterExpression, innerExpression: null, isMaterializedQueryable: true); // Assert result.Should().BeNull(); @@ -49,7 +46,7 @@ public class FilterExpressionProviderTests filterTerm.Values.Returns([]); // Act - var result = _provider.GetExpression(metadata, filterTerm, parameterExpression, innerExpression: null); + var result = _provider.GetExpression(metadata, filterTerm, parameterExpression, innerExpression: null, isMaterializedQueryable: true); // Assert result.Should().BeNull(); @@ -70,7 +67,7 @@ public class FilterExpressionProviderTests metadata.Name.Returns(name); // Act - Action act = () => _provider.GetExpression(metadata, filterTerm, parameterExpression, innerExpression: null); + Action act = () => _provider.GetExpression(metadata, filterTerm, parameterExpression, innerExpression: null, isMaterializedQueryable: true); // Assert act.Should().ThrowExactly() @@ -83,7 +80,6 @@ public class FilterExpressionProviderTests // Arrange var metadata = Substitute.For(); var propertyInfoMock = Substitute.For(); - var typeConverterMock = Substitute.For(); var filterExpressionWorkflowMock = Substitute.For(); var filterTerm = Substitute.For(); var modelType = typeof(Post); @@ -96,9 +92,6 @@ public class FilterExpressionProviderTests propertyInfoMock.PropertyType.Returns(modelType); metadata.Name.Returns(name); metadata.PropertyInfo.Returns(propertyInfoMock); - _typeConverterProviderMock - .GetTypeConverter(modelType) - .Returns(typeConverterMock); _filterExpressionWorkflowBuilderMock .BuildDefaultWorkflow() .Returns(filterExpressionWorkflowMock); @@ -112,7 +105,7 @@ public class FilterExpressionProviderTests .Returns(expression); // Act - var result = _provider.GetExpression(metadata, filterTerm, parameterExpression, innerExpression: null); + var result = _provider.GetExpression(metadata, filterTerm, parameterExpression, innerExpression: null, isMaterializedQueryable: true); // Assert result.Should().NotBeNull(); @@ -125,7 +118,6 @@ public class FilterExpressionProviderTests // Arrange var metadata = Substitute.For(); var propertyInfoMock = Substitute.For(); - var typeConverterMock = Substitute.For(); var filterExpressionWorkflowMock = Substitute.For(); var filterTerm = Substitute.For(); var modelType = typeof(Post); @@ -139,9 +131,6 @@ public class FilterExpressionProviderTests propertyInfoMock.PropertyType.Returns(modelType); metadata.Name.Returns(name); metadata.PropertyInfo.Returns(propertyInfoMock); - _typeConverterProviderMock - .GetTypeConverter(modelType) - .Returns(typeConverterMock); _filterExpressionWorkflowBuilderMock .BuildDefaultWorkflow() .Returns(filterExpressionWorkflowMock); @@ -155,7 +144,7 @@ public class FilterExpressionProviderTests .Returns(expression); // Act - var result = _provider.GetExpression(metadata, filterTerm, parameterExpression, innerExpression); + var result = _provider.GetExpression(metadata, filterTerm, parameterExpression, innerExpression, isMaterializedQueryable: true); // Assert result.Should().NotBeNull(); diff --git a/test/Strainer.UnitTests/Services/Filtering/FilterOperatorMapperTests.cs b/test/Strainer.UnitTests/Services/Filtering/FilterOperatorMapperTests.cs index d98520d8..1af5df70 100644 --- a/test/Strainer.UnitTests/Services/Filtering/FilterOperatorMapperTests.cs +++ b/test/Strainer.UnitTests/Services/Filtering/FilterOperatorMapperTests.cs @@ -463,12 +463,23 @@ public class FilterOperatorMapperTests result.Should().Be(expectedEqual); } - private static Expression> BuildLambdaExpression(T property, T filter, string operatorSymbol) + private static Expression> BuildLambdaExpression( + T property, + T filter, + string operatorSymbol, + bool isCaseInsensitiveForValues = false, + bool isMaterializedQueryable = true, + bool? isStringBasedProperty = null) { var filterOperator = DefaultOperators[operatorSymbol]; var propertyValue = Expression.Constant(property, typeof(T)); var filterValue = Expression.Constant(filter, typeof(T)); - var filterExpressionContext = new FilterExpressionContext(filterValue, propertyValue); + var filterExpressionContext = new FilterExpressionContext( + filterValue, + propertyValue, + isCaseInsensitiveForValues, + isMaterializedQueryable, + isStringBasedProperty ?? typeof(T) == typeof(string)); var expression = filterOperator.ExpressionProvider.Invoke(filterExpressionContext); return Expression.Lambda>(expression); diff --git a/test/Strainer.UnitTests/Services/Filtering/Steps/ApplyFilterOperatorStepTests.cs b/test/Strainer.UnitTests/Services/Filtering/Steps/ApplyFilterOperatorStepTests.cs index 278a2034..eb0737c6 100644 --- a/test/Strainer.UnitTests/Services/Filtering/Steps/ApplyFilterOperatorStepTests.cs +++ b/test/Strainer.UnitTests/Services/Filtering/Steps/ApplyFilterOperatorStepTests.cs @@ -1,7 +1,9 @@ using Fluorite.Strainer.Exceptions; +using Fluorite.Strainer.Models; using Fluorite.Strainer.Models.Filtering.Operators; using Fluorite.Strainer.Models.Filtering.Terms; using Fluorite.Strainer.Models.Metadata; +using Fluorite.Strainer.Services; using Fluorite.Strainer.Services.Filtering; using Fluorite.Strainer.Services.Filtering.Steps; using NSubstitute.ExceptionExtensions; @@ -12,11 +14,13 @@ namespace Fluorite.Strainer.UnitTests.Services.Filtering.Steps; public class ApplyFilterOperatorStepTests { + private readonly IStrainerOptionsProvider _optionsProviderMock = Substitute.For(); + private readonly ApplyFilterOperatorStep _step; public ApplyFilterOperatorStepTests() { - _step = new ApplyFilterOperatorStep(); + _step = new ApplyFilterOperatorStep(_optionsProviderMock); } [Fact] @@ -51,6 +55,10 @@ public class ApplyFilterOperatorStepTests .PropertyInfo .Returns(propertyInfo); context.PropertyMetadata = propertyMetadata; + var strainerOptions = new StrainerOptions(); + _optionsProviderMock + .GetStrainerOptions() + .Returns(strainerOptions); // Act _step.Execute(context); @@ -58,7 +66,8 @@ public class ApplyFilterOperatorStepTests // Assert context.FinalExpression.Should().Be(finalExpression); - funcMock.Received(1); + funcMock.Received(1).Invoke(Arg.Any()); + _optionsProviderMock.Received(1).GetStrainerOptions(); } [Fact] @@ -82,6 +91,10 @@ public class ApplyFilterOperatorStepTests propertyMetadata .PropertyInfo .Returns(typeof(Blog).GetProperty(nameof(Blog.Title))); + var strainerOptions = new StrainerOptions(); + _optionsProviderMock + .GetStrainerOptions() + .Returns(strainerOptions); var context = new FilterExpressionWorkflowContext { FilterTermValue = "lorem", @@ -100,6 +113,8 @@ public class ApplyFilterOperatorStepTests $"Failed to use operator * for filter value '{context.FilterTermValue}' on property '*Blog.Title' of type 'System.String'\n." + "Please ensure that this operator is supported by type 'System.String'.") .WithInnerExceptionExactly(innerException.GetType()); + + _optionsProviderMock.Received(1).GetStrainerOptions(); } private class Blog diff --git a/test/Strainer.UnitTests/Services/Linq/QueryableEvaluatorTests.cs b/test/Strainer.UnitTests/Services/Linq/QueryableEvaluatorTests.cs new file mode 100644 index 00000000..946016d5 --- /dev/null +++ b/test/Strainer.UnitTests/Services/Linq/QueryableEvaluatorTests.cs @@ -0,0 +1,52 @@ +using Fluorite.Strainer.Services.Linq; +using System.Collections; +using System.Collections.ObjectModel; + +namespace Fluorite.Strainer.UnitTests.Services.Linq; + +public class QueryableEvaluatorTests +{ + private readonly QueryableEvaluator _evaluator = new(); + + [Fact] + public void Should_Return_QueryableEvaluation_ForUnknownQueryable() + { + // Arrange + var queryable = Substitute.For>(); + + // Act + var result = _evaluator.IsMaterialized(queryable); + + // Assert + result.Should().BeFalse(); + } + + [Theory] + [ClassData(typeof(CollectionSourceData))] + public void Should_Return_QueryableEvaluation_ForKnownCollectionTypes(IQueryable queryable) + { + // Act + var result = _evaluator.IsMaterialized(queryable); + + // Assert + result.Should().BeTrue(); + } + + public class CollectionSourceData : IEnumerable + { + public IEnumerator GetEnumerator() + { + return new object[] + { + Enumerable.Empty().AsQueryable(), + new List().AsQueryable(), + Array.Empty().AsQueryable(), + new ReadOnlyCollection(Array.Empty()).AsQueryable(), + } + .Chunk(1) + .GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/test/Strainer.UnitTests/Services/Metadata/PropertyInfoProviderTests.cs b/test/Strainer.UnitTests/Services/Metadata/PropertyInfoProviderTests.cs index d054c4e0..af5ca4f1 100644 --- a/test/Strainer.UnitTests/Services/Metadata/PropertyInfoProviderTests.cs +++ b/test/Strainer.UnitTests/Services/Metadata/PropertyInfoProviderTests.cs @@ -10,18 +10,16 @@ public class PropertyInfoProviderTests public void Provider_Returns_PropertyInfo() { // Arrange - var name = "foo"; - var propertyInfoMock = Substitute.For(); - var typeMock = Substitute.For(); - typeMock.GetProperty(name, BindingFlags.Instance | BindingFlags.Public).Returns(propertyInfoMock); + var type = typeof(string); + var name = nameof(string.Length); var provider = new PropertyInfoProvider(); // Act - var result = provider.GetPropertyInfo(typeMock, name); + var result = provider.GetPropertyInfo(type, name); // Assert result.Should().NotBeNull(); - result.Should().BeSameAs(propertyInfoMock); + result.Equals(type.GetProperty(name)).Should().BeTrue(); } [Fact] diff --git a/test/Strainer.UnitTests/Services/Pipelines/FilterPipelineOperationTests.cs b/test/Strainer.UnitTests/Services/Pipelines/FilterPipelineOperationTests.cs index 12950d37..d75e086e 100644 --- a/test/Strainer.UnitTests/Services/Pipelines/FilterPipelineOperationTests.cs +++ b/test/Strainer.UnitTests/Services/Pipelines/FilterPipelineOperationTests.cs @@ -4,6 +4,7 @@ using Fluorite.Strainer.Models.Filtering.Terms; using Fluorite.Strainer.Models.Metadata; using Fluorite.Strainer.Services; using Fluorite.Strainer.Services.Filtering; +using Fluorite.Strainer.Services.Linq; using Fluorite.Strainer.Services.Metadata; using Fluorite.Strainer.Services.Pipelines; using System.Linq.Expressions; @@ -14,6 +15,7 @@ public class FilterPipelineOperationTests { private readonly ICustomFilteringExpressionProvider _customFilteringExpressionProvider = Substitute.For(); private readonly IFilterExpressionProvider _filterExpressionProvider = Substitute.For(); + private readonly IQueryableEvaluator _queryableEvaluator = Substitute.For(); private readonly IFilterTermParser _filterTermParser = Substitute.For(); private readonly IMetadataFacade _metadataFacade = Substitute.For(); private readonly IStrainerOptionsProvider _strainerOptionsProvider = Substitute.For(); @@ -25,6 +27,7 @@ public class FilterPipelineOperationTests _operation = new FilterPipelineOperation( _customFilteringExpressionProvider, _filterExpressionProvider, + _queryableEvaluator, _filterTermParser, _metadataFacade, _strainerOptionsProvider); @@ -62,6 +65,7 @@ public class FilterPipelineOperationTests { // Arrange var source = new[] { "foo" }.AsQueryable(); + var isMaterializedQueryable = true; var model = new StrainerModel { Filters = "input", @@ -81,6 +85,9 @@ public class FilterPipelineOperationTests _filterTermParser .GetParsedTerms(model.Filters) .Returns(terms); + _queryableEvaluator + .IsMaterialized(source) + .Returns(isMaterializedQueryable); _metadataFacade .GetMetadata(isSortableRequired: false, isFilterableRequired: true, filterTermName) .Returns(metadata); @@ -94,7 +101,10 @@ public class FilterPipelineOperationTests _strainerOptionsProvider.Received(1).GetStrainerOptions(); _filterTermParser.Received(1).GetParsedTerms(model.Filters); _metadataFacade.Received(1).GetMetadata(false, true, filterTermName); - _filterExpressionProvider.Received(1).GetExpression(metadata, filterTerm, Arg.Any(), Arg.Any()); + _queryableEvaluator.Received(1).IsMaterialized(source); + _filterExpressionProvider + .Received(1) + .GetExpression(metadata, filterTerm, Arg.Any(), Arg.Any(), isMaterializedQueryable); } [Fact] @@ -102,6 +112,7 @@ public class FilterPipelineOperationTests { // Arrange var source = new[] { "foo" }.AsQueryable(); + var isMaterializedQueryable = true; var sourceClone = source.ToArray(); var model = new StrainerModel { @@ -126,8 +137,11 @@ public class FilterPipelineOperationTests _metadataFacade .GetMetadata(isSortableRequired: false, isFilterableRequired: true, filterTermName) .Returns(metadata); + _queryableEvaluator + .IsMaterialized(source) + .Returns(isMaterializedQueryable); _filterExpressionProvider - .GetExpression(metadata, filterTerm, Arg.Any(), Arg.Any()) + .GetExpression(metadata, filterTerm, Arg.Any(), Arg.Any(), isMaterializedQueryable) .Returns(validExpression); // Act @@ -141,7 +155,10 @@ public class FilterPipelineOperationTests _strainerOptionsProvider.Received(1).GetStrainerOptions(); _filterTermParser.Received(1).GetParsedTerms(model.Filters); _metadataFacade.Received(1).GetMetadata(false, true, filterTermName); - _filterExpressionProvider.Received(1).GetExpression(metadata, filterTerm, Arg.Any(), Arg.Any()); + _queryableEvaluator.Received(1).IsMaterialized(source); + _filterExpressionProvider + .Received(1) + .GetExpression(metadata, filterTerm, Arg.Any(), Arg.Any(), isMaterializedQueryable); } [Fact] @@ -149,6 +166,7 @@ public class FilterPipelineOperationTests { // Arrange var source = new[] { "foo" }.AsQueryable(); + var isMaterializedQueryable = true; var model = new StrainerModel { Filters = "input", @@ -167,6 +185,9 @@ public class FilterPipelineOperationTests _filterTermParser .GetParsedTerms(model.Filters) .Returns(terms); + _queryableEvaluator + .IsMaterialized(source) + .Returns(isMaterializedQueryable); _metadataFacade .GetMetadata(isSortableRequired: false, isFilterableRequired: true, filterTermName) .Returns((IPropertyMetadata)null); @@ -180,6 +201,7 @@ public class FilterPipelineOperationTests _strainerOptionsProvider.Received(1).GetStrainerOptions(); _filterTermParser.Received(1).GetParsedTerms(model.Filters); + _queryableEvaluator.Received(1).IsMaterialized(source); _metadataFacade.Received(1).GetMetadata(false, true, filterTermName); } @@ -188,6 +210,7 @@ public class FilterPipelineOperationTests { // Arrange var source = new[] { "foo" }.AsQueryable(); + var isMaterializedQueryable = true; var model = new StrainerModel { Filters = "input", @@ -206,6 +229,9 @@ public class FilterPipelineOperationTests _filterTermParser .GetParsedTerms(model.Filters) .Returns(terms); + _queryableEvaluator + .IsMaterialized(source) + .Returns(isMaterializedQueryable); _metadataFacade .GetMetadata(isSortableRequired: false, isFilterableRequired: true, filterTermName) .Returns((IPropertyMetadata)null); @@ -218,6 +244,7 @@ public class FilterPipelineOperationTests _strainerOptionsProvider.Received(1).GetStrainerOptions(); _filterTermParser.Received(1).GetParsedTerms(model.Filters); + _queryableEvaluator.Received(1).IsMaterialized(source); _metadataFacade.Received(1).GetMetadata(false, true, filterTermName); } @@ -226,6 +253,7 @@ public class FilterPipelineOperationTests { // Arrange var source = new[] { "foo" }.AsQueryable(); + var isMaterializedQueryable = true; var sourceClone = source.ToArray(); var model = new StrainerModel { @@ -246,6 +274,9 @@ public class FilterPipelineOperationTests _filterTermParser .GetParsedTerms(model.Filters) .Returns(terms); + _queryableEvaluator + .IsMaterialized(source) + .Returns(isMaterializedQueryable); _metadataFacade .GetMetadata(isSortableRequired: false, isFilterableRequired: true, filterTermName) .Returns((IPropertyMetadata)null); @@ -267,6 +298,7 @@ public class FilterPipelineOperationTests _strainerOptionsProvider.Received(1).GetStrainerOptions(); _filterTermParser.Received(1).GetParsedTerms(model.Filters); + _queryableEvaluator.Received(1).IsMaterialized(source); _metadataFacade.Received(1).GetMetadata(false, true, filterTermName); } @@ -275,6 +307,7 @@ public class FilterPipelineOperationTests { // Arrange var source = new[] { "foo", "boat", "ID" }.AsQueryable(); + var isMaterializedQueryable = true; var sourceClone = source.ToArray(); var model = new StrainerModel { @@ -297,6 +330,9 @@ public class FilterPipelineOperationTests _filterTermParser .GetParsedTerms(model.Filters) .Returns(terms); + _queryableEvaluator + .IsMaterialized(source) + .Returns(isMaterializedQueryable); _metadataFacade .GetMetadata(isSortableRequired: false, isFilterableRequired: true, filterTermName1) .Returns(metadata1); @@ -304,10 +340,10 @@ public class FilterPipelineOperationTests .GetMetadata(isSortableRequired: false, isFilterableRequired: true, filterTermName2) .Returns(metadata2); _filterExpressionProvider - .GetExpression(metadata1, filterTerm, Arg.Any(), Arg.Any()) + .GetExpression(metadata1, filterTerm, Arg.Any(), Arg.Any(), isMaterializedQueryable) .Returns(x => CreateStringGreaterThanLengthExpression(x[2] as ParameterExpression, length: 2)); _filterExpressionProvider - .GetExpression(metadata2, filterTerm, Arg.Any(), Arg.Is(x => x != null)) + .GetExpression(metadata2, filterTerm, Arg.Any(), Arg.Is(x => x != null), isMaterializedQueryable) .Returns(x => CreateStringGreaterThanLengthExpression(x[2] as ParameterExpression, length: 3)); // Act @@ -320,10 +356,15 @@ public class FilterPipelineOperationTests _strainerOptionsProvider.Received(1).GetStrainerOptions(); _filterTermParser.Received(1).GetParsedTerms(model.Filters); + _queryableEvaluator.Received(1).IsMaterialized(source); _metadataFacade.Received(1).GetMetadata(false, true, filterTermName1); _metadataFacade.Received(1).GetMetadata(false, true, filterTermName2); - _filterExpressionProvider.Received(1).GetExpression(metadata1, filterTerm, Arg.Any(), Arg.Any()); - _filterExpressionProvider.Received(1).GetExpression(metadata2, filterTerm, Arg.Any(), Arg.Is(x => x != null)); + _filterExpressionProvider + .Received(1) + .GetExpression(metadata1, filterTerm, Arg.Any(), Arg.Any(), isMaterializedQueryable); + _filterExpressionProvider + .Received(1) + .GetExpression(metadata2, filterTerm, Arg.Any(), Arg.Is(x => x != null), isMaterializedQueryable); } [Fact] @@ -331,6 +372,7 @@ public class FilterPipelineOperationTests { // Arrange var source = new[] { "foo", "boat", "ID" }.AsQueryable(); + var isMaterializedQueryable = true; var sourceClone = source.ToArray(); var model = new StrainerModel { @@ -356,6 +398,9 @@ public class FilterPipelineOperationTests _filterTermParser .GetParsedTerms(model.Filters) .Returns(terms); + _queryableEvaluator + .IsMaterialized(source) + .Returns(isMaterializedQueryable); _metadataFacade .GetMetadata(isSortableRequired: false, isFilterableRequired: true, filterTermName1) .Returns(metadata1); @@ -363,10 +408,10 @@ public class FilterPipelineOperationTests .GetMetadata(isSortableRequired: false, isFilterableRequired: true, filterTermName2) .Returns(metadata2); _filterExpressionProvider - .GetExpression(metadata1, filterTerm1, Arg.Any(), Arg.Any()) + .GetExpression(metadata1, filterTerm1, Arg.Any(), Arg.Any(), isMaterializedQueryable) .Returns(x => CreateStringGreaterThanLengthExpression(x[2] as ParameterExpression, length: 2)); _filterExpressionProvider - .GetExpression(metadata2, filterTerm2, Arg.Any(), Arg.Any()) + .GetExpression(metadata2, filterTerm2, Arg.Any(), Arg.Any(), isMaterializedQueryable) .Returns(x => CreateStringGreaterThanLengthExpression(x[2] as ParameterExpression, length: 3)); // Act @@ -379,10 +424,15 @@ public class FilterPipelineOperationTests _strainerOptionsProvider.Received(1).GetStrainerOptions(); _filterTermParser.Received(1).GetParsedTerms(model.Filters); + _queryableEvaluator.Received(1).IsMaterialized(source); _metadataFacade.Received(1).GetMetadata(false, true, filterTermName1); _metadataFacade.Received(1).GetMetadata(false, true, filterTermName2); - _filterExpressionProvider.Received(1).GetExpression(metadata1, filterTerm1, Arg.Any(), Arg.Is(y => y == null)); - _filterExpressionProvider.Received(1).GetExpression(metadata2, filterTerm2, Arg.Any(), Arg.Any()); + _filterExpressionProvider + .Received(1) + .GetExpression(metadata1, filterTerm1, Arg.Any(), Arg.Is(y => y == null), isMaterializedQueryable); + _filterExpressionProvider + .Received(1) + .GetExpression(metadata2, filterTerm2, Arg.Any(), Arg.Any(), true); } private Expression CreateStringGreaterThanLengthExpression(ParameterExpression parameterExpression, int length) -- GitLab From 2ab1e6fc5862704d2d3d9b3d6b993b5267fad3fd Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Tue, 28 Jan 2025 00:56:11 +0100 Subject: [PATCH 39/56] Fix bugs after last rework, add a lot of more unit tests to filter operators --- .../Operators/IFilterExpressionContext.cs | 2 +- .../Filtering/FilterOperatorMapper.cs | 24 +- .../Filtering/FilterOperatorMapperTests.cs | 592 ++++++++++++------ 3 files changed, 422 insertions(+), 196 deletions(-) diff --git a/src/Strainer/Models/Filtering/Operators/IFilterExpressionContext.cs b/src/Strainer/Models/Filtering/Operators/IFilterExpressionContext.cs index c0c56046..2a5b7a28 100644 --- a/src/Strainer/Models/Filtering/Operators/IFilterExpressionContext.cs +++ b/src/Strainer/Models/Filtering/Operators/IFilterExpressionContext.cs @@ -16,7 +16,7 @@ public interface IFilterExpressionContext /// Gets a value indicating whether Strainer should operate in case insensitive mode /// when comparing values. /// - /// Value taken from global . + /// Value taken from global . /// public bool IsCaseInsensitiveForValues { get; } diff --git a/src/Strainer/Services/Filtering/FilterOperatorMapper.cs b/src/Strainer/Services/Filtering/FilterOperatorMapper.cs index 971aca39..7f34c97a 100644 --- a/src/Strainer/Services/Filtering/FilterOperatorMapper.cs +++ b/src/Strainer/Services/Filtering/FilterOperatorMapper.cs @@ -364,7 +364,7 @@ public static class FilterOperatorMapper { return Expression.Call( context.PropertyValue, - typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string), typeof(StringComparison) }), + typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string) }), context.FilterValue); } }) @@ -388,7 +388,7 @@ public static class FilterOperatorMapper { return Expression.Call( context.PropertyValue, - typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string), typeof(StringComparison) }), + typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string) }), context.FilterValue); } }) @@ -412,7 +412,7 @@ public static class FilterOperatorMapper { return Expression.Call( context.PropertyValue, - typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string), typeof(StringComparison) }), + typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string) }), context.FilterValue); } }) @@ -441,10 +441,10 @@ public static class FilterOperatorMapper } else { - return Expression.Call( + return Expression.Not(Expression.Call( context.PropertyValue, - typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string), typeof(StringComparison) }), - context.FilterValue); + typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string) }), + context.FilterValue)); } }) .IsStringBased() @@ -465,10 +465,10 @@ public static class FilterOperatorMapper } else { - return Expression.Call( + return Expression.Not(Expression.Call( context.PropertyValue, - typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string), typeof(StringComparison) }), - context.FilterValue); + typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string) }), + context.FilterValue)); } }) .IsStringBased() @@ -489,10 +489,10 @@ public static class FilterOperatorMapper } else { - return Expression.Call( + return Expression.Not(Expression.Call( context.PropertyValue, - typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string), typeof(StringComparison) }), - context.FilterValue); + typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string) }), + context.FilterValue)); } }) .IsStringBased() diff --git a/test/Strainer.UnitTests/Services/Filtering/FilterOperatorMapperTests.cs b/test/Strainer.UnitTests/Services/Filtering/FilterOperatorMapperTests.cs index 1af5df70..b886a74c 100644 --- a/test/Strainer.UnitTests/Services/Filtering/FilterOperatorMapperTests.cs +++ b/test/Strainer.UnitTests/Services/Filtering/FilterOperatorMapperTests.cs @@ -8,20 +8,36 @@ namespace Fluorite.Strainer.UnitTests.Services.Filtering; public class FilterOperatorMapperTests { [Theory] - [InlineData(null, null, true)] - [InlineData("foo", null, false)] - [InlineData(null, "foo", false)] - [InlineData("", "", true)] - [InlineData(" ", " ", true)] - [InlineData("123", "123", true)] - [InlineData("foo", "foo", true)] - [InlineData("foo", "FOO", false)] - [InlineData("foo", "bar", false)] - public void Should_work_on_equal_operator(string value1, string value2, bool expectedEqual) + [InlineData(null, null, false, false, true)] + [InlineData("foo", null, false, false, false)] + [InlineData(null, "foo", false, false, false)] + [InlineData("", "", false, false, true)] + [InlineData(" ", " ", false, false, true)] + [InlineData("123", "123", false, false, true)] + [InlineData("foo", "foo", false, false, true)] + [InlineData("foo", "FOO", false, false, false)] + [InlineData("FOO", "foo", false, false, false)] + [InlineData("foo", "bar", false, false, false)] + [InlineData(null, null, true, true, true)] + [InlineData("foo", null, true, true, false)] + [InlineData(null, "foo", true, true, false)] + [InlineData("", "", true, true, true)] + [InlineData(" ", " ", true, true, true)] + [InlineData("123", "123", true, true, true)] + [InlineData("foo", "foo", true, true, true)] + [InlineData("foo", "FOO", true, true, true)] + [InlineData("FOO", "foo", true, true, true)] + [InlineData("foo", "bar", true, true, false)] + public void Should_work_on_equal_operator( + string property, + string filter, + bool isCaseInsensitiveForValues, + bool isMaterializedQueryable, + bool expectedEqual) { // Arrange var operatorSymbol = FilterOperatorSymbols.EqualsSymbol; - var lambda = BuildLambdaExpression(value1, value2, operatorSymbol); + var lambda = BuildLambdaExpression(property, filter, operatorSymbol, isCaseInsensitiveForValues, isMaterializedQueryable); // Act var func = lambda.Compile(); @@ -32,20 +48,36 @@ public class FilterOperatorMapperTests } [Theory] - [InlineData(null, null, false)] - [InlineData("foo", null, true)] - [InlineData(null, "foo", true)] - [InlineData("", "", false)] - [InlineData(" ", " ", false)] - [InlineData("123", "123", false)] - [InlineData("foo", "foo", false)] - [InlineData("foo", "FOO", true)] - [InlineData("foo", "bar", true)] - public void Should_work_on_does_not_equal_operator(string value1, string value2, bool expectedEqual) + [InlineData(null, null, false, false, false)] + [InlineData("foo", null, false, false, true)] + [InlineData(null, "foo", false, false, true)] + [InlineData("", "", false, false, false)] + [InlineData(" ", " ", false, false, false)] + [InlineData("123", "123", false, false, false)] + [InlineData("foo", "foo", false, false, false)] + [InlineData("foo", "FOO", false, false, true)] + [InlineData("Foo", "foo", false, false, true)] + [InlineData("foo", "bar", false, false, true)] + [InlineData(null, null, true, true, false)] + [InlineData("foo", null, true, true, true)] + [InlineData(null, "foo", true, true, true)] + [InlineData("", "", true, true, false)] + [InlineData(" ", " ", true, true, false)] + [InlineData("123", "123", true, true, false)] + [InlineData("foo", "foo", true, true, false)] + [InlineData("foo", "FOO", true, true, false)] + [InlineData("FOO", "foo", true, true, false)] + [InlineData("foo", "bar", true, true, true)] + public void Should_work_on_does_not_equal_operator( + string property, + string filter, + bool isCaseInsensitiveForValues, + bool isMaterializedQueryable, + bool expectedEqual) { // Arrange var operatorSymbol = FilterOperatorSymbols.DoesNotEqual; - var lambda = BuildLambdaExpression(value1, value2, operatorSymbol); + var lambda = BuildLambdaExpression(property, filter, operatorSymbol, isCaseInsensitiveForValues, isMaterializedQueryable); // Act var func = lambda.Compile(); @@ -60,13 +92,13 @@ public class FilterOperatorMapperTests [InlineData(1000, null, false)] [InlineData(null, 1000, false)] [InlineData(1000, 1000, false)] - [InlineData(1000, 2000, false)] - [InlineData(2000, 1000, true)] - public void Should_work_on_greater_than_operator(int? value1, int? value2, bool expectedEqual) + [InlineData(1000, 9999, false)] + [InlineData(9999, 1000, true)] + public void Should_work_on_greater_than_operator(int? property, int? filter, bool expectedEqual) { // Arrange var operatorSymbol = FilterOperatorSymbols.GreaterThan; - var lambda = BuildLambdaExpression(value1, value2, operatorSymbol); + var lambda = BuildLambdaExpression(property, filter, operatorSymbol); // Act var func = lambda.Compile(); @@ -81,13 +113,13 @@ public class FilterOperatorMapperTests [InlineData(1000, null, false)] [InlineData(null, 1000, false)] [InlineData(1000, 1000, true)] - [InlineData(1000, 2000, false)] - [InlineData(2000, 1000, true)] - public void Should_work_on_greater_than_or_equal_to_operator(int? value1, int? value2, bool expectedEqual) + [InlineData(1000, 9999, false)] + [InlineData(9999, 1000, true)] + public void Should_work_on_greater_than_or_equal_to_operator(int? property, int? filter, bool expectedEqual) { // Arrange var operatorSymbol = FilterOperatorSymbols.GreaterThanOrEqualTo; - var lambda = BuildLambdaExpression(value1, value2, operatorSymbol); + var lambda = BuildLambdaExpression(property, filter, operatorSymbol); // Act var func = lambda.Compile(); @@ -102,13 +134,13 @@ public class FilterOperatorMapperTests [InlineData(1000, null, false)] [InlineData(null, 1000, false)] [InlineData(1000, 1000, false)] - [InlineData(1000, 2000, true)] - [InlineData(2000, 1000, false)] - public void Should_work_on_less_than_operator(int? value1, int? value2, bool expectedEqual) + [InlineData(1000, 9999, true)] + [InlineData(9999, 1000, false)] + public void Should_work_on_less_than_operator(int? property, int? filter, bool expectedEqual) { // Arrange var operatorSymbol = FilterOperatorSymbols.LessThan; - var lambda = BuildLambdaExpression(value1, value2, operatorSymbol); + var lambda = BuildLambdaExpression(property, filter, operatorSymbol); // Act var func = lambda.Compile(); @@ -123,13 +155,13 @@ public class FilterOperatorMapperTests [InlineData(1000, null, false)] [InlineData(null, 1000, false)] [InlineData(1000, 1000, true)] - [InlineData(1000, 2000, true)] - [InlineData(2000, 1000, false)] - public void Should_work_on_less_than_or_equal_to_operator(int? value1, int? value2, bool expectedEqual) + [InlineData(1000, 9999, true)] + [InlineData(9999, 1000, false)] + public void Should_work_on_less_than_or_equal_to_operator(int? property, int? filter, bool expectedEqual) { // Arrange var operatorSymbol = FilterOperatorSymbols.LessThanOrEqualTo; - var lambda = BuildLambdaExpression(value1, value2, operatorSymbol); + var lambda = BuildLambdaExpression(property, filter, operatorSymbol); // Act var func = lambda.Compile(); @@ -140,19 +172,34 @@ public class FilterOperatorMapperTests } [Theory] - [InlineData("", "", true)] - [InlineData(" ", " ", true)] - [InlineData("123", "123", true)] - [InlineData("foo", "foo", true)] - [InlineData("foo", "FOO", false)] - [InlineData("foo", "bar", false)] - [InlineData("foobar", "foo", true)] - [InlineData("barfoo", "foo", true)] - public void Should_work_on_contains_operator(string propertyFilter, string filterFilter, bool expectedEqual) + [InlineData("", "", false, false, true)] + [InlineData(" ", " ", false, false, true)] + [InlineData("123", "123", false, false, true)] + [InlineData("foo", "foo", false, false, true)] + [InlineData("foo", "FOO", false, false, false)] + [InlineData("Foo", "foo", false, false, false)] + [InlineData("foo", "bar", false, false, false)] + [InlineData("foobar", "foo", false, false, true)] + [InlineData("barfoo", "foo", false, false, true)] + [InlineData("", "", true, true, true)] + [InlineData(" ", " ", true, true, true)] + [InlineData("123", "123", true, true, true)] + [InlineData("foo", "foo", true, true, true)] + [InlineData("foo", "FOO", true, true, true)] + [InlineData("FOO", "foo", true, true, true)] + [InlineData("foo", "bar", true, true, false)] + [InlineData("foobar", "foo", true, true, true)] + [InlineData("barfoo", "foo", true, true, true)] + public void Should_work_on_contains_operator( + string property, + string filter, + bool isCaseInsensitiveForValues, + bool isMaterializedQueryable, + bool expectedEqual) { // Arrange var operatorSymbol = FilterOperatorSymbols.Contains; - var lambda = BuildLambdaExpression(propertyFilter, filterFilter, operatorSymbol); + var lambda = BuildLambdaExpression(property, filter, operatorSymbol, isCaseInsensitiveForValues, isMaterializedQueryable); // Act var func = lambda.Compile(); @@ -163,19 +210,25 @@ public class FilterOperatorMapperTests } [Theory] - [InlineData("", "", false)] - [InlineData(" ", " ", false)] - [InlineData("123", "123", false)] - [InlineData("foo", "foo", false)] - [InlineData("foo", "FOO", true)] - [InlineData("foo", "bar", true)] - [InlineData("foobar", "foo", false)] - [InlineData("barfoo", "foo", false)] - public void Should_work_on_does_not_contain_operator(string propertyFilter, string filterFilter, bool expectedEqual) + [InlineData("", "", false, false, false)] + [InlineData(" ", " ", false, false, false)] + [InlineData("123", "123", false, false, false)] + [InlineData("foo", "foo", false, false, false)] + [InlineData("foo", "FOO", false, false, true)] + [InlineData("FOO", "foo", false, false, true)] + [InlineData("foo", "bar", false, false, true)] + [InlineData("foobar", "foo", false, false, false)] + [InlineData("barfoo", "foo", false, false, false)] + public void Should_work_on_does_not_contain_operator( + string property, + string filter, + bool isCaseInsensitiveForValues, + bool isMaterializedQueryable, + bool expectedEqual) { // Arrange var operatorSymbol = FilterOperatorSymbols.DoesNotContain; - var lambda = BuildLambdaExpression(propertyFilter, filterFilter, operatorSymbol); + var lambda = BuildLambdaExpression(property, filter, operatorSymbol, isCaseInsensitiveForValues, isMaterializedQueryable); // Act var func = lambda.Compile(); @@ -186,19 +239,34 @@ public class FilterOperatorMapperTests } [Theory] - [InlineData("", "", true)] - [InlineData(" ", " ", true)] - [InlineData("123", "123", true)] - [InlineData("foo", "foo", true)] - [InlineData("foo", "FOO", false)] - [InlineData("foo", "bar", false)] - [InlineData("foobar", "foo", true)] - [InlineData("barfoo", "foo", false)] - public void Should_work_on_starts_with_operator(string propertyFilter, string filterFilter, bool expectedEqual) + [InlineData("", "", false, false, true)] + [InlineData(" ", " ", false, false, true)] + [InlineData("123", "123", false, false, true)] + [InlineData("foo", "foo", false, false, true)] + [InlineData("foo", "FOO", false, false, false)] + [InlineData("FOO", "foo", false, false, false)] + [InlineData("foo", "bar", false, false, false)] + [InlineData("foobar", "foo", false, false, true)] + [InlineData("barfoo", "foo", false, false, false)] + [InlineData("", "", true, true, true)] + [InlineData(" ", " ", true, true, true)] + [InlineData("123", "123", true, true, true)] + [InlineData("foo", "foo", true, true, true)] + [InlineData("foo", "FOO", true, true, true)] + [InlineData("FOO", "foo", true, true, true)] + [InlineData("foo", "bar", true, true, false)] + [InlineData("foobar", "foo", true, true, true)] + [InlineData("barfoo", "foo", true, true, false)] + public void Should_work_on_starts_with_operator( + string property, + string filter, + bool isCaseInsensitiveForValues, + bool isMaterializedQueryable, + bool expectedEqual) { // Arrange var operatorSymbol = FilterOperatorSymbols.StartsWith; - var lambda = BuildLambdaExpression(propertyFilter, filterFilter, operatorSymbol); + var lambda = BuildLambdaExpression(property, filter, operatorSymbol, isCaseInsensitiveForValues, isMaterializedQueryable); // Act var func = lambda.Compile(); @@ -209,19 +277,34 @@ public class FilterOperatorMapperTests } [Theory] - [InlineData("", "", false)] - [InlineData(" ", " ", false)] - [InlineData("123", "123", false)] - [InlineData("foo", "foo", false)] - [InlineData("foo", "FOO", true)] - [InlineData("foo", "bar", true)] - [InlineData("foobar", "foo", false)] - [InlineData("barfoo", "foo", true)] - public void Should_work_on_does_not_start_with_operator(string propertyFilter, string filterFilter, bool expectedEqual) + [InlineData("", "", false, false, false)] + [InlineData(" ", " ", false, false, false)] + [InlineData("123", "123", false, false, false)] + [InlineData("foo", "foo", false, false, false)] + [InlineData("foo", "FOO", false, false, true)] + [InlineData("FOO", "foo", false, false, true)] + [InlineData("foo", "bar", false, false, true)] + [InlineData("foobar", "foo", false, false, false)] + [InlineData("barfoo", "foo", false, false, true)] + [InlineData("", "", true, true, false)] + [InlineData(" ", " ", true, true, false)] + [InlineData("123", "123", true, true, false)] + [InlineData("foo", "foo", true, true, false)] + [InlineData("foo", "FOO", true, true, false)] + [InlineData("FOO", "foo", true, true, false)] + [InlineData("foo", "bar", true, true, true)] + [InlineData("foobar", "foo", true, true, false)] + [InlineData("barfoo", "foo", true, true, true)] + public void Should_work_on_does_not_start_with_operator( + string property, + string filter, + bool isCaseInsensitiveForValues, + bool isMaterializedQueryable, + bool expectedEqual) { // Arrange var operatorSymbol = FilterOperatorSymbols.DoesNotStartWith; - var lambda = BuildLambdaExpression(propertyFilter, filterFilter, operatorSymbol); + var lambda = BuildLambdaExpression(property, filter, operatorSymbol, isCaseInsensitiveForValues, isMaterializedQueryable); // Act var func = lambda.Compile(); @@ -232,19 +315,34 @@ public class FilterOperatorMapperTests } [Theory] - [InlineData("", "", true)] - [InlineData(" ", " ", true)] - [InlineData("123", "123", true)] - [InlineData("foo", "foo", true)] - [InlineData("foo", "FOO", false)] - [InlineData("foo", "bar", false)] - [InlineData("foobar", "foo", false)] - [InlineData("barfoo", "foo", true)] - public void Should_work_on_ends_with_operator(string propertyFilter, string filterFilter, bool expectedEqual) + [InlineData("", "", false, false, true)] + [InlineData(" ", " ", false, false, true)] + [InlineData("123", "123", false, false, true)] + [InlineData("foo", "foo", false, false, true)] + [InlineData("foo", "FOO", false, false, false)] + [InlineData("FOO", "foo", false, false, false)] + [InlineData("foo", "bar", false, false, false)] + [InlineData("foobar", "foo", false, false, false)] + [InlineData("barfoo", "foo", false, false, true)] + [InlineData("", "", true, true, true)] + [InlineData(" ", " ", true, true, true)] + [InlineData("123", "123", true, true, true)] + [InlineData("foo", "foo", true, true, true)] + [InlineData("foo", "FOO", true, true, true)] + [InlineData("FOO", "foo", true, true, true)] + [InlineData("foo", "bar", true, true, false)] + [InlineData("foobar", "foo", true, true, false)] + [InlineData("barfoo", "foo", true, true, true)] + public void Should_work_on_ends_with_operator( + string property, + string filter, + bool isCaseInsensitiveForValues, + bool isMaterializedQueryable, + bool expectedEqual) { // Arrange var operatorSymbol = FilterOperatorSymbols.EndsWith; - var lambda = BuildLambdaExpression(propertyFilter, filterFilter, operatorSymbol); + var lambda = BuildLambdaExpression(property, filter, operatorSymbol, isCaseInsensitiveForValues, isMaterializedQueryable); // Act var func = lambda.Compile(); @@ -255,19 +353,34 @@ public class FilterOperatorMapperTests } [Theory] - [InlineData("", "", false)] - [InlineData(" ", " ", false)] - [InlineData("123", "123", false)] - [InlineData("foo", "foo", false)] - [InlineData("foo", "FOO", true)] - [InlineData("foo", "bar", true)] - [InlineData("foobar", "foo", true)] - [InlineData("barfoo", "foo", false)] - public void Should_work_on_does_not_end_with_operator(string propertyFilter, string filterFilter, bool expectedEqual) + [InlineData("", "", false, false, false)] + [InlineData(" ", " ", false, false, false)] + [InlineData("123", "123", false, false, false)] + [InlineData("foo", "foo", false, false, false)] + [InlineData("foo", "FOO", false, false, true)] + [InlineData("FOO", "foo", false, false, true)] + [InlineData("foo", "bar", false, false, true)] + [InlineData("foobar", "foo", false, false, true)] + [InlineData("barfoo", "foo", false, false, false)] + [InlineData("", "", true, true, false)] + [InlineData(" ", " ", true, true, false)] + [InlineData("123", "123", true, true, false)] + [InlineData("foo", "foo", true, true, false)] + [InlineData("foo", "FOO", true, true, false)] + [InlineData("FOO", "foo", true, true, false)] + [InlineData("foo", "bar", true, true, true)] + [InlineData("foobar", "foo", true, true, true)] + [InlineData("barfoo", "foo", true, true, false)] + public void Should_work_on_does_not_end_with_operator( + string property, + string filter, + bool isCaseInsensitiveForValues, + bool isMaterializedQueryable, + bool expectedEqual) { // Arrange var operatorSymbol = FilterOperatorSymbols.DoesNotEndWith; - var lambda = BuildLambdaExpression(propertyFilter, filterFilter, operatorSymbol); + var lambda = BuildLambdaExpression(property, filter, operatorSymbol, isCaseInsensitiveForValues, isMaterializedQueryable); // Act var func = lambda.Compile(); @@ -278,19 +391,33 @@ public class FilterOperatorMapperTests } [Theory] - [InlineData("", "", true)] - [InlineData(" ", " ", true)] - [InlineData("123", "123", true)] - [InlineData("foo", "foo", true)] - [InlineData("foo", "FOO", true)] - [InlineData("foo", "bar", false)] - [InlineData("foobar", "foo", true)] - [InlineData("barfoo", "foo", true)] - public void Should_work_on_contains_case_insensitive_operator(string propertyFilter, string filterFilter, bool expectedEqual) + [InlineData("", "", false, true)] + [InlineData(" ", " ", false, true)] + [InlineData("123", "123", false, true)] + [InlineData("foo", "foo", false, true)] + [InlineData("foo", "FOO", false, false)] + [InlineData("FOO", "foo", false, false)] + [InlineData("foo", "bar", false, false)] + [InlineData("foobar", "foo", false, true)] + [InlineData("barfoo", "foo", false, true)] + [InlineData("", "", true, true)] + [InlineData(" ", " ", true, true)] + [InlineData("123", "123", true, true)] + [InlineData("foo", "foo", true, true)] + [InlineData("foo", "FOO", true, true)] + [InlineData("FOO", "foo", true, true)] + [InlineData("foo", "bar", true, false)] + [InlineData("foobar", "foo", true, true)] + [InlineData("barfoo", "foo", true, true)] + public void Should_work_on_contains_case_insensitive_operator( + string property, + string filter, + bool isMaterializedQueryable, + bool expectedEqual) { // Arrange var operatorSymbol = FilterOperatorSymbols.ContainsCaseInsensitive; - var lambda = BuildLambdaExpression(propertyFilter, filterFilter, operatorSymbol); + var lambda = BuildLambdaExpression(property, filter, operatorSymbol, isMaterializedQueryable: isMaterializedQueryable); // Act var func = lambda.Compile(); @@ -301,19 +428,33 @@ public class FilterOperatorMapperTests } [Theory] - [InlineData("", "", false)] - [InlineData(" ", " ", false)] - [InlineData("123", "123", false)] - [InlineData("foo", "foo", false)] - [InlineData("foo", "FOO", false)] - [InlineData("foo", "bar", true)] - [InlineData("foobar", "foo", false)] - [InlineData("barfoo", "foo", false)] - public void Should_work_on_does_not_contain_case_insensitive_operator(string propertyFilter, string filterFilter, bool expectedEqual) + [InlineData("", "", false, false)] + [InlineData(" ", " ", false, false)] + [InlineData("123", "123", false, false)] + [InlineData("foo", "foo", false, false)] + [InlineData("foo", "FOO", false, true)] + [InlineData("FOO", "foo", false, true)] + [InlineData("foo", "bar", false, true)] + [InlineData("foobar", "foo", false, false)] + [InlineData("barfoo", "foo", false, false)] + [InlineData("", "", true, false)] + [InlineData(" ", " ", true, false)] + [InlineData("123", "123", true, false)] + [InlineData("foo", "foo", true, false)] + [InlineData("foo", "FOO", true, false)] + [InlineData("FOO", "foo", true, false)] + [InlineData("foo", "bar", true, true)] + [InlineData("foobar", "foo", true, false)] + [InlineData("barfoo", "foo", true, false)] + public void Should_work_on_does_not_contain_case_insensitive_operator( + string property, + string filter, + bool isMaterializedQueryable, + bool expectedEqual) { // Arrange var operatorSymbol = FilterOperatorSymbols.DoesNotContainCaseInsensitive; - var lambda = BuildLambdaExpression(propertyFilter, filterFilter, operatorSymbol); + var lambda = BuildLambdaExpression(property, filter, operatorSymbol, isMaterializedQueryable: isMaterializedQueryable); // Act var func = lambda.Compile(); @@ -324,19 +465,33 @@ public class FilterOperatorMapperTests } [Theory] - [InlineData("", "", true)] - [InlineData(" ", " ", true)] - [InlineData("123", "123", true)] - [InlineData("foo", "foo", true)] - [InlineData("foo", "FOO", true)] - [InlineData("foo", "bar", false)] - [InlineData("foobar", "foo", true)] - [InlineData("barfoo", "foo", false)] - public void Should_work_on_starts_with_case_insensitive_operator(string propertyFilter, string filterFilter, bool expectedEqual) + [InlineData("", "", false, true)] + [InlineData(" ", " ", false, true)] + [InlineData("123", "123", false, true)] + [InlineData("foo", "foo", false, true)] + [InlineData("foo", "FOO", false, false)] + [InlineData("FOO", "foo", false, false)] + [InlineData("foo", "bar", false, false)] + [InlineData("foobar", "foo", false, true)] + [InlineData("barfoo", "foo", false, false)] + [InlineData("", "", true, true)] + [InlineData(" ", " ", true, true)] + [InlineData("123", "123", true, true)] + [InlineData("foo", "foo", true, true)] + [InlineData("foo", "FOO", true, true)] + [InlineData("FOO", "foo", true, true)] + [InlineData("foo", "bar", true, false)] + [InlineData("foobar", "foo", true, true)] + [InlineData("barfoo", "foo", true, false)] + public void Should_work_on_starts_with_case_insensitive_operator( + string property, + string filter, + bool isMaterializedQueryable, + bool expectedEqual) { // Arrange var operatorSymbol = FilterOperatorSymbols.StartsWithCaseInsensitive; - var lambda = BuildLambdaExpression(propertyFilter, filterFilter, operatorSymbol); + var lambda = BuildLambdaExpression(property, filter, operatorSymbol, isMaterializedQueryable: isMaterializedQueryable); // Act var func = lambda.Compile(); @@ -347,19 +502,33 @@ public class FilterOperatorMapperTests } [Theory] - [InlineData("", "", false)] - [InlineData(" ", " ", false)] - [InlineData("123", "123", false)] - [InlineData("foo", "foo", false)] - [InlineData("foo", "FOO", false)] - [InlineData("foo", "bar", true)] - [InlineData("foobar", "foo", false)] - [InlineData("barfoo", "foo", true)] - public void Should_work_on_does_not_start_with_case_insensitive_operator(string propertyFilter, string filterFilter, bool expectedEqual) + [InlineData("", "", false, false)] + [InlineData(" ", " ", false, false)] + [InlineData("123", "123", false, false)] + [InlineData("foo", "foo", false, false)] + [InlineData("foo", "FOO", false, true)] + [InlineData("FOO", "foo", false, true)] + [InlineData("foo", "bar", false, true)] + [InlineData("foobar", "foo", false, false)] + [InlineData("barfoo", "foo", false, true)] + [InlineData("", "", true, false)] + [InlineData(" ", " ", true, false)] + [InlineData("123", "123", true, false)] + [InlineData("foo", "foo", true, false)] + [InlineData("foo", "FOO", true, false)] + [InlineData("FOO", "foo", true, false)] + [InlineData("foo", "bar", true, true)] + [InlineData("foobar", "foo", true, false)] + [InlineData("barfoo", "foo", true, true)] + public void Should_work_on_does_not_start_with_case_insensitive_operator( + string property, + string filter, + bool isMaterializedQueryable, + bool expectedEqual) { // Arrange var operatorSymbol = FilterOperatorSymbols.DoesNotStartWithCaseInsensitive; - var lambda = BuildLambdaExpression(propertyFilter, filterFilter, operatorSymbol); + var lambda = BuildLambdaExpression(property, filter, operatorSymbol, isMaterializedQueryable: isMaterializedQueryable); // Act var func = lambda.Compile(); @@ -370,19 +539,33 @@ public class FilterOperatorMapperTests } [Theory] - [InlineData("", "", true)] - [InlineData(" ", " ", true)] - [InlineData("123", "123", true)] - [InlineData("foo", "foo", true)] - [InlineData("foo", "FOO", true)] - [InlineData("foo", "bar", false)] - [InlineData("foobar", "foo", false)] - [InlineData("barfoo", "foo", true)] - public void Should_work_on_ends_with_case_insensitive_operator(string propertyFilter, string filterFilter, bool expectedEqual) + [InlineData("", "", false, true)] + [InlineData(" ", " ", false, true)] + [InlineData("123", "123", false, true)] + [InlineData("foo", "foo", false, true)] + [InlineData("foo", "FOO", false, false)] + [InlineData("FOO", "foo", false, false)] + [InlineData("foo", "bar", false, false)] + [InlineData("foobar", "foo", false, false)] + [InlineData("barfoo", "foo", false, true)] + [InlineData("", "", true, true)] + [InlineData(" ", " ", true, true)] + [InlineData("123", "123", true, true)] + [InlineData("foo", "foo", true, true)] + [InlineData("foo", "FOO", true, true)] + [InlineData("FOO", "foo", true, true)] + [InlineData("foo", "bar", true, false)] + [InlineData("foobar", "foo", true, false)] + [InlineData("barfoo", "foo", true, true)] + public void Should_work_on_ends_with_case_insensitive_operator( + string property, + string filter, + bool isMaterializedQueryable, + bool expectedEqual) { // Arrange var operatorSymbol = FilterOperatorSymbols.EndsWithCaseInsensitive; - var lambda = BuildLambdaExpression(propertyFilter, filterFilter, operatorSymbol); + var lambda = BuildLambdaExpression(property, filter, operatorSymbol, isMaterializedQueryable: isMaterializedQueryable); // Act var func = lambda.Compile(); @@ -393,19 +576,33 @@ public class FilterOperatorMapperTests } [Theory] - [InlineData("", "", false)] - [InlineData(" ", " ", false)] - [InlineData("123", "123", false)] - [InlineData("foo", "foo", false)] - [InlineData("foo", "FOO", false)] - [InlineData("foo", "bar", true)] - [InlineData("foobar", "foo", true)] - [InlineData("barfoo", "foo", false)] - public void Should_work_on_does_not_end_with_case_insensitive_operator(string propertyFilter, string filterFilter, bool expectedEqual) + [InlineData("", "", false, false)] + [InlineData(" ", " ", false, false)] + [InlineData("123", "123", false, false)] + [InlineData("foo", "foo", false, false)] + [InlineData("foo", "FOO", false, true)] + [InlineData("FOO", "foo", false, true)] + [InlineData("foo", "bar", false, true)] + [InlineData("foobar", "foo", false, true)] + [InlineData("barfoo", "foo", false, false)] + [InlineData("", "", true, false)] + [InlineData(" ", " ", true, false)] + [InlineData("123", "123", true, false)] + [InlineData("foo", "foo", true, false)] + [InlineData("foo", "FOO", true, false)] + [InlineData("FOO", "foo", true, false)] + [InlineData("foo", "bar", true, true)] + [InlineData("foobar", "foo", true, true)] + [InlineData("barfoo", "foo", true, false)] + public void Should_work_on_does_not_end_with_case_insensitive_operator( + string property, + string filter, + bool isMaterializedQueryable, + bool expectedEqual) { // Arrange var operatorSymbol = FilterOperatorSymbols.DoesNotEndWithCaseInsensitive; - var lambda = BuildLambdaExpression(propertyFilter, filterFilter, operatorSymbol); + var lambda = BuildLambdaExpression(property, filter, operatorSymbol, isMaterializedQueryable: isMaterializedQueryable); // Act var func = lambda.Compile(); @@ -416,20 +613,35 @@ public class FilterOperatorMapperTests } [Theory] - [InlineData(null, null, true)] - [InlineData("foo", null, false)] - [InlineData(null, "foo", false)] - [InlineData("", "", true)] - [InlineData(" ", " ", true)] - [InlineData("123", "123", true)] - [InlineData("foo", "foo", true)] - [InlineData("foo", "FOO", true)] - [InlineData("foo", "bar", false)] - public void Should_work_on_equal_case_insensitive_operator(string value1, string value2, bool expectedEqual) + [InlineData(null, null, false, true)] + [InlineData("foo", null, false, false)] + [InlineData(null, "foo", false, false)] + [InlineData("", "", false, true)] + [InlineData(" ", " ", false, true)] + [InlineData("123", "123", false, true)] + [InlineData("foo", "foo", false, true)] + [InlineData("foo", "FOO", false, false)] + [InlineData("FOO", "foo", false, false)] + [InlineData("foo", "bar", false, false)] + [InlineData(null, null, true, true)] + [InlineData("foo", null, true, false)] + [InlineData(null, "foo", true, false)] + [InlineData("", "", true, true)] + [InlineData(" ", " ", true, true)] + [InlineData("123", "123", true, true)] + [InlineData("foo", "foo", true, true)] + [InlineData("foo", "FOO", true, true)] + [InlineData("FOO", "foo", true, true)] + [InlineData("foo", "bar", true, false)] + public void Should_work_on_equal_case_insensitive_operator( + string property, + string filter, + bool isMaterializedQueryable, + bool expectedEqual) { // Arrange var operatorSymbol = FilterOperatorSymbols.EqualsCaseInsensitive; - var lambda = BuildLambdaExpression(value1, value2, operatorSymbol); + var lambda = BuildLambdaExpression(property, filter, operatorSymbol, isMaterializedQueryable: isMaterializedQueryable); // Act var func = lambda.Compile(); @@ -440,20 +652,35 @@ public class FilterOperatorMapperTests } [Theory] - [InlineData(null, null, false)] - [InlineData("foo", null, true)] - [InlineData(null, "foo", true)] - [InlineData("", "", false)] - [InlineData(" ", " ", false)] - [InlineData("123", "123", false)] - [InlineData("foo", "foo", false)] - [InlineData("foo", "FOO", false)] - [InlineData("foo", "bar", true)] - public void Should_work_on_does_not_equal_case_insensitive_operator(string value1, string value2, bool expectedEqual) + [InlineData(null, null, false, false)] + [InlineData("foo", null, false, true)] + [InlineData(null, "foo", false, true)] + [InlineData("", "", false, false)] + [InlineData(" ", " ", false, false)] + [InlineData("123", "123", false, false)] + [InlineData("foo", "foo", false, false)] + [InlineData("foo", "FOO", false, true)] + [InlineData("FOO", "foo", false, true)] + [InlineData("foo", "bar", false, true)] + [InlineData(null, null, true, false)] + [InlineData("foo", null, true, true)] + [InlineData(null, "foo", true, true)] + [InlineData("", "", true, false)] + [InlineData(" ", " ", true, false)] + [InlineData("123", "123", true, false)] + [InlineData("foo", "foo", true, false)] + [InlineData("foo", "FOO", true, false)] + [InlineData("FOO", "foo", true, false)] + [InlineData("foo", "bar", true, true)] + public void Should_work_on_does_not_equal_case_insensitive_operator( + string property, + string filter, + bool isMaterializedQueryable, + bool expectedEqual) { // Arrange var operatorSymbol = FilterOperatorSymbols.DoesNotEqualCaseInsensitive; - var lambda = BuildLambdaExpression(value1, value2, operatorSymbol); + var lambda = BuildLambdaExpression(property, filter, operatorSymbol, isMaterializedQueryable: isMaterializedQueryable); // Act var func = lambda.Compile(); @@ -468,8 +695,7 @@ public class FilterOperatorMapperTests T filter, string operatorSymbol, bool isCaseInsensitiveForValues = false, - bool isMaterializedQueryable = true, - bool? isStringBasedProperty = null) + bool isMaterializedQueryable = true) { var filterOperator = DefaultOperators[operatorSymbol]; var propertyValue = Expression.Constant(property, typeof(T)); @@ -479,7 +705,7 @@ public class FilterOperatorMapperTests propertyValue, isCaseInsensitiveForValues, isMaterializedQueryable, - isStringBasedProperty ?? typeof(T) == typeof(string)); + isStringBasedProperty: typeof(T) == typeof(string)); var expression = filterOperator.ExpressionProvider.Invoke(filterExpressionContext); return Expression.Lambda>(expression); -- GitLab From 62f7d19bad42a5381fef5696acaabb5dc73bac83 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Tue, 28 Jan 2025 21:19:22 +0100 Subject: [PATCH 40/56] Add unit tests for Stariner module builder factory --- .../StrainerModuleBuilderFactoryTests.cs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 test/Strainer.UnitTests/Services/Configuration/StrainerModuleBuilderFactoryTests.cs diff --git a/test/Strainer.UnitTests/Services/Configuration/StrainerModuleBuilderFactoryTests.cs b/test/Strainer.UnitTests/Services/Configuration/StrainerModuleBuilderFactoryTests.cs new file mode 100644 index 00000000..f6007ebf --- /dev/null +++ b/test/Strainer.UnitTests/Services/Configuration/StrainerModuleBuilderFactoryTests.cs @@ -0,0 +1,64 @@ +using Fluorite.Strainer.Exceptions; +using Fluorite.Strainer.Models; +using Fluorite.Strainer.Services; +using Fluorite.Strainer.Services.Configuration; +using Fluorite.Strainer.Services.Metadata; +using Fluorite.Strainer.Services.Modules; + +namespace Fluorite.Strainer.UnitTests.Services.Configuration; + +public class StrainerModuleBuilderFactoryTests +{ + private readonly IPropertyInfoProvider _propertyInfoProviderMock = Substitute.For(); + private readonly IStrainerOptionsProvider _strainerOptionsProviderMock = Substitute.For(); + + private readonly StrainerModuleBuilderFactory _factory; + + public StrainerModuleBuilderFactoryTests() + { + _factory = new StrainerModuleBuilderFactory( + _propertyInfoProviderMock, + _strainerOptionsProviderMock); + } + + [Fact] + public void Should_CreateModuleBuilder() + { + // Arrange + var moduleTypeParameter = typeof(int); + var strainerModule = Substitute.For(); + var strainerOptions = new StrainerOptions(); + + _strainerOptionsProviderMock.GetStrainerOptions().Returns(strainerOptions); + + // Act + var result = _factory.Create(moduleTypeParameter, strainerModule); + + // Assert + result.Should().NotBeNull(); + result.Should().BeOfType>(); + + _strainerOptionsProviderMock.Received(1).GetStrainerOptions(); + } + + [Fact] + public void Should_Throw_ForNotRuntimeType() + { + // Arrange + var moduleTypeParameter = Substitute.For(); + var strainerModule = Substitute.For(); + var strainerOptions = new StrainerOptions(); + + _strainerOptionsProviderMock.GetStrainerOptions().Returns(strainerOptions); + + // Act + Action act = () => _factory.Create(moduleTypeParameter, strainerModule); + + // Assert + act.Should().ThrowExactly() + .WithMessage($"Unable to create a module builder for module of type {strainerModule.GetType().FullName}.") + .WithInnerExceptionExactly(); + + _strainerOptionsProviderMock.Received(1).GetStrainerOptions(); + } +} -- GitLab From 852e22a112b199527f5d758f57771c3ab4b39e03 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Tue, 28 Jan 2025 21:24:18 +0100 Subject: [PATCH 41/56] Add unit tests for filter expression workflow builder --- .../FilterExpressionWorkflowBuilderTests.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 test/Strainer.UnitTests/Services/Filtering/Steps/FilterExpressionWorkflowBuilderTests.cs diff --git a/test/Strainer.UnitTests/Services/Filtering/Steps/FilterExpressionWorkflowBuilderTests.cs b/test/Strainer.UnitTests/Services/Filtering/Steps/FilterExpressionWorkflowBuilderTests.cs new file mode 100644 index 00000000..98609e56 --- /dev/null +++ b/test/Strainer.UnitTests/Services/Filtering/Steps/FilterExpressionWorkflowBuilderTests.cs @@ -0,0 +1,23 @@ +using Fluorite.Strainer.Services.Filtering; +using Fluorite.Strainer.Services.Filtering.Steps; + +namespace Fluorite.Strainer.UnitTests.Services.Filtering.Steps; + +public class FilterExpressionWorkflowBuilderTests +{ + [Fact] + public void Should_Return_Workflow() + { + // Act + var result = new FilterExpressionWorkflowBuilder( + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For()); + + // Assert + result.Should().NotBeNull(); + } +} -- GitLab From 19c4a59eecef6f2da168d9c658808793a2a0a0cc Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Tue, 28 Jan 2025 21:54:51 +0100 Subject: [PATCH 42/56] Update test case, so it calls build --- .../Steps/FilterExpressionWorkflowBuilderTests.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/Strainer.UnitTests/Services/Filtering/Steps/FilterExpressionWorkflowBuilderTests.cs b/test/Strainer.UnitTests/Services/Filtering/Steps/FilterExpressionWorkflowBuilderTests.cs index 98609e56..f5dace03 100644 --- a/test/Strainer.UnitTests/Services/Filtering/Steps/FilterExpressionWorkflowBuilderTests.cs +++ b/test/Strainer.UnitTests/Services/Filtering/Steps/FilterExpressionWorkflowBuilderTests.cs @@ -8,8 +8,8 @@ public class FilterExpressionWorkflowBuilderTests [Fact] public void Should_Return_Workflow() { - // Act - var result = new FilterExpressionWorkflowBuilder( + // Arrange + var builder = new FilterExpressionWorkflowBuilder( Substitute.For(), Substitute.For(), Substitute.For(), @@ -17,7 +17,11 @@ public class FilterExpressionWorkflowBuilderTests Substitute.For(), Substitute.For()); + // Act + var result = builder.BuildDefaultWorkflow(); + // Assert result.Should().NotBeNull(); + result.Should().BeOfType(); } } -- GitLab From 5aef5d62ac71cae0fbba26504a0aecacdbe9022b Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Wed, 29 Jan 2025 00:28:45 +0100 Subject: [PATCH 43/56] Cover missing test case in suffix sorting way formatter --- .../Sorting/SuffixSortingWayFormatterTests.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/Strainer.UnitTests/Services/Sorting/SuffixSortingWayFormatterTests.cs b/test/Strainer.UnitTests/Services/Sorting/SuffixSortingWayFormatterTests.cs index 59fa606b..53500350 100644 --- a/test/Strainer.UnitTests/Services/Sorting/SuffixSortingWayFormatterTests.cs +++ b/test/Strainer.UnitTests/Services/Sorting/SuffixSortingWayFormatterTests.cs @@ -29,6 +29,21 @@ public class SuffixSortingWayFormatterTests act.Should().ThrowExactly(); } + [Fact] + public void Formatter_Throws_ForInvalidSortingWay() + { + // Arrange + var input = "foo"; + SortingWay sortingWay = default; + + // Act + Action act = () => _formatter.Format(input, sortingWay); + + // Assert + act.Should().ThrowExactly() + .WithMessage($"{nameof(sortingWay)} with value '{sortingWay}' is not supported."); + } + [Theory] [InlineData("", SortingWay.Ascending, "")] [InlineData(" ", SortingWay.Ascending, " ")] -- GitLab From b6264ceebe65b8801e11a0941980adbd6079b334 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Wed, 29 Jan 2025 00:45:59 +0100 Subject: [PATCH 44/56] Update XML docs, remove outdated exception information --- .../StrainerServiceCollectionExtensions.cs | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/src/Strainer.AspNetCore/Extensions/DependencyInjection/StrainerServiceCollectionExtensions.cs b/src/Strainer.AspNetCore/Extensions/DependencyInjection/StrainerServiceCollectionExtensions.cs index 4c368a2e..9894025c 100644 --- a/src/Strainer.AspNetCore/Extensions/DependencyInjection/StrainerServiceCollectionExtensions.cs +++ b/src/Strainer.AspNetCore/Extensions/DependencyInjection/StrainerServiceCollectionExtensions.cs @@ -47,10 +47,6 @@ public static class StrainerServiceCollectionExtensions /// /// is . /// - /// - /// Another Strainer processor was already registered within the - /// current . - /// public static IServiceCollection AddStrainer( this IServiceCollection services, ServiceLifetime serviceLifetime = DefaultServiceLifetime) @@ -87,10 +83,6 @@ public static class StrainerServiceCollectionExtensions /// /// is . /// - /// - /// Another Strainer processor was already registered within the - /// current . - /// public static IServiceCollection AddStrainer( this IServiceCollection services, Assembly[] assembliesToScan, @@ -129,10 +121,6 @@ public static class StrainerServiceCollectionExtensions /// /// is . /// - /// - /// Another Strainer processor was already registered within the - /// current . - /// public static IServiceCollection AddStrainer( this IServiceCollection services, IReadOnlyCollection moduleTypes, @@ -169,10 +157,6 @@ public static class StrainerServiceCollectionExtensions /// /// is . /// - /// - /// Another Strainer processor was already registered within the - /// current . - /// public static IServiceCollection AddStrainer( this IServiceCollection services, IConfiguration configuration, @@ -213,10 +197,6 @@ public static class StrainerServiceCollectionExtensions /// /// is . /// - /// - /// Another Strainer processor was already registered within the - /// current . - /// public static IServiceCollection AddStrainer( this IServiceCollection services, IConfiguration configuration, @@ -266,10 +246,6 @@ public static class StrainerServiceCollectionExtensions /// /// is . /// - /// - /// Another Strainer processor was already registered within the - /// current . - /// public static IServiceCollection AddStrainer( this IServiceCollection services, IConfiguration configuration, @@ -309,10 +285,6 @@ public static class StrainerServiceCollectionExtensions /// /// is . /// - /// - /// Another Strainer processor was already registered within the - /// current . - /// public static IServiceCollection AddStrainer( this IServiceCollection services, Action configure, @@ -353,10 +325,6 @@ public static class StrainerServiceCollectionExtensions /// /// is . /// - /// - /// Another Strainer processor was already registered within the - /// current . - /// public static IServiceCollection AddStrainer( this IServiceCollection services, Action configure, @@ -406,10 +374,6 @@ public static class StrainerServiceCollectionExtensions /// /// is . /// - /// - /// Another Strainer processor was already registered within the - /// current . - /// public static IServiceCollection AddStrainer( this IServiceCollection services, Action configure, -- GitLab From 92a4a4fc6b75dfa867a5c66627452be35dbed5b3 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sun, 16 Feb 2025 17:57:07 +0100 Subject: [PATCH 45/56] Add missing test case for filter operators --- .../Services/Filtering/FilterOperatorMapperTests.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/Strainer.UnitTests/Services/Filtering/FilterOperatorMapperTests.cs b/test/Strainer.UnitTests/Services/Filtering/FilterOperatorMapperTests.cs index b886a74c..fbac93d9 100644 --- a/test/Strainer.UnitTests/Services/Filtering/FilterOperatorMapperTests.cs +++ b/test/Strainer.UnitTests/Services/Filtering/FilterOperatorMapperTests.cs @@ -219,6 +219,15 @@ public class FilterOperatorMapperTests [InlineData("foo", "bar", false, false, true)] [InlineData("foobar", "foo", false, false, false)] [InlineData("barfoo", "foo", false, false, false)] + [InlineData("", "", true, true, false)] + [InlineData(" ", " ", true, true, false)] + [InlineData("123", "123", true, true, false)] + [InlineData("foo", "foo", true, true, false)] + [InlineData("foo", "FOO", true, true, false)] + [InlineData("FOO", "foo", true, true, false)] + [InlineData("foo", "bar", true, true, true)] + [InlineData("foobar", "foo", true, true, false)] + [InlineData("barfoo", "foo", true, true, false)] public void Should_work_on_does_not_contain_operator( string property, string filter, -- GitLab From 2b29ee2102b2b686604106a8098c88ece2af546b Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sun, 16 Feb 2025 18:01:23 +0100 Subject: [PATCH 46/56] Add missing tests case for mitigate case insensitibity step --- .../MitigateCaseInsensitivityStepTests.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/Strainer.UnitTests/Services/Filtering/Steps/MitigateCaseInsensitivityStepTests.cs b/test/Strainer.UnitTests/Services/Filtering/Steps/MitigateCaseInsensitivityStepTests.cs index 793da276..f6914ed2 100644 --- a/test/Strainer.UnitTests/Services/Filtering/Steps/MitigateCaseInsensitivityStepTests.cs +++ b/test/Strainer.UnitTests/Services/Filtering/Steps/MitigateCaseInsensitivityStepTests.cs @@ -20,6 +20,43 @@ public class MitigateCaseInsensitivityStepTests _step = new MitigateCaseInsensitivityStep(_optionsProviderMock); } + [Fact] + public void Should_DoNothing_WhenQueryIsAlreadyMaterialized() + { + // Arrange + var type = typeof(Version); + var propertyInfoMock = Substitute.For(); + propertyInfoMock + .PropertyType + .Returns(type); + var metadataMock = Substitute.For(); + metadataMock + .PropertyInfo + .Returns(propertyInfoMock); + var filterOperatorMock = Substitute.For(); + var filterTermMock = Substitute.For(); + filterTermMock + .Operator + .Returns(filterOperatorMock); + var context = new FilterExpressionWorkflowContext + { + PropertyMetadata = metadataMock, + Term = filterTermMock, + }; + var options = new StrainerOptions(); + + _optionsProviderMock + .GetStrainerOptions() + .Returns(options); + + // Act + _step.Execute(context); + + // Assert + context.PropertyValue.Should().BeNull(); + context.FinalExpression.Should().BeNull(); + } + [Fact] public void Should_DoNothing_WhenPropertyIsNotStringType() { -- GitLab From 4ade67621b6d0ecd9cc314a9142c27c58cc083cb Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sun, 16 Feb 2025 18:08:55 +0100 Subject: [PATCH 47/56] Set missing flag in test case --- .../Filtering/Steps/MitigateCaseInsensitivityStepTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Strainer.UnitTests/Services/Filtering/Steps/MitigateCaseInsensitivityStepTests.cs b/test/Strainer.UnitTests/Services/Filtering/Steps/MitigateCaseInsensitivityStepTests.cs index f6914ed2..d884fba2 100644 --- a/test/Strainer.UnitTests/Services/Filtering/Steps/MitigateCaseInsensitivityStepTests.cs +++ b/test/Strainer.UnitTests/Services/Filtering/Steps/MitigateCaseInsensitivityStepTests.cs @@ -40,6 +40,7 @@ public class MitigateCaseInsensitivityStepTests .Returns(filterOperatorMock); var context = new FilterExpressionWorkflowContext { + IsMaterializedQueryable = true, PropertyMetadata = metadataMock, Term = filterTermMock, }; -- GitLab From 6971ce26e2ad804af30891c0c43b58387c217dec Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sun, 16 Feb 2025 19:37:43 +0100 Subject: [PATCH 48/56] Add more test cases for filter term parser tests --- .../Filtering/FilterTermParserTests.cs | 264 +++++++++++------- 1 file changed, 167 insertions(+), 97 deletions(-) diff --git a/test/Strainer.UnitTests/Services/Filtering/FilterTermParserTests.cs b/test/Strainer.UnitTests/Services/Filtering/FilterTermParserTests.cs index 8d937961..09b6fd5e 100644 --- a/test/Strainer.UnitTests/Services/Filtering/FilterTermParserTests.cs +++ b/test/Strainer.UnitTests/Services/Filtering/FilterTermParserTests.cs @@ -6,23 +6,15 @@ namespace Fluorite.Strainer.UnitTests.Services.Filtering; public class FilterTermParserTests { - [Fact] - public void Parser_ReturnsNoFilterTerms_When_InputIsNull() + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData(",")] + public void Parser_ReturnsNoFilterTerms_When_InputIsNullEmptyOrWhitespace(string input) { // Arrange - string input = null; - var filterOperators = FilterOperatorMapper.DefaultOperators; - var strainerConfigurationMock = Substitute.For(); - strainerConfigurationMock - .FilterOperators - .Returns(filterOperators); - var strainerConfigurationProvider = new StrainerConfigurationProvider(strainerConfigurationMock); - var filterOperatorsProvider = new ConfigurationFilterOperatorsProvider(strainerConfigurationProvider); - var operatorParser = new FilterOperatorParser(filterOperatorsProvider); - var namesParser = new FilterTermNamesParser(); - var valuesParser = new FilterTermValuesParser(); - var sectionsParser = new FilterTermSectionsParser(filterOperatorsProvider); - var termParser = new FilterTermParser(operatorParser, namesParser, valuesParser, sectionsParser); + var termParser = BuildFilterTermParser(); // Act var filterTermList = termParser.GetParsedTerms(input); @@ -32,22 +24,11 @@ public class FilterTermParserTests } [Fact] - public void Parser_ReturnsNoFilterTerms_When_InputIsEmpty() + public void Parser_ReturnsNoFilterTerms_When_ThereIsNoName() { // Arrange - var input = string.Empty; - var filterOperators = FilterOperatorMapper.DefaultOperators; - var strainerConfigurationMock = Substitute.For(); - strainerConfigurationMock - .FilterOperators - .Returns(filterOperators); - var strainerConfigurationProvider = new StrainerConfigurationProvider(strainerConfigurationMock); - var filterOperatorsProvider = new ConfigurationFilterOperatorsProvider(strainerConfigurationProvider); - var operatorParser = new FilterOperatorParser(filterOperatorsProvider); - var namesParser = new FilterTermNamesParser(); - var valuesParser = new FilterTermValuesParser(); - var sectionsParser = new FilterTermSectionsParser(filterOperatorsProvider); - var termParser = new FilterTermParser(operatorParser, namesParser, valuesParser, sectionsParser); + var input = "==SomeValue"; + var termParser = BuildFilterTermParser(); // Act var filterTermList = termParser.GetParsedTerms(input); @@ -57,22 +38,11 @@ public class FilterTermParserTests } [Fact] - public void Parser_ReturnsNoFilterTerms_When_InputIsOnlyWhitespace() + public void Parser_ReturnsNoFilterTerms_When_ThereIsOnlyOperator() { // Arrange - var input = " "; - var filterOperators = FilterOperatorMapper.DefaultOperators; - var strainerConfigurationMock = Substitute.For(); - strainerConfigurationMock - .FilterOperators - .Returns(filterOperators); - var strainerConfigurationProvider = new StrainerConfigurationProvider(strainerConfigurationMock); - var filterOperatorsProvider = new ConfigurationFilterOperatorsProvider(strainerConfigurationProvider); - var operatorParser = new FilterOperatorParser(filterOperatorsProvider); - var namesParser = new FilterTermNamesParser(); - var valuesParser = new FilterTermValuesParser(); - var sectionsParser = new FilterTermSectionsParser(filterOperatorsProvider); - var termParser = new FilterTermParser(operatorParser, namesParser, valuesParser, sectionsParser); + var input = "=="; + var termParser = BuildFilterTermParser(); // Act var filterTermList = termParser.GetParsedTerms(input); @@ -82,102 +52,202 @@ public class FilterTermParserTests } [Fact] - public void Parser_ReturnsNoFilterTerms_When_ThereIsNoName() + public void Parser_ReturnsFilterTerm_When_ThereIsOnlyName() { // Arrange - var input = "==SomeValue"; - var filterOperators = FilterOperatorMapper.DefaultOperators; - var strainerConfigurationMock = Substitute.For(); - strainerConfigurationMock - .FilterOperators - .Returns(filterOperators); - var strainerConfigurationProvider = new StrainerConfigurationProvider(strainerConfigurationMock); - var filterOperatorsProvider = new ConfigurationFilterOperatorsProvider(strainerConfigurationProvider); - var operatorParser = new FilterOperatorParser(filterOperatorsProvider); - var namesParser = new FilterTermNamesParser(); - var valuesParser = new FilterTermValuesParser(); - var sectionsParser = new FilterTermSectionsParser(filterOperatorsProvider); - var termParser = new FilterTermParser(operatorParser, namesParser, valuesParser, sectionsParser); + var input = "FilterName"; + var termParser = BuildFilterTermParser(); // Act var filterTermList = termParser.GetParsedTerms(input); // Assert - filterTermList.Should().BeEmpty(); + filterTermList.Should().HaveCount(1); + filterTermList[0].Input.Should().BeSameAs(input); + filterTermList[0].Names.Should().BeEquivalentTo(input); + filterTermList[0].Operator.Should().BeNull(); + filterTermList[0].Values.Should().NotBeNull().And.BeEmpty(); } [Fact] - public void Parser_ReturnsNoFilterTerms_When_ThereIsOnlyOperator() + public void Parser_ReturnsFilterTerm_When_ThereIsNameWithOperator() { // Arrange - var input = "=="; - var filterOperators = FilterOperatorMapper.DefaultOperators; - var strainerConfigurationMock = Substitute.For(); - strainerConfigurationMock - .FilterOperators - .Returns(filterOperators); - var strainerConfigurationProvider = new StrainerConfigurationProvider(strainerConfigurationMock); - var filterOperatorsProvider = new ConfigurationFilterOperatorsProvider(strainerConfigurationProvider); - var operatorParser = new FilterOperatorParser(filterOperatorsProvider); - var namesParser = new FilterTermNamesParser(); - var valuesParser = new FilterTermValuesParser(); - var sectionsParser = new FilterTermSectionsParser(filterOperatorsProvider); - var termParser = new FilterTermParser(operatorParser, namesParser, valuesParser, sectionsParser); + var name = "FilterName"; + var filterOperator = "=="; + var input = $"{name}{filterOperator}"; + var termParser = BuildFilterTermParser(); // Act var filterTermList = termParser.GetParsedTerms(input); // Assert - filterTermList.Should().BeEmpty(); + filterTermList.Should().HaveCount(1); + filterTermList[0].Input.Should().BeSameAs(input); + filterTermList[0].Names.Should().BeEquivalentTo(name); + filterTermList[0].Operator.Should().NotBeNull(); + filterTermList[0].Operator.Symbol.Should().Be(filterOperator); + filterTermList[0].Values.Should().NotBeNull().And.BeEmpty(); } [Fact] - public void Parser_ReturnsFilterTerm_When_ThereIsOnlyName() + public void Parser_ReturnsFilterTerm() { // Arrange - var input = "CustomFilterName"; - var filterOperators = FilterOperatorMapper.DefaultOperators; - var strainerConfigurationMock = Substitute.For(); - strainerConfigurationMock - .FilterOperators - .Returns(filterOperators); - var strainerConfigurationProvider = new StrainerConfigurationProvider(strainerConfigurationMock); - var filterOperatorsProvider = new ConfigurationFilterOperatorsProvider(strainerConfigurationProvider); - var operatorParser = new FilterOperatorParser(filterOperatorsProvider); - var namesParser = new FilterTermNamesParser(); - var valuesParser = new FilterTermValuesParser(); - var sectionsParser = new FilterTermSectionsParser(filterOperatorsProvider); - var termParser = new FilterTermParser(operatorParser, namesParser, valuesParser, sectionsParser); + var name = "FilterName"; + var filterOperator = "=="; + var value = "123"; + var input = $"{name}{filterOperator}{value}"; + var termParser = BuildFilterTermParser(); // Act var filterTermList = termParser.GetParsedTerms(input); // Assert filterTermList.Should().HaveCount(1); + filterTermList[0].Input.Should().BeSameAs(input); + filterTermList[0].Names.Should().BeEquivalentTo(name); + filterTermList[0].Operator.Should().NotBeNull(); + filterTermList[0].Operator.Symbol.Should().Be(filterOperator); + filterTermList[0].Values.Should().BeEquivalentTo(value); } [Fact] - public void Parser_ReturnsFilterTerm_When_ThereIsNameWithOperator() + public void Parser_ReturnsFilterTerm_With_MultipleNames() + { + // Arrange + var name1 = "Foo"; + var name2 = "Bar"; + var filterOperator = "=="; + var value = "123"; + var input = $"{name1}|{name2}{filterOperator}{value}"; + var termParser = BuildFilterTermParser(); + + // Act + var filterTermList = termParser.GetParsedTerms(input); + + // Assert + filterTermList.Should().HaveCount(1); + filterTermList[0].Input.Should().BeSameAs(input); + filterTermList[0].Names.Should().BeEquivalentTo([name1, name2]); + filterTermList[0].Operator.Should().NotBeNull(); + filterTermList[0].Operator.Symbol.Should().Be(filterOperator); + filterTermList[0].Values.Should().BeEquivalentTo(value); + } + + [Fact] + public void Parser_ReturnsFilterTerm_With_MultipleValues() + { + // Arrange + var name = "FilterName"; + var filterOperator = "=="; + var value1 = "123"; + var value2 = "123"; + var input = $"{name}{filterOperator}{value1}|{value2}"; + var termParser = BuildFilterTermParser(); + + // Act + var filterTermList = termParser.GetParsedTerms(input); + + // Assert + filterTermList.Should().HaveCount(1); + filterTermList[0].Input.Should().BeSameAs(input); + filterTermList[0].Names.Should().BeEquivalentTo(name); + filterTermList[0].Operator.Should().NotBeNull(); + filterTermList[0].Operator.Symbol.Should().Be(filterOperator); + filterTermList[0].Values.Should().BeEquivalentTo([value1, value2]); + } + + [Fact] + public void Parser_ReturnsFilterTerm_With_MultipleNamesAndValues() { // Arrange - var input = "CustomFilterName=="; - var filterOperators = FilterOperatorMapper.DefaultOperators; + var name1 = "Foo"; + var name2 = "Bar"; + var filterOperator = "=="; + var value1 = "123"; + var value2 = "456"; + var input = $"{name1}|{name2}{filterOperator}{value1}|{value2}"; + var termParser = BuildFilterTermParser(); + + // Act + var filterTermList = termParser.GetParsedTerms(input); + + // Assert + filterTermList.Should().HaveCount(1); + filterTermList[0].Input.Should().BeSameAs(input); + filterTermList[0].Names.Should().BeEquivalentTo([name1, name2]); + filterTermList[0].Operator.Should().NotBeNull(); + filterTermList[0].Operator.Symbol.Should().Be(filterOperator); + filterTermList[0].Values.Should().BeEquivalentTo([value1, value2]); + } + + [Fact] + public void Parser_ReturnsFilterTerm_With_MultipleNamesInParenthesis() + { + // Arrange + var name1 = "Foo"; + var name2 = "Bar"; + var filterOperator = "=="; + var value = "123"; + var input = $"({name1}|{name2}){filterOperator}{value}"; + var termParser = BuildFilterTermParser(); + + // Act + var filterTermList = termParser.GetParsedTerms(input); + + // Assert + filterTermList.Should().HaveCount(1); + filterTermList[0].Input.Should().Be($"{name1}|{name2}{filterOperator}{value}"); + filterTermList[0].Names.Should().BeEquivalentTo([name1, name2]); + filterTermList[0].Operator.Should().NotBeNull(); + filterTermList[0].Operator.Symbol.Should().Be(filterOperator); + filterTermList[0].Values.Should().BeEquivalentTo(value); + } + + [Fact] + public void Parser_ReturnsMultipleFilterTerms() + { + // Arrange + var name1 = "Foo"; + var name2 = "Bar"; + var filterOperator = "=="; + var value1 = "123"; + var value2 = "456"; + var input1 = $"{name1}{filterOperator}{value1}"; + var input2 = $"{name2}{filterOperator}{value2}"; + var termParser = BuildFilterTermParser(); + + // Act + var filterTermList = termParser.GetParsedTerms($"{input1},{input2}"); + + // Assert + filterTermList.Should().HaveCount(2); + filterTermList[0].Input.Should().Be(input1); + filterTermList[0].Names.Should().BeEquivalentTo(name1); + filterTermList[0].Operator.Should().NotBeNull(); + filterTermList[0].Operator.Symbol.Should().Be(filterOperator); + filterTermList[0].Values.Should().BeEquivalentTo(value1); + filterTermList[1].Input.Should().Be(input2); + filterTermList[1].Names.Should().BeEquivalentTo(name2); + filterTermList[1].Operator.Should().NotBeNull(); + filterTermList[1].Operator.Symbol.Should().Be(filterOperator); + filterTermList[1].Values.Should().BeEquivalentTo(value2); + } + + private static FilterTermParser BuildFilterTermParser() + { var strainerConfigurationMock = Substitute.For(); strainerConfigurationMock .FilterOperators - .Returns(filterOperators); + .Returns(FilterOperatorMapper.DefaultOperators); var strainerConfigurationProvider = new StrainerConfigurationProvider(strainerConfigurationMock); var filterOperatorsProvider = new ConfigurationFilterOperatorsProvider(strainerConfigurationProvider); var operatorParser = new FilterOperatorParser(filterOperatorsProvider); var namesParser = new FilterTermNamesParser(); var valuesParser = new FilterTermValuesParser(); var sectionsParser = new FilterTermSectionsParser(filterOperatorsProvider); - var termParser = new FilterTermParser(operatorParser, namesParser, valuesParser, sectionsParser); - // Act - var filterTermList = termParser.GetParsedTerms(input); - - // Assert - filterTermList.Should().HaveCount(1); + return new FilterTermParser(operatorParser, namesParser, valuesParser, sectionsParser); } } -- GitLab From da6c1ec692d9d7235785a5d9bc71f1d35ed8a6af Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Wed, 19 Feb 2025 20:13:47 +0100 Subject: [PATCH 49/56] Fix parameter names --- src/Strainer/Services/StrainerContext.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Strainer/Services/StrainerContext.cs b/src/Strainer/Services/StrainerContext.cs index 42a123e6..bba7464f 100644 --- a/src/Strainer/Services/StrainerContext.cs +++ b/src/Strainer/Services/StrainerContext.cs @@ -18,15 +18,15 @@ public class StrainerContext : IStrainerContext public StrainerContext( IConfigurationCustomMethodsProvider customMethodsConfigurationProvider, IStrainerOptionsProvider optionsProvider, - IFilterContext filteringContext, + IFilterContext filterContext, ISortingContext sortingContext, - IMetadataFacade metadataProvidersFacade, + IMetadataFacade metadataFacade, IPipelineContext pipelineContext) { CustomMethods = Guard.Against.Null(customMethodsConfigurationProvider); - Filter = Guard.Against.Null(filteringContext); + Filter = Guard.Against.Null(filterContext); Sorting = Guard.Against.Null(sortingContext); - Metadata = Guard.Against.Null(metadataProvidersFacade); + Metadata = Guard.Against.Null(metadataFacade); Pipeline = Guard.Against.Null(pipelineContext); Options = Guard.Against.Null(optionsProvider).GetStrainerOptions(); } -- GitLab From cda4f161771cec78f9dfc1b8607d9bfe5a019a92 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Wed, 19 Feb 2025 20:14:00 +0100 Subject: [PATCH 50/56] Add unit tests for context services --- .../Services/Filtering/FilterContextTest.cs | 30 +++++++++++++ .../Pipelines/PipelineContextTests.cs | 19 ++++++++ .../StrainerPipelineBuilderFactoryTests.cs | 31 +++++++++++++ .../Services/Sorting/SortContextTests.cs | 33 ++++++++++++++ .../Services/StrainerContextTests.cs | 43 +++++++++++++++++++ 5 files changed, 156 insertions(+) create mode 100644 test/Strainer.UnitTests/Services/Filtering/FilterContextTest.cs create mode 100644 test/Strainer.UnitTests/Services/Pipelines/PipelineContextTests.cs create mode 100644 test/Strainer.UnitTests/Services/Pipelines/StrainerPipelineBuilderFactoryTests.cs create mode 100644 test/Strainer.UnitTests/Services/Sorting/SortContextTests.cs create mode 100644 test/Strainer.UnitTests/Services/StrainerContextTests.cs diff --git a/test/Strainer.UnitTests/Services/Filtering/FilterContextTest.cs b/test/Strainer.UnitTests/Services/Filtering/FilterContextTest.cs new file mode 100644 index 00000000..0ca21d51 --- /dev/null +++ b/test/Strainer.UnitTests/Services/Filtering/FilterContextTest.cs @@ -0,0 +1,30 @@ +using Fluorite.Strainer.Services.Filtering; +using Fluorite.Strainer.Services.Validation; + +namespace Fluorite.Strainer.UnitTests.Services.Filtering; + +public class FilterContextTest +{ + [Fact] + public void Should_Create_FilterContext() + { + // Arrange + var filterExpressionProvider = Substitute.For(); + var operatorParser = Substitute.For(); + var operatorValidator = Substitute.For(); + var filterTermParser = Substitute.For(); + + // Act + var result = new FilterContext( + filterExpressionProvider, + operatorParser, + operatorValidator, + filterTermParser); + + // Assert + result.ExpressionProvider.Should().BeSameAs(filterExpressionProvider); + result.OperatorParser.Should().BeSameAs(operatorParser); + result.OperatorValidator.Should().BeSameAs(operatorValidator); + result.TermParser.Should().BeSameAs(filterTermParser); + } +} diff --git a/test/Strainer.UnitTests/Services/Pipelines/PipelineContextTests.cs b/test/Strainer.UnitTests/Services/Pipelines/PipelineContextTests.cs new file mode 100644 index 00000000..51d38083 --- /dev/null +++ b/test/Strainer.UnitTests/Services/Pipelines/PipelineContextTests.cs @@ -0,0 +1,19 @@ +using Fluorite.Strainer.Services.Pipelines; + +namespace Fluorite.Strainer.UnitTests.Services.Pipelines; + +public class PipelineContextTests +{ + [Fact] + public void Should_Create_PipelineContext() + { + // Arrange + var pipelineBuilderFactory = Substitute.For(); + + // Act + var result = new PipelineContext(pipelineBuilderFactory); + + // Assert + result.BuilderFactory.Should().BeSameAs(pipelineBuilderFactory); + } +} diff --git a/test/Strainer.UnitTests/Services/Pipelines/StrainerPipelineBuilderFactoryTests.cs b/test/Strainer.UnitTests/Services/Pipelines/StrainerPipelineBuilderFactoryTests.cs new file mode 100644 index 00000000..b1a25e8a --- /dev/null +++ b/test/Strainer.UnitTests/Services/Pipelines/StrainerPipelineBuilderFactoryTests.cs @@ -0,0 +1,31 @@ +using Fluorite.Strainer.Models; +using Fluorite.Strainer.Services; +using Fluorite.Strainer.Services.Pipelines; + +namespace Fluorite.Strainer.UnitTests.Services.Pipelines; + +public class StrainerPipelineBuilderFactoryTests +{ + [Fact] + public void Should_Create_PipelineContextBuilder() + { + // Arrange + var options = new StrainerOptions(); + var optionsProvider = Substitute.For(); + optionsProvider.GetStrainerOptions().Returns(options); + var filterPipelineOperation = Substitute.For(); + var sortPipelineOperation = Substitute.For(); + var paginatePipelineOperation = Substitute.For(); + + // Act + var factory = new StrainerPipelineBuilderFactory( + filterPipelineOperation, + sortPipelineOperation, + paginatePipelineOperation, + optionsProvider); + var result = factory.CreateBuilder(); + + // Assert + result.Should().NotBeNull(); + } +} diff --git a/test/Strainer.UnitTests/Services/Sorting/SortContextTests.cs b/test/Strainer.UnitTests/Services/Sorting/SortContextTests.cs new file mode 100644 index 00000000..fbd8a486 --- /dev/null +++ b/test/Strainer.UnitTests/Services/Sorting/SortContextTests.cs @@ -0,0 +1,33 @@ +using Fluorite.Strainer.Services.Sorting; +using Fluorite.Strainer.Services.Validation; + +namespace Fluorite.Strainer.UnitTests.Services.Sorting; + +public class SortContextTests +{ + [Fact] + public void Should_Create_SortContext() + { + // Arrange + var sortExpressionProvider = Substitute.For(); + var expressionValidator = Substitute.For(); + var sortingWayFormatter = Substitute.For(); + var sortTermParser = Substitute.For(); + var sortTermValueParser = Substitute.For(); + + // Act + var result = new SortingContext( + sortExpressionProvider, + expressionValidator, + sortingWayFormatter, + sortTermParser, + sortTermValueParser); + + // Assert + result.ExpressionProvider.Should().BeSameAs(sortExpressionProvider); + result.ExpressionValidator.Should().BeSameAs(expressionValidator); + result.Formatter.Should().BeSameAs(sortingWayFormatter); + result.TermParser.Should().BeSameAs(sortTermParser); + result.TermValueParser.Should().BeSameAs(sortTermValueParser); + } +} diff --git a/test/Strainer.UnitTests/Services/StrainerContextTests.cs b/test/Strainer.UnitTests/Services/StrainerContextTests.cs new file mode 100644 index 00000000..aaf1b71a --- /dev/null +++ b/test/Strainer.UnitTests/Services/StrainerContextTests.cs @@ -0,0 +1,43 @@ +using Fluorite.Strainer.Models; +using Fluorite.Strainer.Services; +using Fluorite.Strainer.Services.Configuration; +using Fluorite.Strainer.Services.Filtering; +using Fluorite.Strainer.Services.Metadata; +using Fluorite.Strainer.Services.Pipelines; +using Fluorite.Strainer.Services.Sorting; + +namespace Fluorite.Strainer.UnitTests.Services; + +public class StrainerContextTests +{ + [Fact] + public void Should_Create_StrainerContext() + { + // Arrange + var customMethodsConfigurationProvider = Substitute.For(); + var options = new StrainerOptions(); + var optionsProvider = Substitute.For(); + optionsProvider.GetStrainerOptions().Returns(options); + var filterContext = Substitute.For(); + var sortingContext = Substitute.For(); + var metadataFacade = Substitute.For(); + var pipelineContext = Substitute.For(); + + // Act + var result = new StrainerContext( + customMethodsConfigurationProvider, + optionsProvider, + filterContext, + sortingContext, + metadataFacade, + pipelineContext); + + // Assert + result.CustomMethods.Should().BeSameAs(customMethodsConfigurationProvider); + result.Filter.Should().BeSameAs(filterContext); + result.Metadata.Should().BeSameAs(metadataFacade); + result.Options.Should().BeSameAs(options); + result.Pipeline.Should().BeSameAs(pipelineContext); + result.Sorting.Should().BeSameAs(sortingContext); + } +} -- GitLab From 31081223991d4e686b8fe764c2c19fbe361a5f6c Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Wed, 19 Feb 2025 20:59:40 +0100 Subject: [PATCH 51/56] Use First() when known that collection is not empty --- .../StrainerServiceCollectionExtensionsTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/Strainer.UnitTests/Extensions/DepedencyInjection/StrainerServiceCollectionExtensionsTests.cs b/test/Strainer.UnitTests/Extensions/DepedencyInjection/StrainerServiceCollectionExtensionsTests.cs index 18fc5315..11f61852 100644 --- a/test/Strainer.UnitTests/Extensions/DepedencyInjection/StrainerServiceCollectionExtensionsTests.cs +++ b/test/Strainer.UnitTests/Extensions/DepedencyInjection/StrainerServiceCollectionExtensionsTests.cs @@ -194,21 +194,21 @@ public class StrainerServiceCollectionExtensionsTests services.AddStrainer(serviceLifetime); // Assert - services.FirstOrDefault(s => s.ServiceType == typeof(IStrainerProcessor)) + services.First(s => s.ServiceType == typeof(IStrainerProcessor)) .Lifetime .Should() .Be(serviceLifetime); if (serviceLifetime == ServiceLifetime.Singleton) { - services.FirstOrDefault(s => s.ServiceType == typeof(IStrainerOptionsProvider)) + services.First(s => s.ServiceType == typeof(IStrainerOptionsProvider)) .ImplementationType .Should() .Be(); } else { - services.FirstOrDefault(s => s.ServiceType == typeof(IStrainerOptionsProvider)) + services.First(s => s.ServiceType == typeof(IStrainerOptionsProvider)) .ImplementationType .Should() .Be(); -- GitLab From d6ec8965525df44bef67c11498f493c242b7e22f Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Wed, 19 Feb 2025 20:59:51 +0100 Subject: [PATCH 52/56] Add unit tests for generic module loading strategy --- .../GenericModuleLoadingStrategy.cs | 5 ++ .../GenericModuleLoadingStrategyTests.cs | 46 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 test/Strainer.UnitTests/Services/Configuration/GenericModuleLoadingStrategyTests.cs diff --git a/src/Strainer/Services/Configuration/GenericModuleLoadingStrategy.cs b/src/Strainer/Services/Configuration/GenericModuleLoadingStrategy.cs index 644d8e1b..ccb47463 100644 --- a/src/Strainer/Services/Configuration/GenericModuleLoadingStrategy.cs +++ b/src/Strainer/Services/Configuration/GenericModuleLoadingStrategy.cs @@ -19,6 +19,11 @@ public class GenericModuleLoadingStrategy : IGenericModuleLoadingStrategy, IModu .GetType() .GetInterfaces() .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IStrainerModule<>)); + if (genericStrainerModuleInterfaceType is null) + { + throw new ArgumentException("Strainer module must be generic.", nameof(strainerModule)); + } + var moduleTypeParameter = genericStrainerModuleInterfaceType.GetGenericArguments().First(); var builder = _strainerModuleBuilderFactory.Create(moduleTypeParameter, strainerModule); var method = genericStrainerModuleInterfaceType.GetMethod(nameof(IStrainerModule.Load)); diff --git a/test/Strainer.UnitTests/Services/Configuration/GenericModuleLoadingStrategyTests.cs b/test/Strainer.UnitTests/Services/Configuration/GenericModuleLoadingStrategyTests.cs new file mode 100644 index 00000000..8c3ec42b --- /dev/null +++ b/test/Strainer.UnitTests/Services/Configuration/GenericModuleLoadingStrategyTests.cs @@ -0,0 +1,46 @@ +using Fluorite.Strainer.Services.Configuration; +using Fluorite.Strainer.Services.Modules; + +namespace Fluorite.Strainer.UnitTests.Services.Configuration; + +public class GenericModuleLoadingStrategyTests +{ + private readonly IStrainerModuleBuilderFactory _strainerModuleBuilderFactory = Substitute.For(); + + private readonly GenericModuleLoadingStrategy _strategy; + + public GenericModuleLoadingStrategyTests() + { + _strategy = new GenericModuleLoadingStrategy(_strainerModuleBuilderFactory); + } + + [Fact] + public void Should_Throw_ForNonGenericModule() + { + // Arrange + var strainerModule = Substitute.For(); + + // Act + Action act = () => _strategy.Load(strainerModule); + + // Assert + act.Should().ThrowExactly() + .WithMessage("Strainer module must be generic.*"); + } + + [Fact] + public void Should_Load_GenericModule() + { + // Arrange + var strainerModule = Substitute.For>(); + + // Act + Action act = () => _strategy.Load(strainerModule); + + // Assert + act.Should().NotThrow(); + + strainerModule.Received(1).Load(Arg.Any>()); + _strainerModuleBuilderFactory.Received(1).Create(typeof(int), strainerModule); + } +} -- GitLab From 68fefe782d45b1062a742ad647341a358ab60ce1 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Wed, 19 Feb 2025 21:09:30 +0100 Subject: [PATCH 53/56] Cover missing test case in page size evaluator --- .../Pagination/PageSizeEvaluatorTests.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/Strainer.UnitTests/Services/Pagination/PageSizeEvaluatorTests.cs b/test/Strainer.UnitTests/Services/Pagination/PageSizeEvaluatorTests.cs index 82dbebcb..3156632b 100644 --- a/test/Strainer.UnitTests/Services/Pagination/PageSizeEvaluatorTests.cs +++ b/test/Strainer.UnitTests/Services/Pagination/PageSizeEvaluatorTests.cs @@ -83,4 +83,28 @@ public class PageSizeEvaluatorTests // Assert result.Should().Be(defaultPageSize); } + + [Fact] + public void Should_Return_PageSize_WhenMaxPageSizeIsZero() + { + // Arrange + var model = new StrainerModel + { + PageSize = 20, + }; + var strainerOptions = new StrainerOptions + { + MaxPageSize = 0, + }; + + _strainerOptionsProviderMock + .GetStrainerOptions() + .Returns(strainerOptions); + + // Act + var result = _evaluator.Evaluate(model); + + // Assert + result.Should().Be(model.PageSize); + } } -- GitLab From f93c81b10c6d15916539a322748d7ba5f7096f88 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sat, 1 Mar 2025 19:00:07 +0100 Subject: [PATCH 54/56] Cover missing test cases when Fluent API is disabled --- .../FluentApiMetadataProviderTests.cs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/test/Strainer.UnitTests/Services/Metadata/FluentApi/FluentApiMetadataProviderTests.cs b/test/Strainer.UnitTests/Services/Metadata/FluentApi/FluentApiMetadataProviderTests.cs index c3c8dac0..b49bb525 100644 --- a/test/Strainer.UnitTests/Services/Metadata/FluentApi/FluentApiMetadataProviderTests.cs +++ b/test/Strainer.UnitTests/Services/Metadata/FluentApi/FluentApiMetadataProviderTests.cs @@ -26,6 +26,24 @@ public class FluentApiMetadataProviderTests _propertyMetadataBuilderMock); } + [Fact] + public void GetDefaultMetadata_ReturnsNull_When_FluentApiIsDisabled() + { + // Arrange + _optionsProviderMock + .GetStrainerOptions() + .Returns(new StrainerOptions + { + MetadataSourceType = MetadataSourceType.None, + }); + + // Act + var metadata = _provider.GetDefaultMetadata(typeof(Post)); + + // Assert + metadata.Should().BeNull(); + } + [Fact] public void GetDefaultMetadata_ReturnsNull_When_NoMetadataAvailable() { @@ -102,6 +120,28 @@ public class FluentApiMetadataProviderTests metadata.Should().Be(propertyMetadata); } + [Fact] + public void GetPropertyMetadata_ReturnsNull_When_FluentApiIsDisabled() + { + // Arrange + _optionsProviderMock + .GetStrainerOptions() + .Returns(new StrainerOptions + { + MetadataSourceType = MetadataSourceType.None, + }); + + // Act + var metadata = _provider.GetPropertyMetadata( + typeof(Post), + isSortableRequired: false, + isFilterableRequired: false, + name: nameof(Post.Id)); + + // Assert + metadata.Should().BeNull(); + } + [Fact] public void GetPropertyMetadata_ReturnsNull_When_NoMetadataAvailable() { @@ -418,6 +458,24 @@ public class FluentApiMetadataProviderTests metadatas.Should().BeEmpty(); } + [Fact] + public void GetPropertyMetadatas_Returns_Null_When_FluentApiIsDisabled() + { + // Arrange + _optionsProviderMock + .GetStrainerOptions() + .Returns(new StrainerOptions + { + MetadataSourceType = MetadataSourceType.None, + }); + + // Act + var metadatas = _provider.GetPropertyMetadatas(typeof(Post)); + + // Assert + metadatas.Should().BeNull(); + } + [Fact] public void GetPropertyMetadatas_Returns_NullWhenNoMetadataIsFound() { -- GitLab From fa4421bf851477f78a5f0d313558818f262eea55 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sat, 1 Mar 2025 20:27:46 +0100 Subject: [PATCH 55/56] Add unit tests for getting all metadata from Fluent API provider --- .../FluentApi/FluentApiMetadataProvider.cs | 24 ++--- .../FluentApiPropertyMetadataBuilder.cs | 2 +- .../IFluentApiPropertyMetadataBuilder.cs | 2 +- .../FluentApiMetadataProviderTests.cs | 87 +++++++++++++++++-- .../FluentApiPropertyMetadataBuilderTests.cs | 4 +- 5 files changed, 99 insertions(+), 20 deletions(-) diff --git a/src/Strainer/Services/Metadata/FluentApi/FluentApiMetadataProvider.cs b/src/Strainer/Services/Metadata/FluentApi/FluentApiMetadataProvider.cs index 74edf004..cc25d4f5 100644 --- a/src/Strainer/Services/Metadata/FluentApi/FluentApiMetadataProvider.cs +++ b/src/Strainer/Services/Metadata/FluentApi/FluentApiMetadataProvider.cs @@ -31,12 +31,12 @@ public class FluentApiMetadataProvider : IMetadataProvider } var objectMetadata = _metadataProvider.GetObjectMetadata(); + var propertyMetadata = _metadataProvider.GetPropertyMetadata(); - return _metadataProvider - .GetPropertyMetadata() + return propertyMetadata .Keys .Union(objectMetadata.Keys) - .Select(type => (type, BuildMetadataKeyValuePair(type))) + .Select(type => (type, BuildMetadataKeyValuePair(type, objectMetadata, propertyMetadata))) .ToDictionary(tuple => tuple.type, tuple => tuple.Item2) .ToReadOnly(); } @@ -99,7 +99,7 @@ public class FluentApiMetadataProvider : IMetadataProvider var propertyInfo = _propertyInfoProvider.GetPropertyInfo(modelType, name); if (propertyInfo is not null) { - return _propertyMetadataBuilder.BuildDefaultMetadataForProperty(objectMetadata, propertyInfo); + return _propertyMetadataBuilder.BuildMetadataForProperty(objectMetadata, propertyInfo); } } } @@ -124,33 +124,35 @@ public class FluentApiMetadataProvider : IMetadataProvider if (_metadataProvider.GetObjectMetadata().TryGetValue(modelType, out var objectMetadata)) { return _propertyInfoProvider.GetPropertyInfos(modelType) - .Select(p => _propertyMetadataBuilder.BuildDefaultMetadataForProperty(objectMetadata, p)) + .Select(p => _propertyMetadataBuilder.BuildMetadataForProperty(objectMetadata, p)) .ToList(); } return null; } - private IReadOnlyDictionary BuildMetadataKeyValuePair(Type type) + private IReadOnlyDictionary BuildMetadataKeyValuePair( + Type type, + IReadOnlyDictionary objectMetadata, + IReadOnlyDictionary> propertyMetadata) { // TODO: Shouldn't property metadata override object metadata, but still be returned? // So type-wide config is set with object call, but property call overrides that for some special case? - var propertyMetadataDictionary = _metadataProvider.GetPropertyMetadata(); - if (propertyMetadataDictionary.TryGetValue(type, out var metadatas)) + if (propertyMetadata.TryGetValue(type, out var metadatas)) { return metadatas; } - var objectMetadata = _metadataProvider.GetObjectMetadata()[type]; + var objectMetadataForType = objectMetadata[type]; - return GetPropertyMetadatasFromObjectMetadata(type, objectMetadata); + return GetPropertyMetadatasFromObjectMetadata(type, objectMetadataForType); } private IReadOnlyDictionary GetPropertyMetadatasFromObjectMetadata(Type type, IObjectMetadata objectMetadata) { return _propertyInfoProvider .GetPropertyInfos(type) - .Select(propertyInfo => _propertyMetadataBuilder.BuildDefaultMetadataForProperty(objectMetadata, propertyInfo)) + .Select(propertyInfo => _propertyMetadataBuilder.BuildMetadataForProperty(objectMetadata, propertyInfo)) .ToDictionary(p => p.Name, p => p) .ToReadOnlyDictionary(); } diff --git a/src/Strainer/Services/Metadata/FluentApi/FluentApiPropertyMetadataBuilder.cs b/src/Strainer/Services/Metadata/FluentApi/FluentApiPropertyMetadataBuilder.cs index 297d3290..d9972977 100644 --- a/src/Strainer/Services/Metadata/FluentApi/FluentApiPropertyMetadataBuilder.cs +++ b/src/Strainer/Services/Metadata/FluentApi/FluentApiPropertyMetadataBuilder.cs @@ -26,7 +26,7 @@ public class FluentApiPropertyMetadataBuilder : IFluentApiPropertyMetadataBuilde }; } - public IPropertyMetadata BuildDefaultMetadataForProperty(IObjectMetadata objectMetadata, PropertyInfo propertyInfo) + public IPropertyMetadata BuildMetadataForProperty(IObjectMetadata objectMetadata, PropertyInfo propertyInfo) { Guard.Against.Null(objectMetadata); Guard.Against.Null(objectMetadata.DefaultSortingPropertyInfo); diff --git a/src/Strainer/Services/Metadata/FluentApi/IFluentApiPropertyMetadataBuilder.cs b/src/Strainer/Services/Metadata/FluentApi/IFluentApiPropertyMetadataBuilder.cs index c23c76d9..24a1bea4 100644 --- a/src/Strainer/Services/Metadata/FluentApi/IFluentApiPropertyMetadataBuilder.cs +++ b/src/Strainer/Services/Metadata/FluentApi/IFluentApiPropertyMetadataBuilder.cs @@ -7,5 +7,5 @@ public interface IFluentApiPropertyMetadataBuilder { IPropertyMetadata BuildDefaultMetadata(IObjectMetadata objectMetadata); - IPropertyMetadata BuildDefaultMetadataForProperty(IObjectMetadata objectMetadata, PropertyInfo propertyInfo); + IPropertyMetadata BuildMetadataForProperty(IObjectMetadata objectMetadata, PropertyInfo propertyInfo); } diff --git a/test/Strainer.UnitTests/Services/Metadata/FluentApi/FluentApiMetadataProviderTests.cs b/test/Strainer.UnitTests/Services/Metadata/FluentApi/FluentApiMetadataProviderTests.cs index b49bb525..2939cb79 100644 --- a/test/Strainer.UnitTests/Services/Metadata/FluentApi/FluentApiMetadataProviderTests.cs +++ b/test/Strainer.UnitTests/Services/Metadata/FluentApi/FluentApiMetadataProviderTests.cs @@ -317,7 +317,7 @@ public class FluentApiMetadataProviderTests .GetPropertyInfo(typeof(Post), name) .Returns(propertyInfo); _propertyMetadataBuilderMock - .BuildDefaultMetadataForProperty(objectMetadata, propertyInfo) + .BuildMetadataForProperty(objectMetadata, propertyInfo) .Returns(propertyMetadata); // Act @@ -367,7 +367,7 @@ public class FluentApiMetadataProviderTests .GetPropertyInfo(typeof(Post), name) .Returns(propertyInfo); _propertyMetadataBuilderMock - .BuildDefaultMetadataForProperty(objectMetadata, propertyInfo) + .BuildMetadataForProperty(objectMetadata, propertyInfo) .Returns(propertyMetadata); // Act @@ -421,7 +421,7 @@ public class FluentApiMetadataProviderTests } [Fact] - public void GetPropertyMetadata_Returns_Null_With_FluentApiMetadataSourceType_Disabled() + public void GetAllPropertyMetadata_Returns_Null_With_FluentApiMetadataSourceType_Disabled() { // Arrange _optionsProviderMock @@ -436,7 +436,7 @@ public class FluentApiMetadataProviderTests } [Fact] - public void GetPropertyMetadata_Returns_EmptyMetadata_When_NoMetadataIsAvailable() + public void GetAllPropertyMetadata_Returns_EmptyMetadata_When_NoMetadataIsAvailable() { // Arrange _optionsProviderMock @@ -458,6 +458,83 @@ public class FluentApiMetadataProviderTests metadatas.Should().BeEmpty(); } + [Fact] + public void GetAllPropertyMetadata_Returns_AllMetadataFromProperties() + { + // Arrange + _optionsProviderMock + .GetStrainerOptions() + .Returns(new StrainerOptions()); + var type = typeof(Post); + var propertyMetadataMock = Substitute.For(); + var propertyMetadataDictionary = new Dictionary> + { + [type] = new Dictionary + { + [nameof(Post.Id)] = propertyMetadataMock, + }, + }; + _configurationMetadataProviderMock + .GetPropertyMetadata() + .Returns(propertyMetadataDictionary); + var objectMetadataDictionary = new Dictionary(); + _configurationMetadataProviderMock + .GetObjectMetadata() + .Returns(objectMetadataDictionary); + + // Act + var metadatas = _provider.GetAllPropertyMetadata(); + + // Assert + metadatas.Should().NotBeNullOrEmpty(); + metadatas.Keys.Should().BeEquivalentTo([type]); + metadatas[type].Should().NotBeNullOrEmpty(); + metadatas[type].Should().BeSameAs(propertyMetadataDictionary[type]); + + metadatas.Should().BeEquivalentTo(propertyMetadataDictionary); + } + + [Fact] + public void GetAllPropertyMetadata_Returns_AllMetadataFromObject() + { + // Arrange + _optionsProviderMock + .GetStrainerOptions() + .Returns(new StrainerOptions()); + var type = typeof(Post); + var propertyInfo = type.GetProperty(nameof(Post.Id)); + var propertyMetadataDictionary = new Dictionary>(); + _configurationMetadataProviderMock + .GetPropertyMetadata() + .Returns(propertyMetadataDictionary); + var objectMetadataMock = Substitute.For(); + var objectMetadataDictionary = new Dictionary + { + [type] = objectMetadataMock, + }; + var propertyMetadataMock = Substitute.For(); + propertyMetadataMock.Name.Returns(propertyInfo.Name); + _propertyInfoProviderMock + .GetPropertyInfos(type) + .Returns([propertyInfo]); + _propertyMetadataBuilderMock + .BuildMetadataForProperty(objectMetadataMock, propertyInfo) + .Returns(propertyMetadataMock); + _configurationMetadataProviderMock + .GetObjectMetadata() + .Returns(objectMetadataDictionary); + + // Act + var metadatas = _provider.GetAllPropertyMetadata(); + + // Assert + metadatas.Should().NotBeNullOrEmpty(); + metadatas.Keys.Should().BeEquivalentTo([type]); + metadatas[type].Should().NotBeNullOrEmpty(); + metadatas[type].Keys.Should().BeEquivalentTo([propertyInfo.Name]); + metadatas[type].Values.Should().BeEquivalentTo([propertyMetadataMock]); + } + [Fact] public void GetPropertyMetadatas_Returns_Null_When_FluentApiIsDisabled() { @@ -540,7 +617,7 @@ public class FluentApiMetadataProviderTests .GetPropertyInfos(typeof(Post)) .Returns(propertyInfos); _propertyMetadataBuilderMock - .BuildDefaultMetadataForProperty(objectMetadata, propertyInfo) + .BuildMetadataForProperty(objectMetadata, propertyInfo) .Returns(propertyMetadata); // Act diff --git a/test/Strainer.UnitTests/Services/Metadata/FluentApi/FluentApiPropertyMetadataBuilderTests.cs b/test/Strainer.UnitTests/Services/Metadata/FluentApi/FluentApiPropertyMetadataBuilderTests.cs index f90c2c5f..65b197b1 100644 --- a/test/Strainer.UnitTests/Services/Metadata/FluentApi/FluentApiPropertyMetadataBuilderTests.cs +++ b/test/Strainer.UnitTests/Services/Metadata/FluentApi/FluentApiPropertyMetadataBuilderTests.cs @@ -56,7 +56,7 @@ public class FluentApiPropertyMetadataBuilderTests objectMetadata.DefaultSortingPropertyInfo.Returns(propertyInfo); // Act - var result = _builder.BuildDefaultMetadataForProperty(objectMetadata, propertyInfo); + var result = _builder.BuildMetadataForProperty(objectMetadata, propertyInfo); // Assert result.Should().NotBeNull(); @@ -92,7 +92,7 @@ public class FluentApiPropertyMetadataBuilderTests .Returns(strainerOptions); // Act - var result = _builder.BuildDefaultMetadataForProperty(objectMetadata, differentPropertyInfo); + var result = _builder.BuildMetadataForProperty(objectMetadata, differentPropertyInfo); // Assert result.Should().NotBeNull(); -- GitLab From 91b57012ab321911b1be64457d824c314125aad5 Mon Sep 17 00:00:00 2001 From: Piotr Wosiek Date: Sat, 1 Mar 2025 20:46:48 +0100 Subject: [PATCH 56/56] Cover missing test case in Attribute metadata provider --- .../AttributeMetadataProviderTests.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/Strainer.UnitTests/Services/Metadata/Attributes/AttributeMetadataProviderTests.cs b/test/Strainer.UnitTests/Services/Metadata/Attributes/AttributeMetadataProviderTests.cs index 91585b56..6d71871d 100644 --- a/test/Strainer.UnitTests/Services/Metadata/Attributes/AttributeMetadataProviderTests.cs +++ b/test/Strainer.UnitTests/Services/Metadata/Attributes/AttributeMetadataProviderTests.cs @@ -68,6 +68,24 @@ public class AttributeMetadataProviderTests result.Values.Should().OnlyContain(x => x.Any()); } + [Fact] + public void Provider_Returns_NoDefaultMetadata_WhenNoneIsFound() + { + // Arrange + _attributeMetadataRetrieverMock + .GetDefaultMetadataFromObjectAttribute(typeof(Comment)) + .ReturnsNull(); + _attributeMetadataRetrieverMock + .GetDefaultMetadataFromPropertyAttribute(typeof(Comment)) + .ReturnsNull(); + + // Act + var result = _provider.GetDefaultMetadata(typeof(Comment)); + + // Assert + result.Should().BeNull(); + } + [Fact] public void Provider_Returns_DefaultMetadata_ForObject() { -- GitLab