diff --git a/StyleCop.ruleset b/StyleCop.ruleset index 792caf94fa32e4172e842c8bcc7ec7c733a92db7..7f3c0343b4b12f65f394e4d730de38453e59bb4c 100644 --- a/StyleCop.ruleset +++ b/StyleCop.ruleset @@ -574,6 +574,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Strainer.AspNetCore/Extensions/DependencyInjection/StrainerServiceCollectionExtensions.cs b/src/Strainer.AspNetCore/Extensions/DependencyInjection/StrainerServiceCollectionExtensions.cs index 7b948d8aa60d19946960f7155ff377815e69e8d4..9894025c3b63d76b379fc114c37b27fbac0127bb 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; @@ -46,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) @@ -86,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, @@ -100,7 +93,7 @@ public static class StrainerServiceCollectionExtensions var moduleTypes = GetModuleTypesFromAssemblies(assembliesToScan); - services.AddSingleton(new AssemblySourceProvider(assembliesToScan)); + services.TryAddSingleton(new AssemblySourceProvider(assembliesToScan)); return services.AddStrainer(moduleTypes, serviceLifetime); } @@ -128,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, @@ -168,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, @@ -212,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, @@ -265,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, @@ -280,7 +257,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); } @@ -308,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, @@ -352,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, @@ -405,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, @@ -420,7 +385,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 +395,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.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.TryAddSingleton(serviceProvider => { using var scope = serviceProvider.CreateScope(); var configurationFactory = scope.ServiceProvider.GetRequiredService(); @@ -538,12 +497,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/src/Strainer.AspNetCore/Strainer.AspNetCore.csproj b/src/Strainer.AspNetCore/Strainer.AspNetCore.csproj index cbf04954a4a7f9d5f5ccca00bfb2f1d292b6df31..c55db2b9ba8335c05ee72634e9355e5dc7b254b6 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.ExampleWebApi/Strainer.ExampleWebApi.csproj b/src/Strainer.ExampleWebApi/Strainer.ExampleWebApi.csproj index a9e90afd77b17ee1dae7c8b0e89c2a1f6f71113a..1f91954e7d669e72534833a5797a349bd402d92b 100644 --- a/src/Strainer.ExampleWebApi/Strainer.ExampleWebApi.csproj +++ b/src/Strainer.ExampleWebApi/Strainer.ExampleWebApi.csproj @@ -21,7 +21,7 @@ - + diff --git a/src/Strainer/Attributes/StrainerObjectAttribute.cs b/src/Strainer/Attributes/StrainerObjectAttribute.cs index 2c4d85891f884799c060fb5d3c3476f8c4f9c59a..4cdc7eb4c38cbd276f308c44fdd8bb1659d13370 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 971fd8a4a26910bd27e63e170b5d5471dccad830..493bbd290c8e30bed801e6a4a2ec3ee12b7c3f8d 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/Collections/ReadOnlyHashSet.cs b/src/Strainer/Collections/ReadOnlyHashSet.cs index af2c5d7b049388856068045aa6da5ea112fb9b4b..01b9de81545b3367f9a925227b9cd6ffbe0367ac 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; diff --git a/src/Strainer/Exceptions/StrainerException.cs b/src/Strainer/Exceptions/StrainerException.cs index 48f0d369bbf85414d493e1c065af7c740a7149a5..e3455540616ed04326054d8c6651064907be0c7b 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 { @@ -18,9 +16,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 0000000000000000000000000000000000000000..a0113f186ee400be4b7280a0600cc51b5660c90a --- /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 089f006a88d835b7c198616efef9271cd1d4d797..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..64dc899ec057c1546f12f83b3972ceceb70264a5 --- /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 0000000000000000000000000000000000000000..1f5c075959e5f637eda780fe5c388c41667e4f6a --- /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/Models/Filtering/Operators/FilterExpressionContext.cs b/src/Strainer/Models/Filtering/Operators/FilterExpressionContext.cs index 76c874e2f7526f58d1aa4197d0150348de83b415..156de9cbe1ced5993fde07b0c6e8a87f910b9586 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/FilterOperator.cs b/src/Strainer/Models/Filtering/Operators/FilterOperator.cs index 980ca5c557c8fdcef2b5da78dc2ae0e3f8a7a7c1..7db9fcfa2edd8d3c2813e16bce792a0f147e9683 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/IFilterExpressionContext.cs b/src/Strainer/Models/Filtering/Operators/IFilterExpressionContext.cs index ae62c8c3388d4b9499e410ac10051a9d4aa6b8e8..2a5b7a28a6458e1d4c4328004ab69c13a796d53b 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/Filtering/Operators/IFilterOperator.cs b/src/Strainer/Models/Filtering/Operators/IFilterOperator.cs index 893603393dd082e0c7cbcff46dc666ebd5e1105e..64a30f13c98d0134f24a1a0a87de63538a5575cb 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/Models/Filtering/Terms/FilterTerm.cs b/src/Strainer/Models/Filtering/Terms/FilterTerm.cs index 663f467f958fa1657ad56a3ec029791c935f08f1..de586cdf6e7a9daecc9aff722347e294dd915896 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/IObjectMetadata.cs b/src/Strainer/Models/Metadata/IObjectMetadata.cs index b7b7c0830479370942281d67c10d397dd38749a4..1d71e4946877094a34430bd8889ac52b9cb5c209 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 d6e5bf97df1ae792605ba6dc974edd65d9655d33..3508b0f4c877d6407b2753aa69d5dc8438c80553 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 6053cda0dfa1e290c06b6e96afffdab336dae8d5..2c2dd0be21493cc85b8b356f0a06ae1cba9c1a36 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 1252435a8bb46d4bf3c57a3af5ebadf636c86c87..10fc2ca25a1378b9fae8507f677dc3faa6c6ed90 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; @@ -7,7 +8,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. @@ -34,16 +35,16 @@ public class PropertyMetadata : IPropertyMetadata, IEquatable /// 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,83 +58,9 @@ public class PropertyMetadata : IPropertyMetadata, IEquatable /// 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; } - - 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/SortingWay.cs b/src/Strainer/Models/Sorting/SortingWay.cs index 04d8ce2de547163e58f8e60ab78f3859bd62cc5f..4671300d4463946917fc0155dd99282d2b63e64f 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/Models/Sorting/Terms/SortTerm.cs b/src/Strainer/Models/Sorting/Terms/SortTerm.cs index 20b74439b19fb29a2938310ba17744d9519a276a..2a562f64266301c14c2181bc3d244d9d27733c44 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; - } } diff --git a/src/Strainer/Models/StrainerOptions.cs b/src/Strainer/Models/StrainerOptions.cs index 6b6c85a517a2ecf82bfc373429709f0339221fe6..a67d7b2b4e825be2247029f3372fdd54b4e92290 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/Configuration/GenericModuleLoadingStrategy.cs b/src/Strainer/Services/Configuration/GenericModuleLoadingStrategy.cs index 644d8e1b1eace82038bdbe4a11b5a775230f6725..ccb474634e1c53fc208cf9d001ea1c5f417cfda6 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/src/Strainer/Services/Configuration/StrainerConfigurationBuilder.cs b/src/Strainer/Services/Configuration/StrainerConfigurationBuilder.cs index 9bbcd03baa2454dfc619f612409ed332a0272e56..498e1ffdec5c09d6024601f32dc5df42b725306d 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/src/Strainer/Services/Filtering/CustomFilterMethodBuilder.cs b/src/Strainer/Services/Filtering/CustomFilterMethodBuilder.cs index fb79c6324741b230bcd4716bf9dbdd56d1f28a48..1dac7c6f182932e92f727d0c36415d308e97c45a 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 0000000000000000000000000000000000000000..f203484e1128a46ed68a88642c2bab9b0036aaba --- /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 0000000000000000000000000000000000000000..ae273e3807965a910225aba68135f7c7137b5192 --- /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/FilterExpressionProvider.cs b/src/Strainer/Services/Filtering/FilterExpressionProvider.cs index 8a15bf938610eef446d59099e749f9e8b420a895..299b6da37dda14ee2a49bb80c2a3928760784249 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,10 +67,10 @@ public class FilterExpressionProvider : IFilterExpressionProvider FilterTermConstant = filterTermValue, FilterTermValue = filterTermValue, FinalExpression = null, + IsMaterializedQueryable = isMaterializedQueryable, 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 000efc65578704899f1d080bbb4531ca9dcf7284..dd8f4e04fb4ecc51e8e3f3447ac501b9bb2ad5bd 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; @@ -16,11 +15,11 @@ public class FilterExpressionWorkflowContext public Expression? FinalExpression { get; set; } + public bool IsMaterializedQueryable { get; set; } + public IPropertyMetadata? PropertyMetadata { get; set; } public Expression? PropertyValue { get; set; } public IFilterTerm? Term { get; set; } - - public ITypeConverter? TypeConverter { get; set; } } diff --git a/src/Strainer/Services/Filtering/FilterOperatorBuilder.cs b/src/Strainer/Services/Filtering/FilterOperatorBuilder.cs index e286c2bd1e1803f282e08869649014aeb57b8699..e268f9e108a43b37c115df9f1ad7378a7ebfe0c5 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 0000000000000000000000000000000000000000..749a8731a637c37c90d22b25a756f628855ff6eb --- /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 0000000000000000000000000000000000000000..6da2bd18137c7d6d9fce752dd0aa151067445cb5 --- /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 0000000000000000000000000000000000000000..4a3205e79a6264fbc355dc6f3cde54a2ceb60a5d --- /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 fefee36eb82596557f42d39ac9ecaa14df801c7c..7f34c97ade827c27df0cadbda6192e4f7e4983ec 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; @@ -34,13 +35,53 @@ public static class FilterOperatorMapper { return new List { - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.EqualsSymbol) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.EqualsSymbol) .HasName("equal") - .HasExpression((context) => Expression.Equal(context.FilterValue, context.PropertyValue)) + .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(symbol: FilterOperatorSymbols.DoesNotEqual) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.DoesNotEqual) .HasName("does not equal") - .HasExpression((context) => Expression.NotEqual(context.FilterValue, context.PropertyValue)) + .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(), }; } @@ -49,11 +90,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(), @@ -64,11 +105,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(), @@ -79,29 +120,74 @@ public static class FilterOperatorMapper { return new List { - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.Contains) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.Contains) .HasName("contains") + .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() - .HasExpression((context) => Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string) }), - context.FilterValue)) .Build(), - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.StartsWith) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.StartsWith) .HasName("starts with") + .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() - .HasExpression((context) => Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string) }), - context.FilterValue)) .Build(), - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.EndsWith) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.EndsWith) .HasName("ends with") + .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() - .HasExpression((context) => Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string) }), - context.FilterValue)) .Build(), }; } @@ -110,29 +196,74 @@ public static class FilterOperatorMapper { return new List { - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.DoesNotContain) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.DoesNotContain) .HasName("does not contain") + .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() - .HasExpression((context) => Expression.Not(Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string) }), - context.FilterValue))) .Build(), - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.DoesNotStartWith) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.DoesNotStartWith) .HasName("does not start with") + .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() - .HasExpression((context) => Expression.Not(Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string) }), - context.FilterValue))) .Build(), - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.DoesNotEndWith) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.DoesNotEndWith) .HasName("does not end with") + .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() - .HasExpression((context) => Expression.Not(Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string) }), - context.FilterValue))) .Build(), }; } @@ -141,16 +272,72 @@ public static class FilterOperatorMapper { return new List { - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.EqualsCaseInsensitive) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.EqualsCaseInsensitive) .HasName("equal (case insensitive)") + .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() - .HasExpression((context) => Expression.Equal(context.FilterValue, context.PropertyValue)) .IsCaseInsensitive() .Build(), - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.DoesNotEqualCaseInsensitive) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.DoesNotEqualCaseInsensitive) .HasName("does not equal (case insensitive)") + .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() - .HasExpression((context) => Expression.NotEqual(context.FilterValue, context.PropertyValue)) .IsCaseInsensitive() .Build(), }; @@ -160,31 +347,76 @@ public static class FilterOperatorMapper { return new List { - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.ContainsCaseInsensitive) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.ContainsCaseInsensitive) .HasName("contains (case insensitive)") + .HasExpression((context) => + { + 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) }), + context.FilterValue); + } + }) .IsStringBased() - .HasExpression((context) => Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string) }), - context.FilterValue)) .IsCaseInsensitive() .Build(), - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.StartsWithCaseInsensitive) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.StartsWithCaseInsensitive) .HasName("starts with (case insensitive)") + .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) }), + context.FilterValue); + } + }) .IsStringBased() - .HasExpression((context) => Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string) }), - context.FilterValue)) .IsCaseInsensitive() .Build(), - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.EndsWithCaseInsensitive) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.EndsWithCaseInsensitive) .HasName("ends with (case insensitive)") + .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) }), + context.FilterValue); + } + }) .IsStringBased() - .HasExpression((context) => Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string) }), - context.FilterValue)) .IsCaseInsensitive() .Build(), }; @@ -194,31 +426,76 @@ public static class FilterOperatorMapper { return new List { - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.DoesNotContainCaseInsensitive) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.DoesNotContainCaseInsensitive) .HasName("does not contain (case insensitive)") + .HasExpression((context) => + { + 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.Not(Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string) }), + context.FilterValue)); + } + }) .IsStringBased() - .HasExpression((context) => Expression.Not(Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string) }), - context.FilterValue))) .IsCaseInsensitive() .Build(), - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.DoesNotStartWithCaseInsensitive) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.DoesNotStartWithCaseInsensitive) .HasName("does not start with (case insensitive)") + .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.Not(Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string) }), + context.FilterValue)); + } + }) .IsStringBased() - .HasExpression((context) => Expression.Not(Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string) }), - context.FilterValue))) .IsCaseInsensitive() .Build(), - new FilterOperatorBuilder(symbol: FilterOperatorSymbols.DoesNotEndWithCaseInsensitive) + new FilterOperatorBuilder() + .HasSymbol(FilterOperatorSymbols.DoesNotEndWithCaseInsensitive) .HasName("does not end with (case insensitive)") + .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.Not(Expression.Call( + context.PropertyValue, + typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string) }), + context.FilterValue)); + } + }) .IsStringBased() - .HasExpression((context) => Expression.Not(Expression.Call( - context.PropertyValue, - typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string) }), - context.FilterValue))) .IsCaseInsensitive() .Build(), }; diff --git a/src/Strainer/Services/Filtering/ICustomFilterMethodBuilder.cs b/src/Strainer/Services/Filtering/ICustomFilterMethodBuilder.cs index 25ca3fe333a3557bd1d42d7682c4fe801d907437..4737ca374a81e6fec16b9defb3a43cfd579d788d 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 0000000000000000000000000000000000000000..58e0b6d23e095908545de76c41292a330e0de240 --- /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 0000000000000000000000000000000000000000..cc89297f79bdcea3ee23d4d7decd7c077dde2a74 --- /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/IFilterExpressionProvider.cs b/src/Strainer/Services/Filtering/IFilterExpressionProvider.cs index 252f6bcded8ecdab24168d5ec1077e899d487091..5f645f9315087de717fb247ccab4ca374081a2d2 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/IFilterOperatorBuilder.cs b/src/Strainer/Services/Filtering/IFilterOperatorBuilder.cs index 7028ddc2d234d9e4bdaa81e185eab1a519bcae68..b0258ab2a5400f4bf0934d753c135c65338af94c 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 0000000000000000000000000000000000000000..564c56cea433b3c6b23d78060e9bc026cd875f52 --- /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 0000000000000000000000000000000000000000..785c6438c0cfe51009f41d1f72fcdb89132034ef --- /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 0000000000000000000000000000000000000000..0ef799bd22bbb155485a98a4760da2c4b74e1bc0 --- /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/Filtering/Steps/ApplyFilterOperatorStep.cs b/src/Strainer/Services/Filtering/Steps/ApplyFilterOperatorStep.cs index 6fca398f66df268ba383973f4de808e6ae0d4791..5234a8ac37c89871ba1031c9a8500b59dfa912b1 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,11 +23,18 @@ 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 { - context.FinalExpression = context.Term.Operator.Expression(filterOperatorContext); + context.FinalExpression = context.Term.Operator.ExpressionProvider(filterOperatorContext); } catch (Exception ex) { diff --git a/src/Strainer/Services/Filtering/Steps/ChangeTypeOfFilterValueStep.cs b/src/Strainer/Services/Filtering/Steps/ChangeTypeOfFilterValueStep.cs index b2a77981aa3870e0b7329316148ad92dbef115a9..b6eb57f69f18f0e63000094d0f2dfd74849610b2 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 525806aedd9e74901c450fe44d2680abf21d42f9..e78e1ca60d36cf21afc8bd8051ebcb20653dc9ee 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/src/Strainer/Services/Filtering/Steps/MitigateCaseInsensitivityStep.cs b/src/Strainer/Services/Filtering/Steps/MitigateCaseInsensitivityStep.cs index 013d5b8bae129c84f7b877d7d26629c818ff77ff..c435808ca02bf0b2ecbfd390d1668b927c591cd0 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 0000000000000000000000000000000000000000..9dc4579694a5f65358848e2db35a1d739ef735c4 --- /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 0000000000000000000000000000000000000000..4d71558255bec58cdeeab04849180d9cd741063b --- /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/Metadata/Attributes/AttributeMetadataRetriever.cs b/src/Strainer/Services/Metadata/Attributes/AttributeMetadataRetriever.cs index ccd60e13d92d8a89ea1d1bcf66be70c467912bc4..b6f8dc1d3cb967018583cd7ffc6eb23472edfc67 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 1864cb017ede45760218770cbf477770157facdc..f9ee9420749f058a1c00bc12dba104e63d59686b 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 909886103f21e17da7501d25f17193d820c77de6..a34220bec2eae73832c8f31def0ba4c2b6d03d3c 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 c8c2f592a288398e8587854e6a498864ca496ccb..7459febf8ef1495ff01230fd3d1ec93615bba38e 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 25d541b080b6eccf4cada16b8db8603a6e7a172f..674c62f0c5865912aa74a862c4db20a7835278b9 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 e25fb48cfa098602bf92b22584f6493afb485a87..cc25d4f5a5ba0e1ab6a3b52f2391322b97347d6b 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(); } @@ -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.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.BuildPropertyMetadataFromPropertyInfo(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.BuildPropertyMetadataFromPropertyInfo(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 ad461bdaf4c99263d73a2cc11b9afafe668ffcf3..d9972977d89913312cb0d7b58139490106c47a27 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 BuildMetadataForProperty(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 ba2f058ae8823cd5255d9c967af4aa5f38dfb985..24a1bea425de6135919eb03e6b802c992d1c2e26 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 BuildMetadataForProperty(IObjectMetadata objectMetadata, PropertyInfo propertyInfo); } diff --git a/src/Strainer/Services/Metadata/IMetadataMapper.cs b/src/Strainer/Services/Metadata/IMetadataMapper.cs deleted file mode 100644 index e6cae92a9502b498b69fa12a6a15f6242390ba62..0000000000000000000000000000000000000000 --- 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 3f85ccd50f76dd3c8ffaf22dfb36a7b6a29c78ce..0000000000000000000000000000000000000000 --- 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/src/Strainer/Services/Metadata/ObjectMetadataBuilder.cs b/src/Strainer/Services/Metadata/ObjectMetadataBuilder.cs index 0ca4bc9cf40eb3b70808502ecbdae174573f4e8b..00bce8c900713e5976556f6e8f112d0443dfc73f 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 aef673b3b5e1c8442ecba8ee3e476bd583d7ca55..2c1a8e9491eb9645f8943f84d8f4aad6d4e32657 100644 --- a/src/Strainer/Services/Metadata/PropertyMetadataBuilder.cs +++ b/src/Strainer/Services/Metadata/PropertyMetadataBuilder.cs @@ -1,4 +1,6 @@ -using Fluorite.Strainer.Models.Metadata; +using Fluorite.Strainer.Exceptions; +using Fluorite.Strainer.Models.Metadata; +using Fluorite.Strainer.Models.Sorting; using Fluorite.Strainer.Services.Sorting; using System.Reflection; @@ -29,7 +31,7 @@ public class PropertyMetadataBuilder : IPropertyMetadataBuilder : IPropertyMetadataBuilder : IPropertyMetadataBuilder : IPropertyMetadataBuilder metadata, string displayName, string fullName) + { + 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/src/Strainer/Services/Modules/StrainerModuleBuilder{T}.cs b/src/Strainer/Services/Modules/StrainerModuleBuilder{T}.cs index b9b71bbd8f46f37faacd0249719ca07d957addd2..5d13060cd3c1c4c103018bac8734966ee55ff405 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/src/Strainer/Services/Pipelines/FilterPipelineOperation.cs b/src/Strainer/Services/Pipelines/FilterPipelineOperation.cs index e2b55123d02bd4d5cf30a0a4c3321c6fc37c5a9a..4b2eb68ad070dddd97edaa516accd334eead80f8 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 { @@ -66,7 +71,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/Pipelines/SortPipelineOperation.cs b/src/Strainer/Services/Pipelines/SortPipelineOperation.cs index ca724afbd4cc1a9fc2eb5d1d16506a9e5a67fbf2..b2e5ccdfc43cd32d519089dfbf21f75cb709c08e 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/CustomSortMethodBuilder.cs b/src/Strainer/Services/Sorting/CustomSortMethodBuilder.cs index a8e0865b4f81464b2947bb9de93b5922eb06ef12..8bfb311224e186747157ff0556e49d60c7a20259 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 0000000000000000000000000000000000000000..5cb33da17fb44ae80246596aef75ff9b6aa86329 --- /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 0000000000000000000000000000000000000000..9534c7b7301669bc45814f4985a03223a118047b --- /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/DescendingPrefixSortingWayFormatter.cs b/src/Strainer/Services/Sorting/DescendingPrefixSortingWayFormatter.cs index 8fbd6716e0c47523314673b368eaf3feadd58cb9..8ea77f937d9ee351734c482f7b955330860f8cdb 100644 --- a/src/Strainer/Services/Sorting/DescendingPrefixSortingWayFormatter.cs +++ b/src/Strainer/Services/Sorting/DescendingPrefixSortingWayFormatter.cs @@ -28,20 +28,10 @@ public class DescendingPrefixSortingWayFormatter : ISortingWayFormatter /// /// 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/ICustomSortMethodBuilder.cs b/src/Strainer/Services/Sorting/ICustomSortMethodBuilder.cs index 0fec1ecd13ef71cec481ee501c4097ecbd5ce8fd..4932d3caf2f1b9adf65685cedb4d937ccef3646e 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 0000000000000000000000000000000000000000..6e26c3e2b32ab3c7f221127f4e1bc79b3e42dd30 --- /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 0000000000000000000000000000000000000000..2b50adc5fcd9b2049941d6d5a7e982ce12b450da --- /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/src/Strainer/Services/Sorting/ISortExpressionProvider.cs b/src/Strainer/Services/Sorting/ISortExpressionProvider.cs index bc5229f6cd4ed1d34e7ab054c94ccec0aa3d0ccc..fe840fcad69850553abc0da9ee0ef5af98ee4c6b 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 ba9716d78413966fce33d557bfe77a84e562af94..cd2416a172c3c05dc5e3609793c68f7361f82dec 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/ISortingWayFormatter.cs b/src/Strainer/Services/Sorting/ISortingWayFormatter.cs index 8038b4cabe4dd8ac7cbcfcc4da75d39fdb56b076..4e6f290a117739e95dba7b0f6bebed13e4f84e7a 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 c8445269cc8b585ccf91ac1a363854e02c071213..b0a645112128e92d519828e501f1724dedc020be 100644 --- a/src/Strainer/Services/Sorting/SortExpressionProvider.cs +++ b/src/Strainer/Services/Sorting/SortExpressionProvider.cs @@ -1,9 +1,8 @@ -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; @@ -17,63 +16,59 @@ 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); } + /// public ISortExpression? GetDefaultExpression() { var propertyMetadata = _metadataProvidersFacade.GetDefaultMetadata(); - - if (propertyMetadata == null) + if (propertyMetadata is null) { 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 defaultSortingWay = propertyMetadata.DefaultSortingWay + ?? _strainerOptionsProvider.GetStrainerOptions().DefaultSortingWay; + var isDescending = defaultSortingWay == SortingWay.Descending; var sortTerm = new SortTerm(name) { - IsDescending = propertyMetadata.IsDefaultSortingDescending, + IsDescending = isDescending, }; - 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/SortPropertyMetadataBuilder.cs b/src/Strainer/Services/Sorting/SortPropertyMetadataBuilder.cs index e35dff679800cdeb01753ae1973096a06ecf1a02..63e6a06b2b69ad9a6739e7da5709c8f7f9bfeba7 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 fffac126dd1dda27814defb690a2fa0bf9cb542e..a06e629b2bc8a5fea107716a6f6810b279313d1c 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/SortingApplier.cs b/src/Strainer/Services/Sorting/SortingApplier.cs index 5925a91c05af6f86a27aa888b8f313b41561ed00..d5dd2a00aab2150e3251a867dadc3c8de5e12dac 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) { @@ -43,39 +43,35 @@ 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.PropertyInfo, sortTerm, isSubsequent); - if (sortExpression != null) - { - sortedSource = sortedSource.OrderWithSortExpression(sortExpression); - isSortingApplied = true; - } + var sortExpression = _sortExpressionProvider.GetExpression(metadata, sortTerm, isSubsequent); + sortedSource = (sortedSource ?? source).OrderWithSortExpression(sortExpression); + isSortingApplied = true; } else { - try + if (_customSortingExpressionProvider.TryGetCustomExpression(sortTerm, isSubsequent, out var sortExpression)) { - if (!_customSortingExpressionProvider.TryGetCustomExpression(sortTerm, isSubsequent, out var sortExpression)) + sortedSource = (sortedSource ?? source).OrderWithSortExpression(sortExpression!); + isSortingApplied = true; + } + else + { + if (options.ThrowExceptions) { - throw new StrainerMethodNotFoundException( + sortedSource = null; + + throw new StrainerSortNotFoundException( sortTerm.Name, $"Property or custom sorting method '{sortTerm.Name}' was not found."); } 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/src/Strainer/Services/Sorting/SuffixSortingWayFormatter.cs b/src/Strainer/Services/Sorting/SuffixSortingWayFormatter.cs index be77e881dc86d542c5afd0cc7edf4287eb12d0ee..c0a1ead3ae8a8274ca34842f4cb7543f866d17e9 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 . /// @@ -42,34 +39,32 @@ 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."), + }; + } } /// /// /// 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; @@ -80,7 +75,7 @@ public class SuffixSortingWayFormatter : ISortingWayFormatter return SortingWay.Ascending; } - return SortingWay.Unknown; + return null; } /// @@ -94,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; @@ -118,11 +106,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/src/Strainer/Services/StrainerContext.cs b/src/Strainer/Services/StrainerContext.cs index 42a123e61a8b5944b286f76ce92263f901ae470f..bba7464fc7ab72478172ca8c9e10f0aaa4f61767 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(); } diff --git a/src/Strainer/Services/StrainerProcessor.cs b/src/Strainer/Services/StrainerProcessor.cs index 442a2dd698a3cd809f757b864c2e3beda7bddd12..80e5bd3f2a485da4002df2752b159829bf01f315 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/src/Strainer/Services/Validation/FilterOperatorValidator.cs b/src/Strainer/Services/Validation/FilterOperatorValidator.cs index 3488e7d23ca429c0a9074bbc2e6d8fc1238ee023..a17d0fcdcabc1ced7e1741232d24f91f1b5b6d05 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/src/Strainer/Strainer.csproj b/src/Strainer/Strainer.csproj index 83475235d19bcc1c2cce78bc8ed565ca6154c5a0..278c8d8e041e06a950a49a4241b8a1769645d529 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/ 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 0d060cd4a7403f9f8135a235bdb0784073894496..60cb4ac66c76d54c45664a07b801989df432fec6 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.IntegrationTests/Filtering/Operators/ContainsCaseInsensitiveOperatorTests.cs b/test/Strainer.IntegrationTests/Filtering/Operators/ContainsCaseInsensitiveOperatorTests.cs index f1183234e6970d9d3126058dff1918ed67b39585..c5343988e831f9d851495a42227c14614c06c9b4 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.IntegrationTests/Strainer.IntegrationTests.csproj b/test/Strainer.IntegrationTests/Strainer.IntegrationTests.csproj index ebf263cafd210e90548d1b13e7701b5c9fe86fb9..91e42b8312d769e413f32410360c5327550e496b 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/DependencyTests.cs b/test/Strainer.UnitTests/DependencyTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..16548118121be356a1e8924f4020a0f92612f2fa --- /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); + } +} diff --git a/test/Strainer.UnitTests/Extensions/DepedencyInjection/StrainerServiceCollectionExtensionsTests.cs b/test/Strainer.UnitTests/Extensions/DepedencyInjection/StrainerServiceCollectionExtensionsTests.cs index a64bb01ca594cd162243c3e5525ddbe5a672abcd..11f6185280e42061cfbc6ebd2e27a3640460a228 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(); @@ -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() { @@ -387,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/Configuration/GenericModuleLoadingStrategyTests.cs b/test/Strainer.UnitTests/Services/Configuration/GenericModuleLoadingStrategyTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..8c3ec42b745b8c1dd7016f7569d2c6ec6ca220b4 --- /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); + } +} diff --git a/test/Strainer.UnitTests/Services/Configuration/StrainerConfigurationBuilderTests.cs b/test/Strainer.UnitTests/Services/Configuration/StrainerConfigurationBuilderTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..cc9abbd2bfbf7ce53860361ae595ec6e107d757e --- /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/Configuration/StrainerModuleBuilderFactoryTests.cs b/test/Strainer.UnitTests/Services/Configuration/StrainerModuleBuilderFactoryTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..f6007ebf6b2a5af919f4dbcfc7436f89290ab81b --- /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(); + } +} diff --git a/test/Strainer.UnitTests/Services/Conversion/StringValueConverterTests.cs b/test/Strainer.UnitTests/Services/Conversion/StringValueConverterTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..4af083cc1f446f3319d40b2c7dc63b8ca89cbd14 --- /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); + } +} diff --git a/test/Strainer.UnitTests/Services/Filtering/CustomFilterMethodBuilderTests.cs b/test/Strainer.UnitTests/Services/Filtering/CustomFilterMethodBuilderTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..ce8dfa4155795f9bf8f675735d08ccd390580606 --- /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/CustomFilteringExpressionProviderTests.cs b/test/Strainer.UnitTests/Services/Filtering/CustomFilteringExpressionProviderTests.cs index b3a1150c1933cffcb4e349e0166e0b96aa6eb463..1a284cfc6560a721a8d296db13405876c16e23ac 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/FilterContextTest.cs b/test/Strainer.UnitTests/Services/Filtering/FilterContextTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..0ca21d519b8ae5e09d957592cdef626db3ac1971 --- /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/Filtering/FilterExpressionProviderTests.cs b/test/Strainer.UnitTests/Services/Filtering/FilterExpressionProviderTests.cs index 733da16543409542ec95482272fbe398794bfd83..490d5e483d628f2e88b09db6da77ea8c3cad8a72 100644 --- a/test/Strainer.UnitTests/Services/Filtering/FilterExpressionProviderTests.cs +++ b/test/Strainer.UnitTests/Services/Filtering/FilterExpressionProviderTests.cs @@ -1,8 +1,6 @@ using Fluorite.Strainer.Models.Filtering.Terms; 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; @@ -10,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; @@ -18,7 +15,6 @@ public class FilterExpressionProviderTests public FilterExpressionProviderTests() { _provider = new FilterExpressionProvider( - _typeConverterProviderMock, _filterExpressionWorkflowBuilderMock); } @@ -33,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(); @@ -50,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(); @@ -71,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() @@ -84,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); @@ -97,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); @@ -109,12 +101,11 @@ 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 - var result = _provider.GetExpression(metadata, filterTerm, parameterExpression, innerExpression: null); + var result = _provider.GetExpression(metadata, filterTerm, parameterExpression, innerExpression: null, isMaterializedQueryable: true); // Assert result.Should().NotBeNull(); @@ -127,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); @@ -141,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); @@ -153,12 +140,11 @@ 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 - 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/FilterOperatorBuilderTests.cs b/test/Strainer.UnitTests/Services/Filtering/FilterOperatorBuilderTests.cs index 1bb1ab6b8bfed31e1836c3fdb682c5a5f1864fee..eecd7b73fc8c5fbabbed403d8ab7da8a2b03499b 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(); @@ -26,7 +27,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/FilterOperatorMapperTests.cs b/test/Strainer.UnitTests/Services/Filtering/FilterOperatorMapperTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..fbac93d92a70127cd333d654813b4bbb01143778 --- /dev/null +++ b/test/Strainer.UnitTests/Services/Filtering/FilterOperatorMapperTests.cs @@ -0,0 +1,722 @@ +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, 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(property, filter, operatorSymbol, isCaseInsensitiveForValues, isMaterializedQueryable); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [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(property, filter, operatorSymbol, isCaseInsensitiveForValues, isMaterializedQueryable); + + // 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, 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(property, filter, 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, 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(property, filter, 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, 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(property, filter, 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, 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(property, filter, operatorSymbol); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [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(property, filter, operatorSymbol, isCaseInsensitiveForValues, isMaterializedQueryable); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [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)] + [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, + bool isCaseInsensitiveForValues, + bool isMaterializedQueryable, + bool expectedEqual) + { + // Arrange + var operatorSymbol = FilterOperatorSymbols.DoesNotContain; + var lambda = BuildLambdaExpression(property, filter, operatorSymbol, isCaseInsensitiveForValues, isMaterializedQueryable); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [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(property, filter, operatorSymbol, isCaseInsensitiveForValues, isMaterializedQueryable); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [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(property, filter, operatorSymbol, isCaseInsensitiveForValues, isMaterializedQueryable); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [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(property, filter, operatorSymbol, isCaseInsensitiveForValues, isMaterializedQueryable); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [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(property, filter, operatorSymbol, isCaseInsensitiveForValues, isMaterializedQueryable); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [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(property, filter, operatorSymbol, isMaterializedQueryable: isMaterializedQueryable); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [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(property, filter, operatorSymbol, isMaterializedQueryable: isMaterializedQueryable); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [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(property, filter, operatorSymbol, isMaterializedQueryable: isMaterializedQueryable); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [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(property, filter, operatorSymbol, isMaterializedQueryable: isMaterializedQueryable); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [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(property, filter, operatorSymbol, isMaterializedQueryable: isMaterializedQueryable); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [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(property, filter, operatorSymbol, isMaterializedQueryable: isMaterializedQueryable); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [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(property, filter, operatorSymbol, isMaterializedQueryable: isMaterializedQueryable); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + [Theory] + [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(property, filter, operatorSymbol, isMaterializedQueryable: isMaterializedQueryable); + + // Act + var func = lambda.Compile(); + var result = func.Invoke(); + + // Assert + result.Should().Be(expectedEqual); + } + + private static Expression> BuildLambdaExpression( + T property, + T filter, + string operatorSymbol, + bool isCaseInsensitiveForValues = false, + bool isMaterializedQueryable = true) + { + var filterOperator = DefaultOperators[operatorSymbol]; + var propertyValue = Expression.Constant(property, typeof(T)); + var filterValue = Expression.Constant(filter, typeof(T)); + 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/FilterOperatorValidatorTests.cs b/test/Strainer.UnitTests/Services/Filtering/FilterOperatorValidatorTests.cs index 505f3d12fe9b0abba869990e9bc09104f9ace728..ec4408cdc7660bb17422989e7213d32f6b9da074 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; @@ -75,7 +74,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,13 +85,26 @@ 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() { // 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/FilterTermParserTests.cs b/test/Strainer.UnitTests/Services/Filtering/FilterTermParserTests.cs index 8d937961826496b4058b5131aca35a0b87a9b9bf..09b6fd5e77984cdb2991bfb9db4b42dbdfac9a5f 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); } } diff --git a/test/Strainer.UnitTests/Services/Filtering/Steps/ApplyFilterOperatorStepTests.cs b/test/Strainer.UnitTests/Services/Filtering/Steps/ApplyFilterOperatorStepTests.cs index aa917f086ec10dd29fa0e9cabe75d7103066e76b..eb0737c6f3fbd6363bc600d241580cebf4ed7a27 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] @@ -38,7 +42,7 @@ public class ApplyFilterOperatorStepTests .Returns(finalExpression); var filterOperator = Substitute.For(); filterOperator - .Expression + .ExpressionProvider .Returns(funcMock); var filterTerm = Substitute.For(); filterTerm @@ -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] @@ -72,7 +81,7 @@ public class ApplyFilterOperatorStepTests .Throws(innerException); var filterOperator = Substitute.For(); filterOperator - .Expression + .ExpressionProvider .Returns(func); var filterTerm = Substitute.For(); filterTerm @@ -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/Filtering/Steps/ChangeTypeOfFilterValueStepTests.cs b/test/Strainer.UnitTests/Services/Filtering/Steps/ChangeTypeOfFilterValueStepTests.cs index d691252a5c2b87e54cd5e54e32ad407fde412263..a512d16aff71df3499f288d106487ef5836ca77c 100644 --- a/test/Strainer.UnitTests/Services/Filtering/Steps/ChangeTypeOfFilterValueStepTests.cs +++ b/test/Strainer.UnitTests/Services/Filtering/Steps/ChangeTypeOfFilterValueStepTests.cs @@ -11,13 +11,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 +34,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 +45,10 @@ public class ChangeTypeOfFilterValueStepTests _step.Execute(context); // Assert - _typeChangeMock + _typeChangerMock .DidNotReceive() .ChangeType(Arg.Any(), Arg.Any()); + _typeConverterProviderMock.ReceivedCalls().Should().BeEmpty(); } [Fact] @@ -69,10 +71,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 +84,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 +109,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 +135,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 +165,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 43c07b003cacee955bbcbe6a37dbe8401016d905..2b30f32f737fe9857b8077f97182185bffd5b16a 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)); } } 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 0000000000000000000000000000000000000000..f5dace03dd83a597bbbcec787a5d274b5b3f31ba --- /dev/null +++ b/test/Strainer.UnitTests/Services/Filtering/Steps/FilterExpressionWorkflowBuilderTests.cs @@ -0,0 +1,27 @@ +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() + { + // Arrange + var builder = new FilterExpressionWorkflowBuilder( + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For()); + + // Act + var result = builder.BuildDefaultWorkflow(); + + // Assert + result.Should().NotBeNull(); + result.Should().BeOfType(); + } +} diff --git a/test/Strainer.UnitTests/Services/Filtering/Steps/MitigateCaseInsensitivityStepTests.cs b/test/Strainer.UnitTests/Services/Filtering/Steps/MitigateCaseInsensitivityStepTests.cs index 793da276169ebb245565d6e0e6776ea563d36890..d884fba2a7c6ab35263b05b4967c2bc4d7d247da 100644 --- a/test/Strainer.UnitTests/Services/Filtering/Steps/MitigateCaseInsensitivityStepTests.cs +++ b/test/Strainer.UnitTests/Services/Filtering/Steps/MitigateCaseInsensitivityStepTests.cs @@ -20,6 +20,44 @@ 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 + { + IsMaterializedQueryable = true, + 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() { diff --git a/test/Strainer.UnitTests/Services/Linq/QueryableEvaluatorTests.cs b/test/Strainer.UnitTests/Services/Linq/QueryableEvaluatorTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..946016d55a240836278733f8727298dbb596d650 --- /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/Attributes/AttributeMetadataProviderTests.cs b/test/Strainer.UnitTests/Services/Metadata/Attributes/AttributeMetadataProviderTests.cs index b3bc3205091d008768e968c4d23998110a54ebce..6d71871dd4174a225374c4054e44fc0522aa9e4b 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; @@ -69,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() { diff --git a/test/Strainer.UnitTests/Services/Metadata/Attributes/AttributeMetadataRetrieverTests.cs b/test/Strainer.UnitTests/Services/Metadata/Attributes/AttributeMetadataRetrieverTests.cs index 4a9935c88a65be46a1210c30f01610ede1047fed..6b95454d5db5387f4d1bb980369f3c8cbebedac4 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; @@ -68,7 +67,7 @@ public class AttributeMetadataRetrieverTests Action act = () => _retriever.GetDefaultMetadataFromObjectAttribute(modelType); // Assert - act.Should().Throw(); + act.Should().ThrowExactly(); } [Fact] @@ -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 67cf1b7fe3c6ee7eda8acb0aeefd22fa1811a769..ad4cfe7d71e0bd9f8fde71a23394fe48ce97d7ce 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 07f2bb65352b109a54cf540649892d5c42550b0c..dbdadc7c0fa1c924510cfeb98463058b4b928dcc 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 45a488fdbc1ac05603fc91cdcd63fa673fd727a7..2939cb790034d11468626e83cdf85e6def3e9917 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() { @@ -67,7 +85,7 @@ public class FluentApiMetadataProviderTests .GetObjectMetadata() .Returns(objectMetadataDictionary); _propertyMetadataBuilderMock - .BuildPropertyMetadata(objectMetadata) + .BuildDefaultMetadata(objectMetadata) .Returns(propertyMetadata); // Act @@ -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() { @@ -277,7 +317,7 @@ public class FluentApiMetadataProviderTests .GetPropertyInfo(typeof(Post), name) .Returns(propertyInfo); _propertyMetadataBuilderMock - .BuildPropertyMetadataFromPropertyInfo(objectMetadata, propertyInfo) + .BuildMetadataForProperty(objectMetadata, propertyInfo) .Returns(propertyMetadata); // Act @@ -327,7 +367,7 @@ public class FluentApiMetadataProviderTests .GetPropertyInfo(typeof(Post), name) .Returns(propertyInfo); _propertyMetadataBuilderMock - .BuildPropertyMetadataFromPropertyInfo(objectMetadata, propertyInfo) + .BuildMetadataForProperty(objectMetadata, propertyInfo) .Returns(propertyMetadata); // Act @@ -381,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 @@ -396,7 +436,7 @@ public class FluentApiMetadataProviderTests } [Fact] - public void GetPropertyMetadata_Returns_EmptyMetadata_When_NoMetadataIsAvailable() + public void GetAllPropertyMetadata_Returns_EmptyMetadata_When_NoMetadataIsAvailable() { // Arrange _optionsProviderMock @@ -418,6 +458,101 @@ 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() + { + // 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() { @@ -482,7 +617,7 @@ public class FluentApiMetadataProviderTests .GetPropertyInfos(typeof(Post)) .Returns(propertyInfos); _propertyMetadataBuilderMock - .BuildPropertyMetadataFromPropertyInfo(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 new file mode 100644 index 0000000000000000000000000000000000000000..65b197b18d9b3b0732ca3983083f2a99e103fb0f --- /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.BuildMetadataForProperty(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.BuildMetadataForProperty(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 260549340ac08faa31722579ed874d3deac653f8..3e7ebfcfc2fde3957e95cd3cbe38610e1d46f6ca 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/MetadataMapperTests.cs b/test/Strainer.UnitTests/Services/Metadata/MetadataMapperTests.cs deleted file mode 100644 index 19804782f78362102ff6ffaff2c3245cdfacde5f..0000000000000000000000000000000000000000 --- 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; } - } -} diff --git a/test/Strainer.UnitTests/Services/Metadata/ObjectMetadataBuilderTests.cs b/test/Strainer.UnitTests/Services/Metadata/ObjectMetadataBuilderTests.cs index 039d95b3b9eb7cc39dbb664a07292e6f65398861..8cb910af8d2486cde1ad3881b55981a49306db64 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/PropertyInfoProviderTests.cs b/test/Strainer.UnitTests/Services/Metadata/PropertyInfoProviderTests.cs index cc9cb691f48acebb7af0bf00301f74d55931a9f7..af5ca4f1880f14a75e737a3e57fbd2464a5b8c5d 100644 --- a/test/Strainer.UnitTests/Services/Metadata/PropertyInfoProviderTests.cs +++ b/test/Strainer.UnitTests/Services/Metadata/PropertyInfoProviderTests.cs @@ -1,12 +1,29 @@ 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 type = typeof(string); + var name = nameof(string.Length); + var provider = new PropertyInfoProvider(); + + // Act + var result = provider.GetPropertyInfo(type, name); + + // Assert + result.Should().NotBeNull(); + result.Equals(type.GetProperty(name)).Should().BeTrue(); + } + + [Fact] + public void Provider_Returns_PropertyInfoAndFullName() { // Arrange Expression> expression = s => s.Property; @@ -22,7 +39,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 +55,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 +67,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 +78,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 diff --git a/test/Strainer.UnitTests/Services/Metadata/PropertyMetadataBuilderTests.cs b/test/Strainer.UnitTests/Services/Metadata/PropertyMetadataBuilderTests.cs index d6fe79e6272fee5c2a1d719e270726f6f990c4c2..319a110a70e902e16ed66c9ec75ec5a6caa5863d 100644 --- a/test/Strainer.UnitTests/Services/Metadata/PropertyMetadataBuilderTests.cs +++ b/test/Strainer.UnitTests/Services/Metadata/PropertyMetadataBuilderTests.cs @@ -1,4 +1,5 @@ -using Fluorite.Strainer.Models.Metadata; +using Fluorite.Strainer.Exceptions; +using Fluorite.Strainer.Models.Metadata; using Fluorite.Strainer.Services.Metadata; namespace Fluorite.Strainer.UnitTests.Services.Metadata; @@ -29,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); @@ -61,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); @@ -93,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); @@ -128,15 +129,128 @@ 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); 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].DefaultSortingWay.Should().BeNull(); + 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 new file mode 100644 index 0000000000000000000000000000000000000000..451e17889b9c01815137cd38d076f40d695685c3 --- /dev/null +++ b/test/Strainer.UnitTests/Services/Metadata/SortPropertyMetadataBuilderTests.cs @@ -0,0 +1,89 @@ +using Fluorite.Strainer.Models.Metadata; +using Fluorite.Strainer.Models.Sorting; +using Fluorite.Strainer.Services.Sorting; + +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].DefaultSortingWay.Should().BeNull(); + 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); + defaultMetadata.Should().BeEmpty(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + 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); + 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].DefaultSortingWay.Should().Be(sortingWay); + 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); + defaultMetadata.Should().NotBeEmpty(); + defaultMetadata.Keys.Should().BeEquivalentTo([type]); + defaultMetadata.Values.First().Name.Should().Be(propertyName); + } + + private class Blog + { + public string Title { get; set; } + } +} diff --git a/test/Strainer.UnitTests/Services/Modules/GenericStrainerModuleBuilderTests.cs b/test/Strainer.UnitTests/Services/Modules/GenericStrainerModuleBuilderTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..3dd6a1d5d6cf6f7b9c6bd4eeaae07659324eda9e --- /dev/null +++ b/test/Strainer.UnitTests/Services/Modules/GenericStrainerModuleBuilderTests.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 GenericStrainerModuleBuilderTests +{ + 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(1) + .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; } + } +} diff --git a/test/Strainer.UnitTests/Services/Modules/StrainerModuleBuilderTests.cs b/test/Strainer.UnitTests/Services/Modules/StrainerModuleBuilderTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..cddf9551cab6097f0ee122ae00bb68af6bedcc2d --- /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; } + } +} diff --git a/test/Strainer.UnitTests/Services/Pagination/PageSizeEvaluatorTests.cs b/test/Strainer.UnitTests/Services/Pagination/PageSizeEvaluatorTests.cs index 82dbebcbe7480f541b603457a7cf9e73b155d04b..3156632b5cf1dff3dcc6903e350498180cd1411c 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); + } } diff --git a/test/Strainer.UnitTests/Services/Pipelines/FilterPipelineOperationTests.cs b/test/Strainer.UnitTests/Services/Pipelines/FilterPipelineOperationTests.cs index cf62ca5263537b95eccd613b125652bc06ee7aff..d75e086e3460e37e15041c96344b4e17d3cdb462 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,7 +112,8 @@ public class FilterPipelineOperationTests { // Arrange var source = new[] { "foo" }.AsQueryable(); - var sourceClone = ((string[])new[] { "foo" }.Clone()).AsQueryable(); + var isMaterializedQueryable = true; + var sourceClone = source.ToArray(); var model = new StrainerModel { Filters = "input", @@ -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); @@ -175,11 +196,12 @@ 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(); _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,7 +253,8 @@ public class FilterPipelineOperationTests { // Arrange var source = new[] { "foo" }.AsQueryable(); - var sourceClone = ((string[])new[] { "foo" }.Clone()).AsQueryable(); + var isMaterializedQueryable = true; + var sourceClone = source.ToArray(); var model = new StrainerModel { Filters = "input", @@ -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,7 +307,8 @@ public class FilterPipelineOperationTests { // Arrange var source = new[] { "foo", "boat", "ID" }.AsQueryable(); - var sourceClone = ((string[])new[] { "foo" }.Clone()).AsQueryable(); + var isMaterializedQueryable = true; + var sourceClone = source.ToArray(); var model = new StrainerModel { Filters = "input", @@ -297,6 +330,77 @@ public class FilterPipelineOperationTests _filterTermParser .GetParsedTerms(model.Filters) .Returns(terms); + _queryableEvaluator + .IsMaterialized(source) + .Returns(isMaterializedQueryable); + _metadataFacade + .GetMetadata(isSortableRequired: false, isFilterableRequired: true, filterTermName1) + .Returns(metadata1); + _metadataFacade + .GetMetadata(isSortableRequired: false, isFilterableRequired: true, filterTermName2) + .Returns(metadata2); + _filterExpressionProvider + .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), isMaterializedQueryable) + .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); + _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(), isMaterializedQueryable); + _filterExpressionProvider + .Received(1) + .GetExpression(metadata2, filterTerm, Arg.Any(), Arg.Is(x => x != null), isMaterializedQueryable); + } + + [Fact] + public void Should_Return_CollectionWithMultipleFilteringsApplied_FromTheMultipleTerms() + { + // Arrange + var source = new[] { "foo", "boat", "ID" }.AsQueryable(); + var isMaterializedQueryable = true; + 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); + _queryableEvaluator + .IsMaterialized(source) + .Returns(isMaterializedQueryable); _metadataFacade .GetMetadata(isSortableRequired: false, isFilterableRequired: true, filterTermName1) .Returns(metadata1); @@ -304,10 +408,10 @@ public class FilterPipelineOperationTests .GetMetadata(isSortableRequired: false, isFilterableRequired: true, filterTermName2) .Returns(metadata2); _filterExpressionProvider - .GetExpression(metadata1, filterTerm, Arg.Any(), Arg.Any()) + .GetExpression(metadata1, filterTerm1, 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, filterTerm2, Arg.Any(), Arg.Any(), isMaterializedQueryable) .Returns(x => CreateStringGreaterThanLengthExpression(x[2] as ParameterExpression, length: 3)); // Act @@ -320,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, filterTerm, Arg.Any(), Arg.Any()); - _filterExpressionProvider.Received(1).GetExpression(metadata2, filterTerm, Arg.Any(), Arg.Is(x => x != null)); + _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) diff --git a/test/Strainer.UnitTests/Services/Pipelines/PipelineContextTests.cs b/test/Strainer.UnitTests/Services/Pipelines/PipelineContextTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..51d38083e4426744760dcc14ede407e0c857d28c --- /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/SortPipelineOperationTests.cs b/test/Strainer.UnitTests/Services/Pipelines/SortPipelineOperationTests.cs index 5e434d04121397cb5c805421b82864b69acce0a9..a9ce326da84e1fa5d8bb2726c310f5130138bb6d 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/Pipelines/StrainerPipelineBuilderFactoryTests.cs b/test/Strainer.UnitTests/Services/Pipelines/StrainerPipelineBuilderFactoryTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..b1a25e8a220e0f643d512914fc9f16601e0fd3f2 --- /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/CustomSortMethodBuilderTests.cs b/test/Strainer.UnitTests/Services/Sorting/CustomSortMethodBuilderTests.cs index d9efea533aa0e8bd29faf59c979bc72787718137..b1016358da13a8e98d26b88dd4b2bb2be390a2f8 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(); } diff --git a/test/Strainer.UnitTests/Services/Sorting/CustomSortingExpressionProviderTests.cs b/test/Strainer.UnitTests/Services/Sorting/CustomSortingExpressionProviderTests.cs index 0c65df08f598d6262feabe614b465252240e4c8e..f3ed2d5980362ecb187a1c2f2abc273dc626973d 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 1f55f5215d4f32f0136f4b3294d8f0606fcdac92..0362aafcd7e84895cec30303b53d9595a1222db4 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/SortContextTests.cs b/test/Strainer.UnitTests/Services/Sorting/SortContextTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..fbd8a486ba5b5ae9479045f715b8eef1097e3ae1 --- /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/Sorting/SortExpressionProviderTests.cs b/test/Strainer.UnitTests/Services/Sorting/SortExpressionProviderTests.cs index 9dfb5ef5e910578661326759b24f778a51d848e4..99de9e5f0bb2668b3966b9647e7218f193645f88 100644 --- a/test/Strainer.UnitTests/Services/Sorting/SortExpressionProviderTests.cs +++ b/test/Strainer.UnitTests/Services/Sorting/SortExpressionProviderTests.cs @@ -1,8 +1,11 @@ -using Fluorite.Strainer.Models.Metadata; +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.ReturnsExtensions; +using NSubstitute.ReceivedExtensions; using System.Reflection; namespace Fluorite.Strainer.UnitTests.Services.Sorting; @@ -10,12 +13,111 @@ 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] + 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_Returns_DefaultExpression() + { + // 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.DefaultSortingWay.Returns(sortingWay); + defaultMetadata.Name.Returns(name); + + _metadataProvidersFacadeMock + .GetDefaultMetadata() + .Returns(defaultMetadata); + + // Act + var result = _provider.GetDefaultExpression(); + + // Assert + result.Should().NotBeNull(); + result.Expression.Should().NotBeNull(); + result.IsDefault.Should().BeTrue(); + result.IsDescending.Should().Be(sortingWay == SortingWay.Descending); + result.IsSubsequent.Should().BeFalse(); + + _metadataProvidersFacadeMock + .Received(1) + .GetDefaultMetadata(); + _strainerOptionsProviderMock + .DidNotReceive() + .GetStrainerOptions(); } [Fact] @@ -31,9 +133,9 @@ public class SortExpressionProviderTests { IsDescending = false, }; - var sortTerms = new Dictionary + var sortTerms = new Dictionary { - { propertyInfo, sortTerm }, + { propertyMetadata, sortTerm }, }; _metadataProvidersFacadeMock @@ -50,57 +152,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(); @@ -131,11 +207,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]) @@ -147,6 +218,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) diff --git a/test/Strainer.UnitTests/Services/Sorting/SortTermParserTests.cs b/test/Strainer.UnitTests/Services/Sorting/SortTermParserTests.cs index e939c1df8bda66a73ecae1cfbddf070d5a244c54..66e780dfb4b043daa409b3ec269d72a5068ecaa1 100644 --- a/test/Strainer.UnitTests/Services/Sorting/SortTermParserTests.cs +++ b/test/Strainer.UnitTests/Services/Sorting/SortTermParserTests.cs @@ -80,9 +80,47 @@ 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 defaultSortingWay = SortingWay.Ascending; + var options = new StrainerOptions + { + DefaultSortingWay = defaultSortingWay, + }; + + _sortTermValueParserMock + .GetParsedValues(input) + .Returns([parsedInput]); + _sortingWayFormatterMock + .GetSortingWay(parsedInput) + .ReturnsNull(); + _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(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 new file mode 100644 index 0000000000000000000000000000000000000000..48137978303fbde484b907a9da8a6a080f030223 --- /dev/null +++ b/test/Strainer.UnitTests/Services/Sorting/SortingApplierTests.cs @@ -0,0 +1,247 @@ +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 System.Linq.Expressions; + +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_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() + { + // Arrange + 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); + + // Assert + result.Should().BeFalse(); + sortedSource.Should().BeNull(); + + _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 + { + } +} diff --git a/test/Strainer.UnitTests/Services/Sorting/SuffixSortingWayFormatterTests.cs b/test/Strainer.UnitTests/Services/Sorting/SuffixSortingWayFormatterTests.cs index d1f34b9825d30101d2f2a27071f345c417c5da75..535003507aafc24c94845f9c9de751d741d42b37 100644 --- a/test/Strainer.UnitTests/Services/Sorting/SuffixSortingWayFormatterTests.cs +++ b/test/Strainer.UnitTests/Services/Sorting/SuffixSortingWayFormatterTests.cs @@ -30,18 +30,18 @@ public class SuffixSortingWayFormatterTests } [Fact] - public void Formatter_Throws_ForUnkownSortingWay_WhenFormatting() + public void Formatter_Throws_ForInvalidSortingWay() { // Arrange - var input = string.Empty; - var sortingWay = SortingWay.Unknown; + var input = "foo"; + SortingWay sortingWay = default; // Act Action act = () => _formatter.Format(input, sortingWay); // Assert act.Should().ThrowExactly() - .WithMessage($"{nameof(sortingWay)} cannot be {nameof(SortingWay.Unknown)}.*"); + .WithMessage($"{nameof(sortingWay)} with value '{sortingWay}' is not supported."); } [Theory] @@ -63,23 +63,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 +98,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/Services/StrainerContextTests.cs b/test/Strainer.UnitTests/Services/StrainerContextTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..aaf1b71ad8a3882fa0a05fd2e3cccacf8deefd57 --- /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); + } +} diff --git a/test/Strainer.UnitTests/Services/StrainerProcessorTests.cs b/test/Strainer.UnitTests/Services/StrainerProcessorTests.cs index a3664cbdee3a9c56ad5a279e78ce1d3d19cff540..fdbcac830ece1b5c4293ca89ace742715fdbda2b 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); diff --git a/test/Strainer.UnitTests/Strainer.UnitTests.csproj b/test/Strainer.UnitTests/Strainer.UnitTests.csproj index ae0da83d3cf5ec1b74704e11349eaffbb217dced..1011c22ad9249b42da93d4164a83e37bab5351bf 100644 --- a/test/Strainer.UnitTests/Strainer.UnitTests.csproj +++ b/test/Strainer.UnitTests/Strainer.UnitTests.csproj @@ -12,19 +12,23 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers diff --git a/test/Strainer.UnitTests/_GlobalUsings.cs b/test/Strainer.UnitTests/_GlobalUsings.cs index 9987e03ad6236396c44f4b5a79845cb29b6821ba..a58c13f67689b69e75800c20e9f116bee6c3e3cf 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;