From 8f953c3d25e87628305c0a035c65efbab48c7330 Mon Sep 17 00:00:00 2001 From: Fati Iseni Date: Thu, 5 Jun 2025 12:27:53 +0200 Subject: [PATCH 01/11] Implement an infrastructure for auto discovery of evaluators and validators. --- .../Evaluators/AsNoTrackingEvaluator.cs | 1 + ...TrackingWithIdentityResolutionEvaluator.cs | 1 + .../Evaluators/AsSplitQueryEvaluator.cs | 1 + .../Evaluators/AsTrackingEvaluator.cs | 1 + .../Evaluators/IgnoreAutoIncludesEvaluator.cs | 1 + .../Evaluators/IgnoreQueryFiltersEvaluator.cs | 1 + .../Evaluators/IncludeEvaluator.cs | 1 + .../Evaluators/IncludeStringEvaluator.cs | 1 + .../Evaluators/LikeEvaluator.cs | 1 + .../Evaluators/QueryTagEvaluator.cs | 1 + .../Evaluators/SpecificationEvaluator.cs | 23 +++----- src/QuerySpecification/DiscoveryAttribute.cs | 26 ++++++++ src/QuerySpecification/DiscoveryStrategy.cs | 22 +++++++ .../Evaluators/LikeMemoryEvaluator.cs | 1 + .../Evaluators/OrderEvaluator.cs | 1 + .../SpecificationInMemoryEvaluator.cs | 14 ++--- .../Evaluators/WhereEvaluator.cs | 1 + .../Internals/TypeHelper.cs | 59 +++++++++++++++++++ .../Internals/TypeProvider.cs | 45 ++++++++++++++ .../Validators/LikeValidator.cs | 1 + .../Validators/SpecificationValidator.cs | 13 ++-- .../Validators/WhereValidator.cs | 1 + .../Evaluators/SpecificationEvaluatorTests.cs | 18 +++--- .../SpecificationInMemoryEvaluatorTests.cs | 4 +- 24 files changed, 199 insertions(+), 40 deletions(-) create mode 100644 src/QuerySpecification/DiscoveryAttribute.cs create mode 100644 src/QuerySpecification/DiscoveryStrategy.cs create mode 100644 src/QuerySpecification/Internals/TypeHelper.cs create mode 100644 src/QuerySpecification/Internals/TypeProvider.cs diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsNoTrackingEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsNoTrackingEvaluator.cs index be517e36..95ec058a 100644 --- a/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsNoTrackingEvaluator.cs +++ b/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsNoTrackingEvaluator.cs @@ -3,6 +3,7 @@ /// /// Evaluator to apply AsNoTracking to the query if the specification has AsNoTracking set to true. /// +[EvaluatorDiscovery(Order = -55)] public sealed class AsNoTrackingEvaluator : IEvaluator { /// diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsNoTrackingWithIdentityResolutionEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsNoTrackingWithIdentityResolutionEvaluator.cs index 5232a1cb..f16f4c04 100644 --- a/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsNoTrackingWithIdentityResolutionEvaluator.cs +++ b/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsNoTrackingWithIdentityResolutionEvaluator.cs @@ -3,6 +3,7 @@ /// /// Evaluator to apply AsNoTracking to the query if the specification has AsNoTracking set to true. /// +[EvaluatorDiscovery(Order = -50)] public sealed class AsNoTrackingWithIdentityResolutionEvaluator : IEvaluator { /// diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsSplitQueryEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsSplitQueryEvaluator.cs index a831452a..1fdc99b3 100644 --- a/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsSplitQueryEvaluator.cs +++ b/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsSplitQueryEvaluator.cs @@ -3,6 +3,7 @@ /// /// Evaluator to apply AsSplitQuery to the query if the specification has AsSplitQuery set to true. /// +[EvaluatorDiscovery(Order = -60)] public sealed class AsSplitQueryEvaluator : IEvaluator { /// diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsTrackingEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsTrackingEvaluator.cs index 64d09d5e..cd1f6cf2 100644 --- a/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsTrackingEvaluator.cs +++ b/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsTrackingEvaluator.cs @@ -3,6 +3,7 @@ /// /// Evaluator to apply AsTracking to the query if the specification has AsTracking set to true. /// +[EvaluatorDiscovery(Order = -45)] public sealed class AsTrackingEvaluator : IEvaluator { /// diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/IgnoreAutoIncludesEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/IgnoreAutoIncludesEvaluator.cs index 3d528746..297c758d 100644 --- a/src/QuerySpecification.EntityFrameworkCore/Evaluators/IgnoreAutoIncludesEvaluator.cs +++ b/src/QuerySpecification.EntityFrameworkCore/Evaluators/IgnoreAutoIncludesEvaluator.cs @@ -3,6 +3,7 @@ /// /// Evaluator to apply IgnoreAutoIncludes to the query if the specification has IgnoreAutoIncludes set to true. /// +[EvaluatorDiscovery(Order = -70)] public sealed class IgnoreAutoIncludesEvaluator : IEvaluator { diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/IgnoreQueryFiltersEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/IgnoreQueryFiltersEvaluator.cs index 4abc6808..37a9f3a1 100644 --- a/src/QuerySpecification.EntityFrameworkCore/Evaluators/IgnoreQueryFiltersEvaluator.cs +++ b/src/QuerySpecification.EntityFrameworkCore/Evaluators/IgnoreQueryFiltersEvaluator.cs @@ -3,6 +3,7 @@ /// /// Evaluator to apply IgnoreQueryFilters to the query if the specification has IgnoreQueryFilters set to true. /// +[EvaluatorDiscovery(Order = -65)] public sealed class IgnoreQueryFiltersEvaluator : IEvaluator { diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/IncludeEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/IncludeEvaluator.cs index 5d8598c2..a3b46a3a 100644 --- a/src/QuerySpecification.EntityFrameworkCore/Evaluators/IncludeEvaluator.cs +++ b/src/QuerySpecification.EntityFrameworkCore/Evaluators/IncludeEvaluator.cs @@ -9,6 +9,7 @@ namespace Pozitron.QuerySpecification; /// /// Evaluates a specification to include navigation properties. /// +[EvaluatorDiscovery(Order = -85)] public sealed class IncludeEvaluator : IEvaluator { private static readonly MethodInfo _includeMethodInfo = typeof(EntityFrameworkQueryableExtensions) diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/IncludeStringEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/IncludeStringEvaluator.cs index ff7e4523..c7708c99 100644 --- a/src/QuerySpecification.EntityFrameworkCore/Evaluators/IncludeStringEvaluator.cs +++ b/src/QuerySpecification.EntityFrameworkCore/Evaluators/IncludeStringEvaluator.cs @@ -3,6 +3,7 @@ /// /// Evaluates a specification to include navigation properties specified by string paths. /// +[EvaluatorDiscovery(Order = -90)] public sealed class IncludeStringEvaluator : IEvaluator { diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/LikeEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/LikeEvaluator.cs index e2592c3b..62a291be 100644 --- a/src/QuerySpecification.EntityFrameworkCore/Evaluators/LikeEvaluator.cs +++ b/src/QuerySpecification.EntityFrameworkCore/Evaluators/LikeEvaluator.cs @@ -19,6 +19,7 @@ This was the previous implementation. We're trying to avoid allocations of LikeE /// /// Evaluates a specification to apply "like" expressions for filtering. /// +[EvaluatorDiscovery(Order = -95)] public sealed class LikeEvaluator : IEvaluator { private LikeEvaluator() { } diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/QueryTagEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/QueryTagEvaluator.cs index 5066f881..4956f481 100644 --- a/src/QuerySpecification.EntityFrameworkCore/Evaluators/QueryTagEvaluator.cs +++ b/src/QuerySpecification.EntityFrameworkCore/Evaluators/QueryTagEvaluator.cs @@ -3,6 +3,7 @@ /// /// Evaluator to apply the query tags to the query. /// +[EvaluatorDiscovery(Order = -75)] public sealed class QueryTagEvaluator : IEvaluator { diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs index aa3ab776..c11a3993 100644 --- a/src/QuerySpecification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs +++ b/src/QuerySpecification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs @@ -18,23 +18,14 @@ public class SpecificationEvaluator /// /// Initializes a new instance of the class. /// - public SpecificationEvaluator() + public SpecificationEvaluator(DiscoveryStrategy strategy = DiscoveryStrategy.All) { - Evaluators = - [ - WhereEvaluator.Instance, - LikeEvaluator.Instance, - IncludeStringEvaluator.Instance, - IncludeEvaluator.Instance, - OrderEvaluator.Instance, - IgnoreQueryFiltersEvaluator.Instance, - AsNoTrackingEvaluator.Instance, - AsNoTrackingWithIdentityResolutionEvaluator.Instance, - AsTrackingEvaluator.Instance, - AsSplitQueryEvaluator.Instance, - IgnoreAutoIncludesEvaluator.Instance, - QueryTagEvaluator.Instance, - ]; + Evaluators = strategy switch + { + DiscoveryStrategy.BuiltInOnly => EvaluatorProvider.GetBuiltInEvaluators(), + DiscoveryStrategy.All => EvaluatorProvider.GetAllEvaluators(), + _ => [] + }; } /// diff --git a/src/QuerySpecification/DiscoveryAttribute.cs b/src/QuerySpecification/DiscoveryAttribute.cs new file mode 100644 index 00000000..7b4a7aa7 --- /dev/null +++ b/src/QuerySpecification/DiscoveryAttribute.cs @@ -0,0 +1,26 @@ +namespace Pozitron.QuerySpecification; + +/// +/// Specifies discovery options for evaluators and validators, such as order and whether discovery is enabled. +/// +[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] +public class DiscoveryAttribute : Attribute +{ + /// + /// Gets the order in which the evaluator should be applied. Lower values are applied first. + /// + public int Order { get; set; } = int.MaxValue; + + /// + /// Gets a value indicating whether the evaluator is discoverable. + /// + public bool Enable { get; set; } = true; +} + +public sealed class EvaluatorDiscoveryAttribute : DiscoveryAttribute +{ +} + +public sealed class ValidatorDiscoveryAttribute : DiscoveryAttribute +{ +} diff --git a/src/QuerySpecification/DiscoveryStrategy.cs b/src/QuerySpecification/DiscoveryStrategy.cs new file mode 100644 index 00000000..8a39fb24 --- /dev/null +++ b/src/QuerySpecification/DiscoveryStrategy.cs @@ -0,0 +1,22 @@ +namespace Pozitron.QuerySpecification; + +/// +/// Specifies the strategy for discovering evaluators and validators. +/// +public enum DiscoveryStrategy +{ + /// + /// Discovery is disabled. + /// + Disable = 1, + + /// + /// Only built-in evaluators/validators from this library are discovered. + /// + BuiltInOnly = 2, + + /// + /// All evaluators from all loaded assemblies are discovered. + /// + All = 3 +} diff --git a/src/QuerySpecification/Evaluators/LikeMemoryEvaluator.cs b/src/QuerySpecification/Evaluators/LikeMemoryEvaluator.cs index 9859d272..a896b5eb 100644 --- a/src/QuerySpecification/Evaluators/LikeMemoryEvaluator.cs +++ b/src/QuerySpecification/Evaluators/LikeMemoryEvaluator.cs @@ -20,6 +20,7 @@ public IEnumerable Evaluate(IEnumerable source, Specification specif /// /// Represents an in-memory evaluator for "Like" expressions. /// +[EvaluatorDiscovery(Order = -95)] public sealed class LikeMemoryEvaluator : IInMemoryEvaluator { /// diff --git a/src/QuerySpecification/Evaluators/OrderEvaluator.cs b/src/QuerySpecification/Evaluators/OrderEvaluator.cs index 5351b0af..9bf9ba52 100644 --- a/src/QuerySpecification/Evaluators/OrderEvaluator.cs +++ b/src/QuerySpecification/Evaluators/OrderEvaluator.cs @@ -3,6 +3,7 @@ /// /// Represents an evaluator for order expressions. /// +[EvaluatorDiscovery(Order = -80)] public sealed class OrderEvaluator : IEvaluator, IInMemoryEvaluator { /// diff --git a/src/QuerySpecification/Evaluators/SpecificationInMemoryEvaluator.cs b/src/QuerySpecification/Evaluators/SpecificationInMemoryEvaluator.cs index 9c6b6175..6905fc6b 100644 --- a/src/QuerySpecification/Evaluators/SpecificationInMemoryEvaluator.cs +++ b/src/QuerySpecification/Evaluators/SpecificationInMemoryEvaluator.cs @@ -18,14 +18,14 @@ public class SpecificationInMemoryEvaluator /// /// Initializes a new instance of the class. /// - public SpecificationInMemoryEvaluator() + public SpecificationInMemoryEvaluator(DiscoveryStrategy strategy = DiscoveryStrategy.All) { - Evaluators = - [ - WhereEvaluator.Instance, - OrderEvaluator.Instance, - LikeMemoryEvaluator.Instance, - ]; + Evaluators = strategy switch + { + DiscoveryStrategy.BuiltInOnly => EvaluatorProvider.GetBuiltInMemoryEvaluators(), + DiscoveryStrategy.All => EvaluatorProvider.GetAllMemoryEvaluators(), + _ => [] + }; } /// diff --git a/src/QuerySpecification/Evaluators/WhereEvaluator.cs b/src/QuerySpecification/Evaluators/WhereEvaluator.cs index a1629496..822acdf3 100644 --- a/src/QuerySpecification/Evaluators/WhereEvaluator.cs +++ b/src/QuerySpecification/Evaluators/WhereEvaluator.cs @@ -3,6 +3,7 @@ /// /// Represents an evaluator for where expressions. /// +[EvaluatorDiscovery(Order = -100)] public sealed class WhereEvaluator : IEvaluator, IInMemoryEvaluator { /// diff --git a/src/QuerySpecification/Internals/TypeHelper.cs b/src/QuerySpecification/Internals/TypeHelper.cs new file mode 100644 index 00000000..c34ef91b --- /dev/null +++ b/src/QuerySpecification/Internals/TypeHelper.cs @@ -0,0 +1,59 @@ +using System.Reflection; + +namespace Pozitron.QuerySpecification; + +internal static class TypeHelper +{ + public static List GetInstancesOf(IEnumerable assemblies) + where TType : class + where TAttribute : DiscoveryAttribute + { + var evaluatorType = typeof(TType); + var evaluators = new List<(TType Instance, int Order, string TypeName)>(); + + var types = assemblies + .SelectMany(a => + { + try { return a.GetTypes(); } catch { return Array.Empty(); } + }) + .Where(t => t.IsClass && !t.IsAbstract && evaluatorType.IsAssignableFrom(t)) + .Distinct(); + + foreach (var type in types) + { + var discoveryAttr = type.GetCustomAttribute(); + if (discoveryAttr is not null && !discoveryAttr.Enable) + continue; + + TType? instance = null; + + if (type.GetConstructor(Type.EmptyTypes) is not null) + { + instance = (TType?)Activator.CreateInstance(type); + } + else if (type.GetFields(BindingFlags.Public | BindingFlags.Static) + .Where(f => evaluatorType.IsAssignableFrom(f.FieldType)) + .FirstOrDefault() is FieldInfo instanceField) + { + instance = (TType?)instanceField.GetValue(null); + } + else if (type.GetProperties(BindingFlags.Public | BindingFlags.Static) + .Where(f => evaluatorType.IsAssignableFrom(f.PropertyType)) + .FirstOrDefault() is PropertyInfo instanceProp) + { + instance = (TType?)instanceProp.GetValue(null); + } + + if (instance is null) continue; + + int order = discoveryAttr?.Order ?? int.MaxValue; + evaluators.Add((instance, order, type.FullName ?? type.Name)); + } + + return evaluators + .OrderBy(e => e.Order) + .ThenBy(e => e.TypeName) + .Select(e => e.Instance) + .ToList(); + } +} diff --git a/src/QuerySpecification/Internals/TypeProvider.cs b/src/QuerySpecification/Internals/TypeProvider.cs new file mode 100644 index 00000000..e0f65078 --- /dev/null +++ b/src/QuerySpecification/Internals/TypeProvider.cs @@ -0,0 +1,45 @@ +namespace Pozitron.QuerySpecification; + +internal class EvaluatorProvider +{ + public static List GetAllMemoryEvaluators() => _allMemoryEvaluators.Value.ToList(); + public static List GetBuiltInMemoryEvaluators() => _builtInMemoryEvaluators.Value.ToList(); + + public static List GetAllEvaluators() => _allEvaluators.Value.ToList(); + public static List GetBuiltInEvaluators() => _builtInEvaluators.Value.ToList(); + + + private static readonly Lazy> _allEvaluators = new( + () => TypeHelper.GetInstancesOf(AppDomain.CurrentDomain.GetAssemblies()), + LazyThreadSafetyMode.ExecutionAndPublication); + + private static readonly Lazy> _builtInEvaluators = new( + () => TypeHelper.GetInstancesOf + (AppDomain.CurrentDomain.GetAssemblies().Where(x => x.FullName != null && x.FullName.StartsWith("Pozitron.QuerySpecification"))), + LazyThreadSafetyMode.ExecutionAndPublication); + + private static readonly Lazy> _allMemoryEvaluators = new( + () => TypeHelper.GetInstancesOf(AppDomain.CurrentDomain.GetAssemblies()), + LazyThreadSafetyMode.ExecutionAndPublication); + + private static readonly Lazy> _builtInMemoryEvaluators = new( + () => TypeHelper.GetInstancesOf + (AppDomain.CurrentDomain.GetAssemblies().Where(x => x.FullName != null && x.FullName.StartsWith("Pozitron.QuerySpecification"))), + LazyThreadSafetyMode.ExecutionAndPublication); +} + +internal class ValidatorProvider +{ + public static List GetAllValidators() => _allValidators.Value.ToList(); + public static List GetBuiltInValidators() => _builtInValidators.Value.ToList(); + + + private static readonly Lazy> _allValidators = new( + () => TypeHelper.GetInstancesOf(AppDomain.CurrentDomain.GetAssemblies()), + LazyThreadSafetyMode.ExecutionAndPublication); + + private static readonly Lazy> _builtInValidators = new( + () => TypeHelper.GetInstancesOf + (AppDomain.CurrentDomain.GetAssemblies().Where(x => x.FullName != null && x.FullName.StartsWith("Pozitron.QuerySpecification"))), + LazyThreadSafetyMode.ExecutionAndPublication); +} diff --git a/src/QuerySpecification/Validators/LikeValidator.cs b/src/QuerySpecification/Validators/LikeValidator.cs index c86cabaf..649f54cb 100644 --- a/src/QuerySpecification/Validators/LikeValidator.cs +++ b/src/QuerySpecification/Validators/LikeValidator.cs @@ -18,6 +18,7 @@ public bool IsValid(T entity, Specification specification) /// /// Represents a validator for "like" expressions. /// +[ValidatorDiscovery(Order = -95)] public sealed class LikeValidator : IValidator { /// diff --git a/src/QuerySpecification/Validators/SpecificationValidator.cs b/src/QuerySpecification/Validators/SpecificationValidator.cs index e0900520..0f9c1e89 100644 --- a/src/QuerySpecification/Validators/SpecificationValidator.cs +++ b/src/QuerySpecification/Validators/SpecificationValidator.cs @@ -18,13 +18,14 @@ public class SpecificationValidator /// /// Initializes a new instance of the class. /// - public SpecificationValidator() + public SpecificationValidator(DiscoveryStrategy strategy = DiscoveryStrategy.All) { - Validators = - [ - WhereValidator.Instance, - LikeValidator.Instance - ]; + Validators = strategy switch + { + DiscoveryStrategy.BuiltInOnly => ValidatorProvider.GetBuiltInValidators(), + DiscoveryStrategy.All => ValidatorProvider.GetAllValidators(), + _ => [] + }; } /// diff --git a/src/QuerySpecification/Validators/WhereValidator.cs b/src/QuerySpecification/Validators/WhereValidator.cs index 9c760415..64c31884 100644 --- a/src/QuerySpecification/Validators/WhereValidator.cs +++ b/src/QuerySpecification/Validators/WhereValidator.cs @@ -3,6 +3,7 @@ /// /// Represents a validator for where expressions. /// +[ValidatorDiscovery(Order = -100)] public sealed class WhereValidator : IValidator { /// diff --git a/tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/SpecificationEvaluatorTests.cs b/tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/SpecificationEvaluatorTests.cs index 7cd7101b..98a22ec4 100644 --- a/tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/SpecificationEvaluatorTests.cs +++ b/tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/SpecificationEvaluatorTests.cs @@ -357,10 +357,10 @@ public void GivenSpecWithMultipleFlags() .ToString(); var expected = DbContext.Countries + .IgnoreAutoIncludes() .IgnoreQueryFilters() - .AsNoTracking() .AsSplitQuery() - .IgnoreAutoIncludes() + .AsNoTracking() .Expression .ToString(); @@ -397,13 +397,13 @@ public void DerivedSpecificationEvaluatorCanAlterDefaultEvaluator() result[3].Should().BeOfType(); result[4].Should().BeOfType(); result[5].Should().BeOfType(); - result[6].Should().BeOfType(); - result[7].Should().BeOfType(); - result[8].Should().BeOfType(); - result[9].Should().BeOfType(); - result[10].Should().BeOfType(); - result[11].Should().BeOfType(); - result[12].Should().BeOfType(); + result[6].Should().BeOfType(); + result[7].Should().BeOfType(); + result[8].Should().BeOfType(); + result[9].Should().BeOfType(); + result[10].Should().BeOfType(); + result[11].Should().BeOfType(); + result[12].Should().BeOfType(); result[13].Should().BeOfType(); } diff --git a/tests/QuerySpecification.Tests/Evaluators/SpecificationInMemoryEvaluatorTests.cs b/tests/QuerySpecification.Tests/Evaluators/SpecificationInMemoryEvaluatorTests.cs index 4accc1b2..5bbfab6e 100644 --- a/tests/QuerySpecification.Tests/Evaluators/SpecificationInMemoryEvaluatorTests.cs +++ b/tests/QuerySpecification.Tests/Evaluators/SpecificationInMemoryEvaluatorTests.cs @@ -264,8 +264,8 @@ public void DerivedSpecificationEvaluatorCanAlterDefaultEvaluator() result.Should().HaveCount(5); result[0].Should().BeOfType(); result[1].Should().BeOfType(); - result[2].Should().BeOfType(); - result[3].Should().BeOfType(); + result[2].Should().BeOfType(); + result[3].Should().BeOfType(); result[4].Should().BeOfType(); } From f911283340a3d3b6405b4baf3e0dc77124c55f30 Mon Sep 17 00:00:00 2001 From: Fati Iseni Date: Thu, 5 Jun 2025 12:33:34 +0200 Subject: [PATCH 02/11] Rename InMemory to Memory for partial and specification evaluators. --- ...IInMemoryEvaluator.cs => IMemoryEvaluator.cs} | 2 +- .../Evaluators/LikeMemoryEvaluator.cs | 4 ++-- .../Evaluators/OrderEvaluator.cs | 2 +- ...luator.cs => SpecificationMemoryEvaluator.cs} | 16 ++++++++-------- .../Evaluators/WhereEvaluator.cs | 2 +- src/QuerySpecification/Internals/TypeProvider.cs | 12 ++++++------ src/QuerySpecification/Specification.cs | 2 +- ...ator.cs => Benchmark7_LikeMemoryEvaluator.cs} | 2 +- ...ator.cs => Benchmark8_LikeMemoryValidator.cs} | 2 +- ...s.cs => SpecificationMemoryEvaluatorTests.cs} | 12 ++++++------ 10 files changed, 28 insertions(+), 28 deletions(-) rename src/QuerySpecification/Evaluators/{IInMemoryEvaluator.cs => IMemoryEvaluator.cs} (94%) rename src/QuerySpecification/Evaluators/{SpecificationInMemoryEvaluator.cs => SpecificationMemoryEvaluator.cs} (87%) rename tests/QuerySpecification.Benchmarks/Benchmarks/{Benchmark7_LikeInMemoryEvaluator.cs => Benchmark7_LikeMemoryEvaluator.cs} (98%) rename tests/QuerySpecification.Benchmarks/Benchmarks/{Benchmark8_LikeInMemoryValidator.cs => Benchmark8_LikeMemoryValidator.cs} (98%) rename tests/QuerySpecification.Tests/Evaluators/{SpecificationInMemoryEvaluatorTests.cs => SpecificationMemoryEvaluatorTests.cs} (94%) diff --git a/src/QuerySpecification/Evaluators/IInMemoryEvaluator.cs b/src/QuerySpecification/Evaluators/IMemoryEvaluator.cs similarity index 94% rename from src/QuerySpecification/Evaluators/IInMemoryEvaluator.cs rename to src/QuerySpecification/Evaluators/IMemoryEvaluator.cs index a481ee72..5ee7f026 100644 --- a/src/QuerySpecification/Evaluators/IInMemoryEvaluator.cs +++ b/src/QuerySpecification/Evaluators/IMemoryEvaluator.cs @@ -3,7 +3,7 @@ /// /// Represents an in-memory evaluator that processes a specification. /// -public interface IInMemoryEvaluator +public interface IMemoryEvaluator { /// /// Evaluates the given specification on the provided enumerable source. diff --git a/src/QuerySpecification/Evaluators/LikeMemoryEvaluator.cs b/src/QuerySpecification/Evaluators/LikeMemoryEvaluator.cs index a896b5eb..4c461acc 100644 --- a/src/QuerySpecification/Evaluators/LikeMemoryEvaluator.cs +++ b/src/QuerySpecification/Evaluators/LikeMemoryEvaluator.cs @@ -14,14 +14,14 @@ public IEnumerable Evaluate(IEnumerable source, Specification specif This was the previous implementation. We're trying to avoid allocations of LikeExpressions, GroupBy and LINQ. The new implementation preserves the behavior and reduces allocations drastically. We've implemented a custom iterator. Also, instead of GroupBy, we have a single array sorted by group, and we slice it to get the groups. - For source of 1000 items, the allocations are reduced from 257.872 bytes to only 64 bytes (the cost of the iterator instance). Refer to LikeInMemoryEvaluatorBenchmark results. + For source of 1000 items, the allocations are reduced from 257.872 bytes to only 64 bytes (the cost of the iterator instance). Refer to LikeMemoryEvaluatorBenchmark results. */ /// /// Represents an in-memory evaluator for "Like" expressions. /// [EvaluatorDiscovery(Order = -95)] -public sealed class LikeMemoryEvaluator : IInMemoryEvaluator +public sealed class LikeMemoryEvaluator : IMemoryEvaluator { /// /// Gets the singleton instance of the class. diff --git a/src/QuerySpecification/Evaluators/OrderEvaluator.cs b/src/QuerySpecification/Evaluators/OrderEvaluator.cs index 9bf9ba52..451e6514 100644 --- a/src/QuerySpecification/Evaluators/OrderEvaluator.cs +++ b/src/QuerySpecification/Evaluators/OrderEvaluator.cs @@ -4,7 +4,7 @@ /// Represents an evaluator for order expressions. /// [EvaluatorDiscovery(Order = -80)] -public sealed class OrderEvaluator : IEvaluator, IInMemoryEvaluator +public sealed class OrderEvaluator : IEvaluator, IMemoryEvaluator { /// /// Gets the singleton instance of the class. diff --git a/src/QuerySpecification/Evaluators/SpecificationInMemoryEvaluator.cs b/src/QuerySpecification/Evaluators/SpecificationMemoryEvaluator.cs similarity index 87% rename from src/QuerySpecification/Evaluators/SpecificationInMemoryEvaluator.cs rename to src/QuerySpecification/Evaluators/SpecificationMemoryEvaluator.cs index 6905fc6b..be91bb68 100644 --- a/src/QuerySpecification/Evaluators/SpecificationInMemoryEvaluator.cs +++ b/src/QuerySpecification/Evaluators/SpecificationMemoryEvaluator.cs @@ -3,22 +3,22 @@ /// /// Evaluates specifications in memory. /// -public class SpecificationInMemoryEvaluator +public class SpecificationMemoryEvaluator { /// - /// Gets the default instance of the class. + /// Gets the default instance of the class. /// - public static SpecificationInMemoryEvaluator Default = new(); + public static SpecificationMemoryEvaluator Default = new(); /// /// Gets the list of in-memory evaluators. /// - protected List Evaluators { get; } + protected List Evaluators { get; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public SpecificationInMemoryEvaluator(DiscoveryStrategy strategy = DiscoveryStrategy.All) + public SpecificationMemoryEvaluator(DiscoveryStrategy strategy = DiscoveryStrategy.All) { Evaluators = strategy switch { @@ -29,10 +29,10 @@ public SpecificationInMemoryEvaluator(DiscoveryStrategy strategy = DiscoveryStra } /// - /// Initializes a new instance of the class with the specified evaluators. + /// Initializes a new instance of the class with the specified evaluators. /// /// The in-memory evaluators to use. - public SpecificationInMemoryEvaluator(IEnumerable evaluators) + public SpecificationMemoryEvaluator(IEnumerable evaluators) { Evaluators = evaluators.ToList(); } diff --git a/src/QuerySpecification/Evaluators/WhereEvaluator.cs b/src/QuerySpecification/Evaluators/WhereEvaluator.cs index 822acdf3..3add7be1 100644 --- a/src/QuerySpecification/Evaluators/WhereEvaluator.cs +++ b/src/QuerySpecification/Evaluators/WhereEvaluator.cs @@ -4,7 +4,7 @@ /// Represents an evaluator for where expressions. /// [EvaluatorDiscovery(Order = -100)] -public sealed class WhereEvaluator : IEvaluator, IInMemoryEvaluator +public sealed class WhereEvaluator : IEvaluator, IMemoryEvaluator { /// /// Gets the singleton instance of the class. diff --git a/src/QuerySpecification/Internals/TypeProvider.cs b/src/QuerySpecification/Internals/TypeProvider.cs index e0f65078..144fcc38 100644 --- a/src/QuerySpecification/Internals/TypeProvider.cs +++ b/src/QuerySpecification/Internals/TypeProvider.cs @@ -2,8 +2,8 @@ internal class EvaluatorProvider { - public static List GetAllMemoryEvaluators() => _allMemoryEvaluators.Value.ToList(); - public static List GetBuiltInMemoryEvaluators() => _builtInMemoryEvaluators.Value.ToList(); + public static List GetAllMemoryEvaluators() => _allMemoryEvaluators.Value.ToList(); + public static List GetBuiltInMemoryEvaluators() => _builtInMemoryEvaluators.Value.ToList(); public static List GetAllEvaluators() => _allEvaluators.Value.ToList(); public static List GetBuiltInEvaluators() => _builtInEvaluators.Value.ToList(); @@ -18,12 +18,12 @@ internal class EvaluatorProvider (AppDomain.CurrentDomain.GetAssemblies().Where(x => x.FullName != null && x.FullName.StartsWith("Pozitron.QuerySpecification"))), LazyThreadSafetyMode.ExecutionAndPublication); - private static readonly Lazy> _allMemoryEvaluators = new( - () => TypeHelper.GetInstancesOf(AppDomain.CurrentDomain.GetAssemblies()), + private static readonly Lazy> _allMemoryEvaluators = new( + () => TypeHelper.GetInstancesOf(AppDomain.CurrentDomain.GetAssemblies()), LazyThreadSafetyMode.ExecutionAndPublication); - private static readonly Lazy> _builtInMemoryEvaluators = new( - () => TypeHelper.GetInstancesOf + private static readonly Lazy> _builtInMemoryEvaluators = new( + () => TypeHelper.GetInstancesOf (AppDomain.CurrentDomain.GetAssemblies().Where(x => x.FullName != null && x.FullName.StartsWith("Pozitron.QuerySpecification"))), LazyThreadSafetyMode.ExecutionAndPublication); } diff --git a/src/QuerySpecification/Specification.cs b/src/QuerySpecification/Specification.cs index de0791fd..988f9058 100644 --- a/src/QuerySpecification/Specification.cs +++ b/src/QuerySpecification/Specification.cs @@ -102,7 +102,7 @@ public virtual bool IsSatisfiedBy(T entity) /// /// Gets the evaluator. /// - protected virtual SpecificationInMemoryEvaluator Evaluator => SpecificationInMemoryEvaluator.Default; + protected virtual SpecificationMemoryEvaluator Evaluator => SpecificationMemoryEvaluator.Default; /// /// Gets the validator. diff --git a/tests/QuerySpecification.Benchmarks/Benchmarks/Benchmark7_LikeInMemoryEvaluator.cs b/tests/QuerySpecification.Benchmarks/Benchmarks/Benchmark7_LikeMemoryEvaluator.cs similarity index 98% rename from tests/QuerySpecification.Benchmarks/Benchmarks/Benchmark7_LikeInMemoryEvaluator.cs rename to tests/QuerySpecification.Benchmarks/Benchmarks/Benchmark7_LikeMemoryEvaluator.cs index aa6774b7..5dcceccf 100644 --- a/tests/QuerySpecification.Benchmarks/Benchmarks/Benchmark7_LikeInMemoryEvaluator.cs +++ b/tests/QuerySpecification.Benchmarks/Benchmarks/Benchmark7_LikeMemoryEvaluator.cs @@ -1,7 +1,7 @@ namespace QuerySpecification.Benchmarks; [MemoryDiagnoser] -public class Benchmark7_LikeInMemoryEvaluator +public class Benchmark7_LikeMemoryEvaluator { private List _source = default!; private CustomerSpec _specification = default!; diff --git a/tests/QuerySpecification.Benchmarks/Benchmarks/Benchmark8_LikeInMemoryValidator.cs b/tests/QuerySpecification.Benchmarks/Benchmarks/Benchmark8_LikeMemoryValidator.cs similarity index 98% rename from tests/QuerySpecification.Benchmarks/Benchmarks/Benchmark8_LikeInMemoryValidator.cs rename to tests/QuerySpecification.Benchmarks/Benchmarks/Benchmark8_LikeMemoryValidator.cs index 211ee5c6..b1cc8fcb 100644 --- a/tests/QuerySpecification.Benchmarks/Benchmarks/Benchmark8_LikeInMemoryValidator.cs +++ b/tests/QuerySpecification.Benchmarks/Benchmarks/Benchmark8_LikeMemoryValidator.cs @@ -1,7 +1,7 @@ namespace QuerySpecification.Benchmarks; [MemoryDiagnoser] -public class Benchmark8_LikeInMemoryValidator +public class Benchmark8_LikeMemoryValidator { private List _source = default!; private CustomerSpec _specification = default!; diff --git a/tests/QuerySpecification.Tests/Evaluators/SpecificationInMemoryEvaluatorTests.cs b/tests/QuerySpecification.Tests/Evaluators/SpecificationMemoryEvaluatorTests.cs similarity index 94% rename from tests/QuerySpecification.Tests/Evaluators/SpecificationInMemoryEvaluatorTests.cs rename to tests/QuerySpecification.Tests/Evaluators/SpecificationMemoryEvaluatorTests.cs index 5bbfab6e..a1e0d1bd 100644 --- a/tests/QuerySpecification.Tests/Evaluators/SpecificationInMemoryEvaluatorTests.cs +++ b/tests/QuerySpecification.Tests/Evaluators/SpecificationMemoryEvaluatorTests.cs @@ -2,9 +2,9 @@ namespace Tests.Evaluators; -public class SpecificationInMemoryEvaluatorTests +public class SpecificationMemoryEvaluatorTests { - private static readonly SpecificationInMemoryEvaluator _evaluator = SpecificationInMemoryEvaluator.Default; + private static readonly SpecificationMemoryEvaluator _evaluator = SpecificationMemoryEvaluator.Default; public record Customer(int Id, string FirstName, string LastName); public record CustomerWithMails(int Id, string FirstName, string LastName, List Emails); @@ -241,14 +241,14 @@ public void Evaluate_DoesNotFilter_GivenSpecWithSelectManyAndIgnorePagination() [Fact] public void Constructor_SetsProvidedEvaluators() { - var evaluators = new List + var evaluators = new List { WhereEvaluator.Instance, OrderEvaluator.Instance, WhereEvaluator.Instance, }; - var evaluator = new SpecificationInMemoryEvaluator(evaluators); + var evaluator = new SpecificationMemoryEvaluator(evaluators); var result = EvaluatorsOf(evaluator); result.Should().HaveSameCount(evaluators); @@ -269,7 +269,7 @@ public void DerivedSpecificationEvaluatorCanAlterDefaultEvaluator() result[4].Should().BeOfType(); } - private class SpecificationEvaluatorDerived : SpecificationInMemoryEvaluator + private class SpecificationEvaluatorDerived : SpecificationMemoryEvaluator { public SpecificationEvaluatorDerived() { @@ -279,5 +279,5 @@ public SpecificationEvaluatorDerived() } [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "k__BackingField")] - public static extern ref List EvaluatorsOf(SpecificationInMemoryEvaluator @this); + public static extern ref List EvaluatorsOf(SpecificationMemoryEvaluator @this); } From 74c98e9fc596a4f7e7b14b52e5b011c99579e1b2 Mon Sep 17 00:00:00 2001 From: Fati Iseni Date: Thu, 5 Jun 2025 12:35:44 +0200 Subject: [PATCH 03/11] Added XMl comments. --- src/Directory.Build.props | 5 +++++ src/QuerySpecification/DiscoveryAttribute.cs | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index deb7ab54..278f6630 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -37,6 +37,11 @@ pozitronicon.png + + + + + diff --git a/src/QuerySpecification/DiscoveryAttribute.cs b/src/QuerySpecification/DiscoveryAttribute.cs index 7b4a7aa7..6bb041ad 100644 --- a/src/QuerySpecification/DiscoveryAttribute.cs +++ b/src/QuerySpecification/DiscoveryAttribute.cs @@ -17,10 +17,16 @@ public class DiscoveryAttribute : Attribute public bool Enable { get; set; } = true; } +/// +/// Specifies discovery options for evaluators, such as order and whether discovery is enabled. +/// public sealed class EvaluatorDiscoveryAttribute : DiscoveryAttribute { } +/// +/// Specifies discovery options for validators, such as order and whether discovery is enabled. +/// public sealed class ValidatorDiscoveryAttribute : DiscoveryAttribute { } From 123fd14df424d72435229f20ddc640d661d22f7c Mon Sep 17 00:00:00 2001 From: Fati Iseni Date: Thu, 5 Jun 2025 14:54:33 +0200 Subject: [PATCH 04/11] Improve type scanning. --- exclusion.dic | 1 + .../Internals/TypeHelper.cs | 39 +++++++++++++++---- .../Internals/TypeProvider.cs | 16 +++----- 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/exclusion.dic b/exclusion.dic index 358812ae..39271a7c 100644 --- a/exclusion.dic +++ b/exclusion.dic @@ -23,3 +23,4 @@ aaaab aaaaab axza Compilable +netstandard diff --git a/src/QuerySpecification/Internals/TypeHelper.cs b/src/QuerySpecification/Internals/TypeHelper.cs index c34ef91b..8c84b056 100644 --- a/src/QuerySpecification/Internals/TypeHelper.cs +++ b/src/QuerySpecification/Internals/TypeHelper.cs @@ -4,19 +4,42 @@ namespace Pozitron.QuerySpecification; internal static class TypeHelper { - public static List GetInstancesOf(IEnumerable assemblies) + internal static readonly Lazy _loadedAssemblies = new( + () => AppDomain.CurrentDomain.GetAssemblies() + .Where(a => + a.FullName != null && + !a.FullName.StartsWith("System", StringComparison.OrdinalIgnoreCase) && + !a.FullName.StartsWith("Microsoft", StringComparison.OrdinalIgnoreCase) && + !a.FullName.StartsWith("netstandard", StringComparison.OrdinalIgnoreCase) && + !a.FullName.StartsWith("Windows", StringComparison.OrdinalIgnoreCase) + ) + .ToArray(), + LazyThreadSafetyMode.ExecutionAndPublication); + + internal static readonly Lazy _specificationAssemblies = new( + () => _loadedAssemblies.Value + .Where(a => a.FullName != null && a.FullName.StartsWith("Pozitron.QuerySpecification", StringComparison.OrdinalIgnoreCase)) + .ToArray(), + LazyThreadSafetyMode.ExecutionAndPublication); + + internal static List GetInstancesOf(bool scanOnlySpecificationAssemblies) + where TType : class + where TAttribute : DiscoveryAttribute + => GetInstancesOf(scanOnlySpecificationAssemblies ? _specificationAssemblies.Value : _loadedAssemblies.Value); + + internal static List GetInstancesOf(IEnumerable assemblies) where TType : class where TAttribute : DiscoveryAttribute { - var evaluatorType = typeof(TType); - var evaluators = new List<(TType Instance, int Order, string TypeName)>(); + var baseType = typeof(TType); + var typeInstances = new List<(TType Instance, int Order, string TypeName)>(); var types = assemblies .SelectMany(a => { try { return a.GetTypes(); } catch { return Array.Empty(); } }) - .Where(t => t.IsClass && !t.IsAbstract && evaluatorType.IsAssignableFrom(t)) + .Where(t => t.IsClass && !t.IsAbstract && !t.ContainsGenericParameters && baseType.IsAssignableFrom(t)) .Distinct(); foreach (var type in types) @@ -32,13 +55,13 @@ public static List GetInstancesOf(IEnumerable evaluatorType.IsAssignableFrom(f.FieldType)) + .Where(f => type.IsAssignableFrom(f.FieldType)) .FirstOrDefault() is FieldInfo instanceField) { instance = (TType?)instanceField.GetValue(null); } else if (type.GetProperties(BindingFlags.Public | BindingFlags.Static) - .Where(f => evaluatorType.IsAssignableFrom(f.PropertyType)) + .Where(f => type.IsAssignableFrom(f.PropertyType)) .FirstOrDefault() is PropertyInfo instanceProp) { instance = (TType?)instanceProp.GetValue(null); @@ -47,10 +70,10 @@ public static List GetInstancesOf(IEnumerable e.Order) .ThenBy(e => e.TypeName) .Select(e => e.Instance) diff --git a/src/QuerySpecification/Internals/TypeProvider.cs b/src/QuerySpecification/Internals/TypeProvider.cs index 144fcc38..551d02ad 100644 --- a/src/QuerySpecification/Internals/TypeProvider.cs +++ b/src/QuerySpecification/Internals/TypeProvider.cs @@ -8,23 +8,20 @@ internal class EvaluatorProvider public static List GetAllEvaluators() => _allEvaluators.Value.ToList(); public static List GetBuiltInEvaluators() => _builtInEvaluators.Value.ToList(); - private static readonly Lazy> _allEvaluators = new( - () => TypeHelper.GetInstancesOf(AppDomain.CurrentDomain.GetAssemblies()), + () => TypeHelper.GetInstancesOf(scanOnlySpecificationAssemblies: false), LazyThreadSafetyMode.ExecutionAndPublication); private static readonly Lazy> _builtInEvaluators = new( - () => TypeHelper.GetInstancesOf - (AppDomain.CurrentDomain.GetAssemblies().Where(x => x.FullName != null && x.FullName.StartsWith("Pozitron.QuerySpecification"))), + () => TypeHelper.GetInstancesOf(scanOnlySpecificationAssemblies: true), LazyThreadSafetyMode.ExecutionAndPublication); private static readonly Lazy> _allMemoryEvaluators = new( - () => TypeHelper.GetInstancesOf(AppDomain.CurrentDomain.GetAssemblies()), + () => TypeHelper.GetInstancesOf(scanOnlySpecificationAssemblies: false), LazyThreadSafetyMode.ExecutionAndPublication); private static readonly Lazy> _builtInMemoryEvaluators = new( - () => TypeHelper.GetInstancesOf - (AppDomain.CurrentDomain.GetAssemblies().Where(x => x.FullName != null && x.FullName.StartsWith("Pozitron.QuerySpecification"))), + () => TypeHelper.GetInstancesOf(scanOnlySpecificationAssemblies: true), LazyThreadSafetyMode.ExecutionAndPublication); } @@ -35,11 +32,10 @@ internal class ValidatorProvider private static readonly Lazy> _allValidators = new( - () => TypeHelper.GetInstancesOf(AppDomain.CurrentDomain.GetAssemblies()), + () => TypeHelper.GetInstancesOf(scanOnlySpecificationAssemblies: false), LazyThreadSafetyMode.ExecutionAndPublication); private static readonly Lazy> _builtInValidators = new( - () => TypeHelper.GetInstancesOf - (AppDomain.CurrentDomain.GetAssemblies().Where(x => x.FullName != null && x.FullName.StartsWith("Pozitron.QuerySpecification"))), + () => TypeHelper.GetInstancesOf(scanOnlySpecificationAssemblies: true), LazyThreadSafetyMode.ExecutionAndPublication); } From 8b549cf64872810a08a642fc2755aa7abcf5811c Mon Sep 17 00:00:00 2001 From: Fati Iseni Date: Thu, 5 Jun 2025 20:36:05 +0200 Subject: [PATCH 05/11] Added tests. --- .../Internals/TypeHelper.cs | 2 +- .../Evaluators/SpecificationEvaluatorTests.cs | 54 +++++--- .../SpecificationMemoryEvaluatorTests.cs | 36 ++++-- .../Internals/TypeHelperTests.cs | 121 ++++++++++++++++++ .../Internals/TypeProviderTests.cs | 70 ++++++++++ .../Validators/SpecificationValidatorTests.cs | 32 ++++- 6 files changed, 283 insertions(+), 32 deletions(-) create mode 100644 tests/QuerySpecification.Tests/Internals/TypeHelperTests.cs create mode 100644 tests/QuerySpecification.Tests/Internals/TypeProviderTests.cs diff --git a/src/QuerySpecification/Internals/TypeHelper.cs b/src/QuerySpecification/Internals/TypeHelper.cs index 8c84b056..c0d42fd0 100644 --- a/src/QuerySpecification/Internals/TypeHelper.cs +++ b/src/QuerySpecification/Internals/TypeHelper.cs @@ -18,7 +18,7 @@ internal static class TypeHelper internal static readonly Lazy _specificationAssemblies = new( () => _loadedAssemblies.Value - .Where(a => a.FullName != null && a.FullName.StartsWith("Pozitron.QuerySpecification", StringComparison.OrdinalIgnoreCase)) + .Where(a => a.FullName!.StartsWith("Pozitron.QuerySpecification", StringComparison.OrdinalIgnoreCase)) .ToArray(), LazyThreadSafetyMode.ExecutionAndPublication); diff --git a/tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/SpecificationEvaluatorTests.cs b/tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/SpecificationEvaluatorTests.cs index 98a22ec4..fa3d5104 100644 --- a/tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/SpecificationEvaluatorTests.cs +++ b/tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/SpecificationEvaluatorTests.cs @@ -385,37 +385,57 @@ public void Constructor_SetsProvidedEvaluators() } [Fact] - public void DerivedSpecificationEvaluatorCanAlterDefaultEvaluator() + public void DerivedSpecificationEvaluatorCanAlterDefaultEvaluators() { + var expectedEvaluators = new List + { + LikeEvaluator.Instance, + WhereEvaluator.Instance, + LikeEvaluator.Instance, + IncludeStringEvaluator.Instance, + IncludeEvaluator.Instance, + OrderEvaluator.Instance, + QueryTagEvaluator.Instance, + IgnoreAutoIncludesEvaluator.Instance, + IgnoreQueryFiltersEvaluator.Instance, + AsSplitQueryEvaluator.Instance, + AsNoTrackingEvaluator.Instance, + AsNoTrackingWithIdentityResolutionEvaluator.Instance, + AsTrackingEvaluator.Instance, + WhereEvaluator.Instance + }; + var evaluator = new SpecificationEvaluatorDerived(); var result = EvaluatorsOf(evaluator); - result.Should().HaveCount(14); - result[0].Should().BeOfType(); - result[1].Should().BeOfType(); - result[2].Should().BeOfType(); - result[3].Should().BeOfType(); - result[4].Should().BeOfType(); - result[5].Should().BeOfType(); - result[6].Should().BeOfType(); - result[7].Should().BeOfType(); - result[8].Should().BeOfType(); - result[9].Should().BeOfType(); - result[10].Should().BeOfType(); - result[11].Should().BeOfType(); - result[12].Should().BeOfType(); - result[13].Should().BeOfType(); + result.Should().Equal(expectedEvaluators); + } + + [Fact] + public void DerivedSpecificationEvaluatorCanDisableDiscovery() + { + var evaluator = new SpecificationEvaluatorWithDisabledDiscovery(); + + var result = EvaluatorsOf(evaluator); + result.Should().BeEmpty(); } private class SpecificationEvaluatorDerived : SpecificationEvaluator { - public SpecificationEvaluatorDerived() + public SpecificationEvaluatorDerived() : base(DiscoveryStrategy.BuiltInOnly) { Evaluators.Add(WhereEvaluator.Instance); Evaluators.Insert(0, LikeEvaluator.Instance); } } + private class SpecificationEvaluatorWithDisabledDiscovery : SpecificationEvaluator + { + public SpecificationEvaluatorWithDisabledDiscovery() : base(DiscoveryStrategy.Disable) + { + } + } + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "k__BackingField")] public static extern ref List EvaluatorsOf(SpecificationEvaluator @this); } diff --git a/tests/QuerySpecification.Tests/Evaluators/SpecificationMemoryEvaluatorTests.cs b/tests/QuerySpecification.Tests/Evaluators/SpecificationMemoryEvaluatorTests.cs index a1e0d1bd..65afc2cc 100644 --- a/tests/QuerySpecification.Tests/Evaluators/SpecificationMemoryEvaluatorTests.cs +++ b/tests/QuerySpecification.Tests/Evaluators/SpecificationMemoryEvaluatorTests.cs @@ -256,28 +256,48 @@ public void Constructor_SetsProvidedEvaluators() } [Fact] - public void DerivedSpecificationEvaluatorCanAlterDefaultEvaluator() + public void DerivedSpecificationEvaluatorCanAlterDefaultEvaluators() { + var expectedEvaluators = new List + { + LikeMemoryEvaluator.Instance, + WhereEvaluator.Instance, + LikeMemoryEvaluator.Instance, + OrderEvaluator.Instance, + WhereEvaluator.Instance + }; + var evaluator = new SpecificationEvaluatorDerived(); var result = EvaluatorsOf(evaluator); - result.Should().HaveCount(5); - result[0].Should().BeOfType(); - result[1].Should().BeOfType(); - result[2].Should().BeOfType(); - result[3].Should().BeOfType(); - result[4].Should().BeOfType(); + result.Should().Equal(expectedEvaluators); + } + + [Fact] + public void DerivedSpecificationEvaluatorCanDisableDiscovery() + { + var evaluator = new SpecificationEvaluatorWithDisabledDiscovery(); + + var result = EvaluatorsOf(evaluator); + result.Should().BeEmpty(); } private class SpecificationEvaluatorDerived : SpecificationMemoryEvaluator { - public SpecificationEvaluatorDerived() + public SpecificationEvaluatorDerived() : base(DiscoveryStrategy.BuiltInOnly) { Evaluators.Add(WhereEvaluator.Instance); Evaluators.Insert(0, LikeMemoryEvaluator.Instance); } } + private class SpecificationEvaluatorWithDisabledDiscovery : SpecificationMemoryEvaluator + { + public SpecificationEvaluatorWithDisabledDiscovery() : base(DiscoveryStrategy.Disable) + { + } + } + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "k__BackingField")] public static extern ref List EvaluatorsOf(SpecificationMemoryEvaluator @this); } diff --git a/tests/QuerySpecification.Tests/Internals/TypeHelperTests.cs b/tests/QuerySpecification.Tests/Internals/TypeHelperTests.cs new file mode 100644 index 00000000..78a1a4ff --- /dev/null +++ b/tests/QuerySpecification.Tests/Internals/TypeHelperTests.cs @@ -0,0 +1,121 @@ +using System.Reflection; + +namespace Tests.Internals; + +public class TypeHelperTests +{ + private static readonly Assembly[] _thisAssemblyArray = [typeof(TypeHelperTests).Assembly]; + + [Fact] + public void InstanceOf_IncludesTypeWithoutAttribute() + { + var result = TypeHelper.GetInstancesOf(_thisAssemblyArray); + + // Should be present + result.Should().ContainSingle(x => x is NoAttributeType); + + // Default order is int.MaxValue. Should be ordered after types with lower order + var orderTypeIndex = result.FindIndex(x => x is TestOrderTypeA); + var noAttrIndex = result.FindIndex(x => x is NoAttributeType); + noAttrIndex.Should().BeGreaterThan(orderTypeIndex); + } + + [Fact] + public void InstanceOf_ReturnsInstance_GivenParameterlessConstructor() + { + var result = TypeHelper.GetInstancesOf(_thisAssemblyArray); + + result.Should().ContainSingle(x=>x is TestCtorType); + } + + [Fact] + public void InstanceOf_ReturnsInstance_GivenSingletonFromStaticField() + { + var result = TypeHelper.GetInstancesOf(_thisAssemblyArray); + + result.Should().ContainSingle(x => x is TestFieldType && ReferenceEquals(x, TestFieldType.Instance)); + } + + [Fact] + public void InstanceOf_ReturnsInstance_GivenSingletonFromStaticProperty() + { + var result = TypeHelper.GetInstancesOf(_thisAssemblyArray); + + result.Should().ContainSingle(x => x is TestPropertyType && ReferenceEquals(x, TestPropertyType.Instance)); + } + + [Fact] + public void InstanceOf_DoesNotReturnsInstance_GivenPrivateConstructorAndNoSingleton() + { + var result = TypeHelper.GetInstancesOf(_thisAssemblyArray); + + result.Should().NotContain(x => x is TestPrivateCtorType); + } + + [Fact] + public void InstanceOf_SkipsTypesWithDisabledDiscovery() + { + var result = TypeHelper.GetInstancesOf(_thisAssemblyArray); + + result.Should().NotContain(x => x is TestDisabledType); + } + + [Fact] + public void InstanceOf_SkipsAbstractAndNonAssignableTypes() + { + var result = TypeHelper.GetInstancesOf(_thisAssemblyArray); + + result.Should().NotContain(x => x is TestAbstractType); + result.Should().NotContain(x => x is NotAssignableType); + } + + [Fact] + public void InstanceOf_OrdersByOrderThenTypeName() + { + var result = TypeHelper.GetInstancesOf(_thisAssemblyArray); + + var indexA = result.FindIndex(x => x is TestOrderTypeA); + var indexB = result.FindIndex(x => x is TestOrderTypeB); + indexA.Should().BeLessThan(indexB, "TestOrderTypeA should come before TestOrderTypeB"); + } + + // Helper types for testing + public interface ITestType { } + public class TestDiscoveryAttribute : DiscoveryAttribute { } + + public abstract class TestAbstractType : ITestType { } + public class NotAssignableType { } + public class NoAttributeType : ITestType { } + + [TestDiscovery] + public class TestCtorType : ITestType { } + + [TestDiscovery] + public class TestPrivateCtorType : ITestType + { + private TestPrivateCtorType() { } + } + + [TestDiscovery] + public class TestFieldType : ITestType + { + public static TestFieldType Instance = new(); + private TestFieldType() { } + } + + [TestDiscovery] + public class TestPropertyType : ITestType + { + public static TestPropertyType Instance { get; } = new(); + private TestPropertyType() { } + } + + [TestDiscovery(Enable = false)] + public class TestDisabledType : ITestType { } + + [TestDiscovery(Order = 1)] + public class TestOrderTypeA : ITestType { } + + [TestDiscovery(Order = 2)] + public class TestOrderTypeB : ITestType { } +} diff --git a/tests/QuerySpecification.Tests/Internals/TypeProviderTests.cs b/tests/QuerySpecification.Tests/Internals/TypeProviderTests.cs new file mode 100644 index 00000000..7912a28b --- /dev/null +++ b/tests/QuerySpecification.Tests/Internals/TypeProviderTests.cs @@ -0,0 +1,70 @@ +namespace Tests.Internals; + +public class TypeProviderTests +{ + [Fact] + public void EvaluatorProvider_GetAllMemoryEvaluators_IncludesCustom() + { + var allEvaluators = EvaluatorProvider.GetAllMemoryEvaluators(); + allEvaluators.Should().ContainSingle(x => x is TestMemoryEvaluator); + } + + [Fact] + public void EvaluatorProvider_GetBuiltInMemoryEvaluators_ExcludesCustom() + { + var builtInEvaluators = EvaluatorProvider.GetBuiltInMemoryEvaluators(); + builtInEvaluators.Should().NotContain(x => x is TestMemoryEvaluator); + } + + [Fact] + public void EvaluatorProvider_GetAllEvaluators_IncludesCustom() + { + var allEvaluators = EvaluatorProvider.GetAllEvaluators(); + allEvaluators.Should().ContainSingle(x => x is TestEvaluator); + } + + [Fact] + public void EvaluatorProvider_GetBuiltInEvaluators_ExcludesCustom() + { + var builtInEvaluators = EvaluatorProvider.GetBuiltInEvaluators(); + builtInEvaluators.Should().NotContain(x => x is TestEvaluator); + } + + [Fact] + public void ValidatorProvider_GetAllValidators_IncludesCustom() + { + var allValidators = ValidatorProvider.GetAllValidators(); + allValidators.Should().ContainSingle(x => x is TestValidator); + } + + [Fact] + public void ValidatorProvider_GetBuiltInValidators_ExcludesCustom() + { + var builtInValidators = ValidatorProvider.GetBuiltInValidators(); + builtInValidators.Should().NotContain(x => x is TestValidator); + } + + public class TestEvaluator : IEvaluator + { + public IQueryable Evaluate(IQueryable source, Specification specification) where T : class + { + return source; + } + } + + public class TestMemoryEvaluator : IMemoryEvaluator + { + public IEnumerable Evaluate(IEnumerable source, Specification specification) + { + return source; + } + } + + public class TestValidator : IValidator + { + public bool IsValid(T entity, Specification specification) + { + return true; + } + } +} diff --git a/tests/QuerySpecification.Tests/Validators/SpecificationValidatorTests.cs b/tests/QuerySpecification.Tests/Validators/SpecificationValidatorTests.cs index 5516831d..b74d0bde 100644 --- a/tests/QuerySpecification.Tests/Validators/SpecificationValidatorTests.cs +++ b/tests/QuerySpecification.Tests/Validators/SpecificationValidatorTests.cs @@ -96,25 +96,45 @@ public void Constructor_SetsProvidedValidators() [Fact] public void DerivedSpecificationValidatorCanAlterDefaultValidators() { + var expectedValidators = new List + { + LikeValidator.Instance, + WhereValidator.Instance, + LikeValidator.Instance, + WhereValidator.Instance + }; + var validator = new SpecificationValidatorDerived(); var result = ValidatorsOf(validator); - result.Should().HaveCount(4); - result[0].Should().BeOfType(); - result[1].Should().BeOfType(); - result[2].Should().BeOfType(); - result[3].Should().BeOfType(); + result.Should().Equal(expectedValidators); + } + + [Fact] + public void DerivedSpecificationValidatorCanDisableDiscovery() + { + var validator = new SpecificationValidatorWithDisabledDiscovery(); + + var result = ValidatorsOf(validator); + result.Should().BeEmpty(); } private class SpecificationValidatorDerived : SpecificationValidator { - public SpecificationValidatorDerived() + public SpecificationValidatorDerived() : base(DiscoveryStrategy.BuiltInOnly) { Validators.Add(WhereValidator.Instance); Validators.Insert(0, LikeValidator.Instance); } } + private class SpecificationValidatorWithDisabledDiscovery : SpecificationValidator + { + public SpecificationValidatorWithDisabledDiscovery() : base(DiscoveryStrategy.Disable) + { + } + } + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "k__BackingField")] public static extern ref List ValidatorsOf(SpecificationValidator @this); } From d686b782144b79f0c5eb7446e1e5567d82a20960 Mon Sep 17 00:00:00 2001 From: Fati Iseni Date: Sat, 7 Jun 2025 22:16:45 +0200 Subject: [PATCH 06/11] Refactored the internals again. --- .editorconfig | 10 +- .../Evaluators/SpecificationEvaluator.cs | 25 +++- src/QuerySpecification/DiscoveryAttribute.cs | 8 ++ src/QuerySpecification/DiscoveryStrategy.cs | 22 --- .../SpecificationMemoryEvaluator.cs | 16 ++- .../Internals/TypeDiscovery.cs | 128 ++++++++++++++++++ .../Internals/TypeHelper.cs | 82 ----------- .../Internals/TypeProvider.cs | 41 ------ .../Validators/SpecificationValidator.cs | 15 +- .../Evaluators/SpecificationEvaluatorTests.cs | 18 +-- .../SpecificationMemoryEvaluatorTests.cs | 18 +-- ...peHelperTests.cs => TypeDiscoveryTests.cs} | 66 +++++++-- .../Internals/TypeProviderTests.cs | 70 ---------- .../Validators/SpecificationValidatorTests.cs | 18 +-- 14 files changed, 237 insertions(+), 300 deletions(-) delete mode 100644 src/QuerySpecification/DiscoveryStrategy.cs create mode 100644 src/QuerySpecification/Internals/TypeDiscovery.cs delete mode 100644 src/QuerySpecification/Internals/TypeHelper.cs delete mode 100644 src/QuerySpecification/Internals/TypeProvider.cs rename tests/QuerySpecification.Tests/Internals/{TypeHelperTests.cs => TypeDiscoveryTests.cs} (58%) delete mode 100644 tests/QuerySpecification.Tests/Internals/TypeProviderTests.cs diff --git a/.editorconfig b/.editorconfig index 90bdaa58..0ef01501 100644 --- a/.editorconfig +++ b/.editorconfig @@ -108,9 +108,9 @@ dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case -dotnet_naming_rule.private_or_internal_field_should_be__fieldname.severity = suggestion -dotnet_naming_rule.private_or_internal_field_should_be__fieldname.symbols = private_or_internal_field -dotnet_naming_rule.private_or_internal_field_should_be__fieldname.style = _fieldname +dotnet_naming_rule.private_field_should_be__fieldname.severity = suggestion +dotnet_naming_rule.private_field_should_be__fieldname.symbols = private_field +dotnet_naming_rule.private_field_should_be__fieldname.style = _fieldname dotnet_naming_rule.public_field_should_be_pascal_case.severity = suggestion dotnet_naming_rule.public_field_should_be_pascal_case.symbols = public_field @@ -122,6 +122,10 @@ dotnet_naming_symbols.interface.applicable_kinds = interface dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.interface.required_modifiers = +dotnet_naming_symbols.private_field.applicable_kinds = field +dotnet_naming_symbols.private_field.applicable_accessibilities = private, private_protected +dotnet_naming_symbols.private_field.required_modifiers = + dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected dotnet_naming_symbols.private_or_internal_field.required_modifiers = diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs index c11a3993..302b4942 100644 --- a/src/QuerySpecification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs +++ b/src/QuerySpecification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs @@ -18,14 +18,25 @@ public class SpecificationEvaluator /// /// Initializes a new instance of the class. /// - public SpecificationEvaluator(DiscoveryStrategy strategy = DiscoveryStrategy.All) + public SpecificationEvaluator() { - Evaluators = strategy switch - { - DiscoveryStrategy.BuiltInOnly => EvaluatorProvider.GetBuiltInEvaluators(), - DiscoveryStrategy.All => EvaluatorProvider.GetAllEvaluators(), - _ => [] - }; + Evaluators = TypeDiscovery.IsAutoDiscoveryEnabled + ? TypeDiscovery.GetEvaluators() + : + [ + WhereEvaluator.Instance, + LikeEvaluator.Instance, + IncludeStringEvaluator.Instance, + IncludeEvaluator.Instance, + OrderEvaluator.Instance, + QueryTagEvaluator.Instance, + IgnoreAutoIncludesEvaluator.Instance, + IgnoreQueryFiltersEvaluator.Instance, + AsSplitQueryEvaluator.Instance, + AsNoTrackingEvaluator.Instance, + AsNoTrackingWithIdentityResolutionEvaluator.Instance, + AsTrackingEvaluator.Instance, + ]; } /// diff --git a/src/QuerySpecification/DiscoveryAttribute.cs b/src/QuerySpecification/DiscoveryAttribute.cs index 6bb041ad..a9c7cd58 100644 --- a/src/QuerySpecification/DiscoveryAttribute.cs +++ b/src/QuerySpecification/DiscoveryAttribute.cs @@ -1,5 +1,13 @@ namespace Pozitron.QuerySpecification; +/// +/// Specifies whether auto discovery for evaluators and validators is enabled. +/// +[AttributeUsage(AttributeTargets.Assembly)] +public sealed class AutoDiscoveryAttribute : Attribute +{ +} + /// /// Specifies discovery options for evaluators and validators, such as order and whether discovery is enabled. /// diff --git a/src/QuerySpecification/DiscoveryStrategy.cs b/src/QuerySpecification/DiscoveryStrategy.cs deleted file mode 100644 index 8a39fb24..00000000 --- a/src/QuerySpecification/DiscoveryStrategy.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Pozitron.QuerySpecification; - -/// -/// Specifies the strategy for discovering evaluators and validators. -/// -public enum DiscoveryStrategy -{ - /// - /// Discovery is disabled. - /// - Disable = 1, - - /// - /// Only built-in evaluators/validators from this library are discovered. - /// - BuiltInOnly = 2, - - /// - /// All evaluators from all loaded assemblies are discovered. - /// - All = 3 -} diff --git a/src/QuerySpecification/Evaluators/SpecificationMemoryEvaluator.cs b/src/QuerySpecification/Evaluators/SpecificationMemoryEvaluator.cs index be91bb68..1783c7ed 100644 --- a/src/QuerySpecification/Evaluators/SpecificationMemoryEvaluator.cs +++ b/src/QuerySpecification/Evaluators/SpecificationMemoryEvaluator.cs @@ -18,14 +18,16 @@ public class SpecificationMemoryEvaluator /// /// Initializes a new instance of the class. /// - public SpecificationMemoryEvaluator(DiscoveryStrategy strategy = DiscoveryStrategy.All) + public SpecificationMemoryEvaluator() { - Evaluators = strategy switch - { - DiscoveryStrategy.BuiltInOnly => EvaluatorProvider.GetBuiltInMemoryEvaluators(), - DiscoveryStrategy.All => EvaluatorProvider.GetAllMemoryEvaluators(), - _ => [] - }; + Evaluators = TypeDiscovery.IsAutoDiscoveryEnabled + ? TypeDiscovery.GetMemoryEvaluators() + : + [ + WhereEvaluator.Instance, + LikeMemoryEvaluator.Instance, + OrderEvaluator.Instance, + ]; } /// diff --git a/src/QuerySpecification/Internals/TypeDiscovery.cs b/src/QuerySpecification/Internals/TypeDiscovery.cs new file mode 100644 index 00000000..fe56d167 --- /dev/null +++ b/src/QuerySpecification/Internals/TypeDiscovery.cs @@ -0,0 +1,128 @@ +using System.Reflection; + +namespace Pozitron.QuerySpecification; + +internal static class TypeDiscovery +{ + private static readonly Lazy _loadedAssemblies = new( + () => + { + try + { + return AppDomain.CurrentDomain + .GetAssemblies() + .Where(a => + a.FullName != null && + !a.FullName.StartsWith("System", StringComparison.OrdinalIgnoreCase) && + !a.FullName.StartsWith("Microsoft", StringComparison.OrdinalIgnoreCase) && + !a.FullName.StartsWith("netstandard", StringComparison.OrdinalIgnoreCase) && + !a.FullName.StartsWith("Windows", StringComparison.OrdinalIgnoreCase) + ) + .ToArray(); + } + catch + { + return []; + } + }, + LazyThreadSafetyMode.ExecutionAndPublication); + + private static readonly Lazy _isAutoDiscoveryEnabled = new( + () => + { + try + { + return _loadedAssemblies + .Value + .Any(x => x.GetCustomAttribute() is not null); + } + catch + { + return false; + } + }, + LazyThreadSafetyMode.ExecutionAndPublication); + + private static readonly Lazy> _evaluators = new( + () => GetInstancesOf(), + LazyThreadSafetyMode.ExecutionAndPublication); + + private static readonly Lazy> _memoryEvaluators = new( + () => GetInstancesOf(), + LazyThreadSafetyMode.ExecutionAndPublication); + + private static readonly Lazy> _validators = new( + () => GetInstancesOf(), + LazyThreadSafetyMode.ExecutionAndPublication); + + + internal static bool IsAutoDiscoveryEnabled => _isAutoDiscoveryEnabled.Value; + internal static List GetMemoryEvaluators() => _memoryEvaluators.Value.ToList(); + internal static List GetEvaluators() => _evaluators.Value.ToList(); + internal static List GetValidators() => _validators.Value.ToList(); + + internal static List GetInstancesOf() + where TType : class + where TAttribute : DiscoveryAttribute + => GetInstancesOf(_loadedAssemblies.Value); + + internal static List GetInstancesOf(IEnumerable assemblies) + where TType : class + where TAttribute : DiscoveryAttribute + { + try + { + var baseType = typeof(TType); + var typeInstances = new List<(TType Instance, int Order, string TypeName)>(); + + var types = assemblies + .SelectMany(a => + { + try { return a.GetTypes(); } catch { return Array.Empty(); } + }) + .Where(t => t.IsClass && !t.IsAbstract && !t.ContainsGenericParameters && baseType.IsAssignableFrom(t)) + .Distinct(); + + foreach (var type in types) + { + var discoveryAttr = type.GetCustomAttribute(); + if (discoveryAttr is not null && !discoveryAttr.Enable) + continue; + + TType? instance = null; + + if (type.GetConstructor(Type.EmptyTypes) is not null) + { + instance = (TType?)Activator.CreateInstance(type); + } + else if (type.GetFields(BindingFlags.Public | BindingFlags.Static) + .Where(f => type.IsAssignableFrom(f.FieldType)) + .FirstOrDefault() is FieldInfo instanceField) + { + instance = (TType?)instanceField.GetValue(null); + } + else if (type.GetProperties(BindingFlags.Public | BindingFlags.Static) + .Where(f => type.IsAssignableFrom(f.PropertyType)) + .FirstOrDefault() is PropertyInfo instanceProp) + { + instance = (TType?)instanceProp.GetValue(null); + } + + if (instance is null) continue; + + int order = discoveryAttr?.Order ?? int.MaxValue; + typeInstances.Add((instance, order, type.Name)); + } + + return typeInstances + .OrderBy(e => e.Order) + .ThenBy(e => e.TypeName) + .Select(e => e.Instance) + .ToList(); + } + catch + { + return []; + } + } +} diff --git a/src/QuerySpecification/Internals/TypeHelper.cs b/src/QuerySpecification/Internals/TypeHelper.cs deleted file mode 100644 index c0d42fd0..00000000 --- a/src/QuerySpecification/Internals/TypeHelper.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Reflection; - -namespace Pozitron.QuerySpecification; - -internal static class TypeHelper -{ - internal static readonly Lazy _loadedAssemblies = new( - () => AppDomain.CurrentDomain.GetAssemblies() - .Where(a => - a.FullName != null && - !a.FullName.StartsWith("System", StringComparison.OrdinalIgnoreCase) && - !a.FullName.StartsWith("Microsoft", StringComparison.OrdinalIgnoreCase) && - !a.FullName.StartsWith("netstandard", StringComparison.OrdinalIgnoreCase) && - !a.FullName.StartsWith("Windows", StringComparison.OrdinalIgnoreCase) - ) - .ToArray(), - LazyThreadSafetyMode.ExecutionAndPublication); - - internal static readonly Lazy _specificationAssemblies = new( - () => _loadedAssemblies.Value - .Where(a => a.FullName!.StartsWith("Pozitron.QuerySpecification", StringComparison.OrdinalIgnoreCase)) - .ToArray(), - LazyThreadSafetyMode.ExecutionAndPublication); - - internal static List GetInstancesOf(bool scanOnlySpecificationAssemblies) - where TType : class - where TAttribute : DiscoveryAttribute - => GetInstancesOf(scanOnlySpecificationAssemblies ? _specificationAssemblies.Value : _loadedAssemblies.Value); - - internal static List GetInstancesOf(IEnumerable assemblies) - where TType : class - where TAttribute : DiscoveryAttribute - { - var baseType = typeof(TType); - var typeInstances = new List<(TType Instance, int Order, string TypeName)>(); - - var types = assemblies - .SelectMany(a => - { - try { return a.GetTypes(); } catch { return Array.Empty(); } - }) - .Where(t => t.IsClass && !t.IsAbstract && !t.ContainsGenericParameters && baseType.IsAssignableFrom(t)) - .Distinct(); - - foreach (var type in types) - { - var discoveryAttr = type.GetCustomAttribute(); - if (discoveryAttr is not null && !discoveryAttr.Enable) - continue; - - TType? instance = null; - - if (type.GetConstructor(Type.EmptyTypes) is not null) - { - instance = (TType?)Activator.CreateInstance(type); - } - else if (type.GetFields(BindingFlags.Public | BindingFlags.Static) - .Where(f => type.IsAssignableFrom(f.FieldType)) - .FirstOrDefault() is FieldInfo instanceField) - { - instance = (TType?)instanceField.GetValue(null); - } - else if (type.GetProperties(BindingFlags.Public | BindingFlags.Static) - .Where(f => type.IsAssignableFrom(f.PropertyType)) - .FirstOrDefault() is PropertyInfo instanceProp) - { - instance = (TType?)instanceProp.GetValue(null); - } - - if (instance is null) continue; - - int order = discoveryAttr?.Order ?? int.MaxValue; - typeInstances.Add((instance, order, type.Name)); - } - - return typeInstances - .OrderBy(e => e.Order) - .ThenBy(e => e.TypeName) - .Select(e => e.Instance) - .ToList(); - } -} diff --git a/src/QuerySpecification/Internals/TypeProvider.cs b/src/QuerySpecification/Internals/TypeProvider.cs deleted file mode 100644 index 551d02ad..00000000 --- a/src/QuerySpecification/Internals/TypeProvider.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace Pozitron.QuerySpecification; - -internal class EvaluatorProvider -{ - public static List GetAllMemoryEvaluators() => _allMemoryEvaluators.Value.ToList(); - public static List GetBuiltInMemoryEvaluators() => _builtInMemoryEvaluators.Value.ToList(); - - public static List GetAllEvaluators() => _allEvaluators.Value.ToList(); - public static List GetBuiltInEvaluators() => _builtInEvaluators.Value.ToList(); - - private static readonly Lazy> _allEvaluators = new( - () => TypeHelper.GetInstancesOf(scanOnlySpecificationAssemblies: false), - LazyThreadSafetyMode.ExecutionAndPublication); - - private static readonly Lazy> _builtInEvaluators = new( - () => TypeHelper.GetInstancesOf(scanOnlySpecificationAssemblies: true), - LazyThreadSafetyMode.ExecutionAndPublication); - - private static readonly Lazy> _allMemoryEvaluators = new( - () => TypeHelper.GetInstancesOf(scanOnlySpecificationAssemblies: false), - LazyThreadSafetyMode.ExecutionAndPublication); - - private static readonly Lazy> _builtInMemoryEvaluators = new( - () => TypeHelper.GetInstancesOf(scanOnlySpecificationAssemblies: true), - LazyThreadSafetyMode.ExecutionAndPublication); -} - -internal class ValidatorProvider -{ - public static List GetAllValidators() => _allValidators.Value.ToList(); - public static List GetBuiltInValidators() => _builtInValidators.Value.ToList(); - - - private static readonly Lazy> _allValidators = new( - () => TypeHelper.GetInstancesOf(scanOnlySpecificationAssemblies: false), - LazyThreadSafetyMode.ExecutionAndPublication); - - private static readonly Lazy> _builtInValidators = new( - () => TypeHelper.GetInstancesOf(scanOnlySpecificationAssemblies: true), - LazyThreadSafetyMode.ExecutionAndPublication); -} diff --git a/src/QuerySpecification/Validators/SpecificationValidator.cs b/src/QuerySpecification/Validators/SpecificationValidator.cs index 0f9c1e89..22b182e2 100644 --- a/src/QuerySpecification/Validators/SpecificationValidator.cs +++ b/src/QuerySpecification/Validators/SpecificationValidator.cs @@ -18,14 +18,15 @@ public class SpecificationValidator /// /// Initializes a new instance of the class. /// - public SpecificationValidator(DiscoveryStrategy strategy = DiscoveryStrategy.All) + public SpecificationValidator() { - Validators = strategy switch - { - DiscoveryStrategy.BuiltInOnly => ValidatorProvider.GetBuiltInValidators(), - DiscoveryStrategy.All => ValidatorProvider.GetAllValidators(), - _ => [] - }; + Validators = TypeDiscovery.IsAutoDiscoveryEnabled + ? TypeDiscovery.GetValidators() + : + [ + WhereValidator.Instance, + LikeValidator.Instance, + ]; } /// diff --git a/tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/SpecificationEvaluatorTests.cs b/tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/SpecificationEvaluatorTests.cs index fa3d5104..dfc0d69c 100644 --- a/tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/SpecificationEvaluatorTests.cs +++ b/tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/SpecificationEvaluatorTests.cs @@ -411,31 +411,15 @@ public void DerivedSpecificationEvaluatorCanAlterDefaultEvaluators() result.Should().Equal(expectedEvaluators); } - [Fact] - public void DerivedSpecificationEvaluatorCanDisableDiscovery() - { - var evaluator = new SpecificationEvaluatorWithDisabledDiscovery(); - - var result = EvaluatorsOf(evaluator); - result.Should().BeEmpty(); - } - private class SpecificationEvaluatorDerived : SpecificationEvaluator { - public SpecificationEvaluatorDerived() : base(DiscoveryStrategy.BuiltInOnly) + public SpecificationEvaluatorDerived() { Evaluators.Add(WhereEvaluator.Instance); Evaluators.Insert(0, LikeEvaluator.Instance); } } - private class SpecificationEvaluatorWithDisabledDiscovery : SpecificationEvaluator - { - public SpecificationEvaluatorWithDisabledDiscovery() : base(DiscoveryStrategy.Disable) - { - } - } - [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "k__BackingField")] public static extern ref List EvaluatorsOf(SpecificationEvaluator @this); } diff --git a/tests/QuerySpecification.Tests/Evaluators/SpecificationMemoryEvaluatorTests.cs b/tests/QuerySpecification.Tests/Evaluators/SpecificationMemoryEvaluatorTests.cs index 65afc2cc..977e05d5 100644 --- a/tests/QuerySpecification.Tests/Evaluators/SpecificationMemoryEvaluatorTests.cs +++ b/tests/QuerySpecification.Tests/Evaluators/SpecificationMemoryEvaluatorTests.cs @@ -273,31 +273,15 @@ public void DerivedSpecificationEvaluatorCanAlterDefaultEvaluators() result.Should().Equal(expectedEvaluators); } - [Fact] - public void DerivedSpecificationEvaluatorCanDisableDiscovery() - { - var evaluator = new SpecificationEvaluatorWithDisabledDiscovery(); - - var result = EvaluatorsOf(evaluator); - result.Should().BeEmpty(); - } - private class SpecificationEvaluatorDerived : SpecificationMemoryEvaluator { - public SpecificationEvaluatorDerived() : base(DiscoveryStrategy.BuiltInOnly) + public SpecificationEvaluatorDerived() { Evaluators.Add(WhereEvaluator.Instance); Evaluators.Insert(0, LikeMemoryEvaluator.Instance); } } - private class SpecificationEvaluatorWithDisabledDiscovery : SpecificationMemoryEvaluator - { - public SpecificationEvaluatorWithDisabledDiscovery() : base(DiscoveryStrategy.Disable) - { - } - } - [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "k__BackingField")] public static extern ref List EvaluatorsOf(SpecificationMemoryEvaluator @this); } diff --git a/tests/QuerySpecification.Tests/Internals/TypeHelperTests.cs b/tests/QuerySpecification.Tests/Internals/TypeDiscoveryTests.cs similarity index 58% rename from tests/QuerySpecification.Tests/Internals/TypeHelperTests.cs rename to tests/QuerySpecification.Tests/Internals/TypeDiscoveryTests.cs index 78a1a4ff..d6a8bba6 100644 --- a/tests/QuerySpecification.Tests/Internals/TypeHelperTests.cs +++ b/tests/QuerySpecification.Tests/Internals/TypeDiscoveryTests.cs @@ -2,14 +2,35 @@ namespace Tests.Internals; -public class TypeHelperTests +public class TypeDiscoveryTests { - private static readonly Assembly[] _thisAssemblyArray = [typeof(TypeHelperTests).Assembly]; + private static readonly Assembly[] _thisAssemblyArray = [typeof(TypeDiscoveryTests).Assembly]; + + [Fact] + public void GetMemoryEvaluators_IncludesCustom() + { + var allEvaluators = TypeDiscovery.GetMemoryEvaluators(); + allEvaluators.Should().ContainSingle(x => x is TestMemoryEvaluator); + } + + [Fact] + public void GetEvaluators_IncludesCustom() + { + var allEvaluators = TypeDiscovery.GetEvaluators(); + allEvaluators.Should().ContainSingle(x => x is TestEvaluator); + } + + [Fact] + public void GetValidators_IncludesCustom() + { + var allValidators = TypeDiscovery.GetValidators(); + allValidators.Should().ContainSingle(x => x is TestValidator); + } [Fact] public void InstanceOf_IncludesTypeWithoutAttribute() { - var result = TypeHelper.GetInstancesOf(_thisAssemblyArray); + var result = TypeDiscovery.GetInstancesOf(_thisAssemblyArray); // Should be present result.Should().ContainSingle(x => x is NoAttributeType); @@ -23,7 +44,7 @@ public void InstanceOf_IncludesTypeWithoutAttribute() [Fact] public void InstanceOf_ReturnsInstance_GivenParameterlessConstructor() { - var result = TypeHelper.GetInstancesOf(_thisAssemblyArray); + var result = TypeDiscovery.GetInstancesOf(_thisAssemblyArray); result.Should().ContainSingle(x=>x is TestCtorType); } @@ -31,7 +52,7 @@ public void InstanceOf_ReturnsInstance_GivenParameterlessConstructor() [Fact] public void InstanceOf_ReturnsInstance_GivenSingletonFromStaticField() { - var result = TypeHelper.GetInstancesOf(_thisAssemblyArray); + var result = TypeDiscovery.GetInstancesOf(_thisAssemblyArray); result.Should().ContainSingle(x => x is TestFieldType && ReferenceEquals(x, TestFieldType.Instance)); } @@ -39,7 +60,7 @@ public void InstanceOf_ReturnsInstance_GivenSingletonFromStaticField() [Fact] public void InstanceOf_ReturnsInstance_GivenSingletonFromStaticProperty() { - var result = TypeHelper.GetInstancesOf(_thisAssemblyArray); + var result = TypeDiscovery.GetInstancesOf(_thisAssemblyArray); result.Should().ContainSingle(x => x is TestPropertyType && ReferenceEquals(x, TestPropertyType.Instance)); } @@ -47,7 +68,7 @@ public void InstanceOf_ReturnsInstance_GivenSingletonFromStaticProperty() [Fact] public void InstanceOf_DoesNotReturnsInstance_GivenPrivateConstructorAndNoSingleton() { - var result = TypeHelper.GetInstancesOf(_thisAssemblyArray); + var result = TypeDiscovery.GetInstancesOf(_thisAssemblyArray); result.Should().NotContain(x => x is TestPrivateCtorType); } @@ -55,7 +76,7 @@ public void InstanceOf_DoesNotReturnsInstance_GivenPrivateConstructorAndNoSingle [Fact] public void InstanceOf_SkipsTypesWithDisabledDiscovery() { - var result = TypeHelper.GetInstancesOf(_thisAssemblyArray); + var result = TypeDiscovery.GetInstancesOf(_thisAssemblyArray); result.Should().NotContain(x => x is TestDisabledType); } @@ -63,7 +84,7 @@ public void InstanceOf_SkipsTypesWithDisabledDiscovery() [Fact] public void InstanceOf_SkipsAbstractAndNonAssignableTypes() { - var result = TypeHelper.GetInstancesOf(_thisAssemblyArray); + var result = TypeDiscovery.GetInstancesOf(_thisAssemblyArray); result.Should().NotContain(x => x is TestAbstractType); result.Should().NotContain(x => x is NotAssignableType); @@ -72,13 +93,38 @@ public void InstanceOf_SkipsAbstractAndNonAssignableTypes() [Fact] public void InstanceOf_OrdersByOrderThenTypeName() { - var result = TypeHelper.GetInstancesOf(_thisAssemblyArray); + var result = TypeDiscovery.GetInstancesOf(_thisAssemblyArray); var indexA = result.FindIndex(x => x is TestOrderTypeA); var indexB = result.FindIndex(x => x is TestOrderTypeB); indexA.Should().BeLessThan(indexB, "TestOrderTypeA should come before TestOrderTypeB"); } + // Custom user evaluators and validators + public class TestEvaluator : IEvaluator + { + public IQueryable Evaluate(IQueryable source, Specification specification) where T : class + { + return source; + } + } + + public class TestMemoryEvaluator : IMemoryEvaluator + { + public IEnumerable Evaluate(IEnumerable source, Specification specification) + { + return source; + } + } + + public class TestValidator : IValidator + { + public bool IsValid(T entity, Specification specification) + { + return true; + } + } + // Helper types for testing public interface ITestType { } public class TestDiscoveryAttribute : DiscoveryAttribute { } diff --git a/tests/QuerySpecification.Tests/Internals/TypeProviderTests.cs b/tests/QuerySpecification.Tests/Internals/TypeProviderTests.cs deleted file mode 100644 index 7912a28b..00000000 --- a/tests/QuerySpecification.Tests/Internals/TypeProviderTests.cs +++ /dev/null @@ -1,70 +0,0 @@ -namespace Tests.Internals; - -public class TypeProviderTests -{ - [Fact] - public void EvaluatorProvider_GetAllMemoryEvaluators_IncludesCustom() - { - var allEvaluators = EvaluatorProvider.GetAllMemoryEvaluators(); - allEvaluators.Should().ContainSingle(x => x is TestMemoryEvaluator); - } - - [Fact] - public void EvaluatorProvider_GetBuiltInMemoryEvaluators_ExcludesCustom() - { - var builtInEvaluators = EvaluatorProvider.GetBuiltInMemoryEvaluators(); - builtInEvaluators.Should().NotContain(x => x is TestMemoryEvaluator); - } - - [Fact] - public void EvaluatorProvider_GetAllEvaluators_IncludesCustom() - { - var allEvaluators = EvaluatorProvider.GetAllEvaluators(); - allEvaluators.Should().ContainSingle(x => x is TestEvaluator); - } - - [Fact] - public void EvaluatorProvider_GetBuiltInEvaluators_ExcludesCustom() - { - var builtInEvaluators = EvaluatorProvider.GetBuiltInEvaluators(); - builtInEvaluators.Should().NotContain(x => x is TestEvaluator); - } - - [Fact] - public void ValidatorProvider_GetAllValidators_IncludesCustom() - { - var allValidators = ValidatorProvider.GetAllValidators(); - allValidators.Should().ContainSingle(x => x is TestValidator); - } - - [Fact] - public void ValidatorProvider_GetBuiltInValidators_ExcludesCustom() - { - var builtInValidators = ValidatorProvider.GetBuiltInValidators(); - builtInValidators.Should().NotContain(x => x is TestValidator); - } - - public class TestEvaluator : IEvaluator - { - public IQueryable Evaluate(IQueryable source, Specification specification) where T : class - { - return source; - } - } - - public class TestMemoryEvaluator : IMemoryEvaluator - { - public IEnumerable Evaluate(IEnumerable source, Specification specification) - { - return source; - } - } - - public class TestValidator : IValidator - { - public bool IsValid(T entity, Specification specification) - { - return true; - } - } -} diff --git a/tests/QuerySpecification.Tests/Validators/SpecificationValidatorTests.cs b/tests/QuerySpecification.Tests/Validators/SpecificationValidatorTests.cs index b74d0bde..971e15e6 100644 --- a/tests/QuerySpecification.Tests/Validators/SpecificationValidatorTests.cs +++ b/tests/QuerySpecification.Tests/Validators/SpecificationValidatorTests.cs @@ -110,31 +110,15 @@ public void DerivedSpecificationValidatorCanAlterDefaultValidators() result.Should().Equal(expectedValidators); } - [Fact] - public void DerivedSpecificationValidatorCanDisableDiscovery() - { - var validator = new SpecificationValidatorWithDisabledDiscovery(); - - var result = ValidatorsOf(validator); - result.Should().BeEmpty(); - } - private class SpecificationValidatorDerived : SpecificationValidator { - public SpecificationValidatorDerived() : base(DiscoveryStrategy.BuiltInOnly) + public SpecificationValidatorDerived() { Validators.Add(WhereValidator.Instance); Validators.Insert(0, LikeValidator.Instance); } } - private class SpecificationValidatorWithDisabledDiscovery : SpecificationValidator - { - public SpecificationValidatorWithDisabledDiscovery() : base(DiscoveryStrategy.Disable) - { - } - } - [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "k__BackingField")] public static extern ref List ValidatorsOf(SpecificationValidator @this); } From a86d1eb7a2b58c8398f6b13f84b6268f783030a3 Mon Sep 17 00:00:00 2001 From: Fati Iseni Date: Sat, 7 Jun 2025 22:21:15 +0200 Subject: [PATCH 07/11] Set Order numbers. --- .../Evaluators/AsNoTrackingEvaluator.cs | 2 +- .../Evaluators/AsNoTrackingWithIdentityResolutionEvaluator.cs | 2 +- .../Evaluators/AsSplitQueryEvaluator.cs | 2 +- .../Evaluators/AsTrackingEvaluator.cs | 2 +- .../Evaluators/IgnoreAutoIncludesEvaluator.cs | 2 +- .../Evaluators/IgnoreQueryFiltersEvaluator.cs | 2 +- .../Evaluators/IncludeEvaluator.cs | 3 +-- .../Evaluators/IncludeStringEvaluator.cs | 2 +- .../Evaluators/LikeEvaluator.cs | 2 +- .../Evaluators/QueryTagEvaluator.cs | 2 +- src/QuerySpecification/Evaluators/LikeMemoryEvaluator.cs | 2 +- src/QuerySpecification/Evaluators/OrderEvaluator.cs | 2 +- src/QuerySpecification/Evaluators/WhereEvaluator.cs | 2 +- src/QuerySpecification/Validators/LikeValidator.cs | 2 +- src/QuerySpecification/Validators/WhereValidator.cs | 2 +- 15 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsNoTrackingEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsNoTrackingEvaluator.cs index 95ec058a..cec76c30 100644 --- a/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsNoTrackingEvaluator.cs +++ b/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsNoTrackingEvaluator.cs @@ -3,7 +3,7 @@ /// /// Evaluator to apply AsNoTracking to the query if the specification has AsNoTracking set to true. /// -[EvaluatorDiscovery(Order = -55)] +[EvaluatorDiscovery(Order = 100)] public sealed class AsNoTrackingEvaluator : IEvaluator { /// diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsNoTrackingWithIdentityResolutionEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsNoTrackingWithIdentityResolutionEvaluator.cs index f16f4c04..95eca9e6 100644 --- a/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsNoTrackingWithIdentityResolutionEvaluator.cs +++ b/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsNoTrackingWithIdentityResolutionEvaluator.cs @@ -3,7 +3,7 @@ /// /// Evaluator to apply AsNoTracking to the query if the specification has AsNoTracking set to true. /// -[EvaluatorDiscovery(Order = -50)] +[EvaluatorDiscovery(Order = 110)] public sealed class AsNoTrackingWithIdentityResolutionEvaluator : IEvaluator { /// diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsSplitQueryEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsSplitQueryEvaluator.cs index 1fdc99b3..04eb9847 100644 --- a/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsSplitQueryEvaluator.cs +++ b/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsSplitQueryEvaluator.cs @@ -3,7 +3,7 @@ /// /// Evaluator to apply AsSplitQuery to the query if the specification has AsSplitQuery set to true. /// -[EvaluatorDiscovery(Order = -60)] +[EvaluatorDiscovery(Order = 90)] public sealed class AsSplitQueryEvaluator : IEvaluator { /// diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsTrackingEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsTrackingEvaluator.cs index cd1f6cf2..da3c64f0 100644 --- a/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsTrackingEvaluator.cs +++ b/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsTrackingEvaluator.cs @@ -3,7 +3,7 @@ /// /// Evaluator to apply AsTracking to the query if the specification has AsTracking set to true. /// -[EvaluatorDiscovery(Order = -45)] +[EvaluatorDiscovery(Order = 120)] public sealed class AsTrackingEvaluator : IEvaluator { /// diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/IgnoreAutoIncludesEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/IgnoreAutoIncludesEvaluator.cs index 297c758d..c0f023ae 100644 --- a/src/QuerySpecification.EntityFrameworkCore/Evaluators/IgnoreAutoIncludesEvaluator.cs +++ b/src/QuerySpecification.EntityFrameworkCore/Evaluators/IgnoreAutoIncludesEvaluator.cs @@ -3,7 +3,7 @@ /// /// Evaluator to apply IgnoreAutoIncludes to the query if the specification has IgnoreAutoIncludes set to true. /// -[EvaluatorDiscovery(Order = -70)] +[EvaluatorDiscovery(Order = 70)] public sealed class IgnoreAutoIncludesEvaluator : IEvaluator { diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/IgnoreQueryFiltersEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/IgnoreQueryFiltersEvaluator.cs index 37a9f3a1..8757064e 100644 --- a/src/QuerySpecification.EntityFrameworkCore/Evaluators/IgnoreQueryFiltersEvaluator.cs +++ b/src/QuerySpecification.EntityFrameworkCore/Evaluators/IgnoreQueryFiltersEvaluator.cs @@ -3,7 +3,7 @@ /// /// Evaluator to apply IgnoreQueryFilters to the query if the specification has IgnoreQueryFilters set to true. /// -[EvaluatorDiscovery(Order = -65)] +[EvaluatorDiscovery(Order = 80)] public sealed class IgnoreQueryFiltersEvaluator : IEvaluator { diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/IncludeEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/IncludeEvaluator.cs index a3b46a3a..7c5849f7 100644 --- a/src/QuerySpecification.EntityFrameworkCore/Evaluators/IncludeEvaluator.cs +++ b/src/QuerySpecification.EntityFrameworkCore/Evaluators/IncludeEvaluator.cs @@ -1,5 +1,4 @@ using Microsoft.EntityFrameworkCore.Query; -using System.Collections; using System.Collections.Concurrent; using System.Diagnostics; using System.Reflection; @@ -9,7 +8,7 @@ namespace Pozitron.QuerySpecification; /// /// Evaluates a specification to include navigation properties. /// -[EvaluatorDiscovery(Order = -85)] +[EvaluatorDiscovery(Order = 40)] public sealed class IncludeEvaluator : IEvaluator { private static readonly MethodInfo _includeMethodInfo = typeof(EntityFrameworkQueryableExtensions) diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/IncludeStringEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/IncludeStringEvaluator.cs index c7708c99..dc641053 100644 --- a/src/QuerySpecification.EntityFrameworkCore/Evaluators/IncludeStringEvaluator.cs +++ b/src/QuerySpecification.EntityFrameworkCore/Evaluators/IncludeStringEvaluator.cs @@ -3,7 +3,7 @@ /// /// Evaluates a specification to include navigation properties specified by string paths. /// -[EvaluatorDiscovery(Order = -90)] +[EvaluatorDiscovery(Order = 30)] public sealed class IncludeStringEvaluator : IEvaluator { diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/LikeEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/LikeEvaluator.cs index 62a291be..41fa04fa 100644 --- a/src/QuerySpecification.EntityFrameworkCore/Evaluators/LikeEvaluator.cs +++ b/src/QuerySpecification.EntityFrameworkCore/Evaluators/LikeEvaluator.cs @@ -19,7 +19,7 @@ This was the previous implementation. We're trying to avoid allocations of LikeE /// /// Evaluates a specification to apply "like" expressions for filtering. /// -[EvaluatorDiscovery(Order = -95)] +[EvaluatorDiscovery(Order = 20)] public sealed class LikeEvaluator : IEvaluator { private LikeEvaluator() { } diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/QueryTagEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/QueryTagEvaluator.cs index 4956f481..1dbf6e49 100644 --- a/src/QuerySpecification.EntityFrameworkCore/Evaluators/QueryTagEvaluator.cs +++ b/src/QuerySpecification.EntityFrameworkCore/Evaluators/QueryTagEvaluator.cs @@ -3,7 +3,7 @@ /// /// Evaluator to apply the query tags to the query. /// -[EvaluatorDiscovery(Order = -75)] +[EvaluatorDiscovery(Order = 60)] public sealed class QueryTagEvaluator : IEvaluator { diff --git a/src/QuerySpecification/Evaluators/LikeMemoryEvaluator.cs b/src/QuerySpecification/Evaluators/LikeMemoryEvaluator.cs index 4c461acc..19ea86f2 100644 --- a/src/QuerySpecification/Evaluators/LikeMemoryEvaluator.cs +++ b/src/QuerySpecification/Evaluators/LikeMemoryEvaluator.cs @@ -20,7 +20,7 @@ public IEnumerable Evaluate(IEnumerable source, Specification specif /// /// Represents an in-memory evaluator for "Like" expressions. /// -[EvaluatorDiscovery(Order = -95)] +[EvaluatorDiscovery(Order = 20)] public sealed class LikeMemoryEvaluator : IMemoryEvaluator { /// diff --git a/src/QuerySpecification/Evaluators/OrderEvaluator.cs b/src/QuerySpecification/Evaluators/OrderEvaluator.cs index 451e6514..7c077753 100644 --- a/src/QuerySpecification/Evaluators/OrderEvaluator.cs +++ b/src/QuerySpecification/Evaluators/OrderEvaluator.cs @@ -3,7 +3,7 @@ /// /// Represents an evaluator for order expressions. /// -[EvaluatorDiscovery(Order = -80)] +[EvaluatorDiscovery(Order = 50)] public sealed class OrderEvaluator : IEvaluator, IMemoryEvaluator { /// diff --git a/src/QuerySpecification/Evaluators/WhereEvaluator.cs b/src/QuerySpecification/Evaluators/WhereEvaluator.cs index 3add7be1..1c69dd58 100644 --- a/src/QuerySpecification/Evaluators/WhereEvaluator.cs +++ b/src/QuerySpecification/Evaluators/WhereEvaluator.cs @@ -3,7 +3,7 @@ /// /// Represents an evaluator for where expressions. /// -[EvaluatorDiscovery(Order = -100)] +[EvaluatorDiscovery(Order = 10)] public sealed class WhereEvaluator : IEvaluator, IMemoryEvaluator { /// diff --git a/src/QuerySpecification/Validators/LikeValidator.cs b/src/QuerySpecification/Validators/LikeValidator.cs index 649f54cb..bd9ee6b7 100644 --- a/src/QuerySpecification/Validators/LikeValidator.cs +++ b/src/QuerySpecification/Validators/LikeValidator.cs @@ -18,7 +18,7 @@ public bool IsValid(T entity, Specification specification) /// /// Represents a validator for "like" expressions. /// -[ValidatorDiscovery(Order = -95)] +[ValidatorDiscovery(Order = 20)] public sealed class LikeValidator : IValidator { /// diff --git a/src/QuerySpecification/Validators/WhereValidator.cs b/src/QuerySpecification/Validators/WhereValidator.cs index 64c31884..13062a86 100644 --- a/src/QuerySpecification/Validators/WhereValidator.cs +++ b/src/QuerySpecification/Validators/WhereValidator.cs @@ -3,7 +3,7 @@ /// /// Represents a validator for where expressions. /// -[ValidatorDiscovery(Order = -100)] +[ValidatorDiscovery(Order = 10)] public sealed class WhereValidator : IValidator { /// From c639cff3a7a0cf10c84573411e62690ab0ed9746 Mon Sep 17 00:00:00 2001 From: Fati Iseni Date: Sat, 7 Jun 2025 23:06:53 +0200 Subject: [PATCH 08/11] Added AutoDiscovery test project. --- QuerySpecification.sln | 7 +++ .../Internals/TypeDiscovery.cs | 2 +- .../QuerySpecification.csproj | 1 + .../GlobalUsings.cs | 4 ++ ...rySpecification.AutoDiscovery.Tests.csproj | 8 +++ .../SpecificationEvaluatorTests.cs | 39 ++++++++++++++ .../SpecificationMemoryEvaluatorTests.cs | 39 ++++++++++++++ .../SpecificationValidatorTests.cs | 39 ++++++++++++++ .../TypeDiscoveryTests.cs | 53 +++++++++++++++++++ 9 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 tests/QuerySpecification.AutoDiscovery.Tests/GlobalUsings.cs create mode 100644 tests/QuerySpecification.AutoDiscovery.Tests/QuerySpecification.AutoDiscovery.Tests.csproj create mode 100644 tests/QuerySpecification.AutoDiscovery.Tests/SpecificationEvaluatorTests.cs create mode 100644 tests/QuerySpecification.AutoDiscovery.Tests/SpecificationMemoryEvaluatorTests.cs create mode 100644 tests/QuerySpecification.AutoDiscovery.Tests/SpecificationValidatorTests.cs create mode 100644 tests/QuerySpecification.AutoDiscovery.Tests/TypeDiscoveryTests.cs diff --git a/QuerySpecification.sln b/QuerySpecification.sln index 7a90114d..37588a22 100644 --- a/QuerySpecification.sln +++ b/QuerySpecification.sln @@ -34,6 +34,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution setup-sqllocaldb.ps1 = setup-sqllocaldb.ps1 EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuerySpecification.AutoDiscovery.Tests", "tests\QuerySpecification.AutoDiscovery.Tests\QuerySpecification.AutoDiscovery.Tests.csproj", "{154BA43D-0416-4840-85E1-B5ED34929E04}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -60,6 +62,10 @@ Global {F25DE99A-0BE4-41C6-9D1C-570CCE6B0BB9}.Debug|Any CPU.Build.0 = Debug|Any CPU {F25DE99A-0BE4-41C6-9D1C-570CCE6B0BB9}.Release|Any CPU.ActiveCfg = Release|Any CPU {F25DE99A-0BE4-41C6-9D1C-570CCE6B0BB9}.Release|Any CPU.Build.0 = Release|Any CPU + {154BA43D-0416-4840-85E1-B5ED34929E04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {154BA43D-0416-4840-85E1-B5ED34929E04}.Debug|Any CPU.Build.0 = Debug|Any CPU + {154BA43D-0416-4840-85E1-B5ED34929E04}.Release|Any CPU.ActiveCfg = Release|Any CPU + {154BA43D-0416-4840-85E1-B5ED34929E04}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -68,6 +74,7 @@ Global {BAFCF13F-05EF-4DEF-AD78-B5006CE86784} = {670F5387-F816-409B-8048-183F2608EDB3} {F85A5DB8-DB84-430A-8DA1-43A613FBD0CC} = {670F5387-F816-409B-8048-183F2608EDB3} {F25DE99A-0BE4-41C6-9D1C-570CCE6B0BB9} = {670F5387-F816-409B-8048-183F2608EDB3} + {154BA43D-0416-4840-85E1-B5ED34929E04} = {670F5387-F816-409B-8048-183F2608EDB3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E5CD485E-D269-41DE-A144-EEC734F5B893} diff --git a/src/QuerySpecification/Internals/TypeDiscovery.cs b/src/QuerySpecification/Internals/TypeDiscovery.cs index fe56d167..29303083 100644 --- a/src/QuerySpecification/Internals/TypeDiscovery.cs +++ b/src/QuerySpecification/Internals/TypeDiscovery.cs @@ -34,7 +34,7 @@ internal static class TypeDiscovery { return _loadedAssemblies .Value - .Any(x => x.GetCustomAttribute() is not null); + .Any(x => x.GetCustomAttributes().Any(attr => attr.GetType().Equals(typeof(AutoDiscoveryAttribute)))); } catch { diff --git a/src/QuerySpecification/QuerySpecification.csproj b/src/QuerySpecification/QuerySpecification.csproj index 8ea96e93..91ed2d30 100644 --- a/src/QuerySpecification/QuerySpecification.csproj +++ b/src/QuerySpecification/QuerySpecification.csproj @@ -19,6 +19,7 @@ + diff --git a/tests/QuerySpecification.AutoDiscovery.Tests/GlobalUsings.cs b/tests/QuerySpecification.AutoDiscovery.Tests/GlobalUsings.cs new file mode 100644 index 00000000..b4889453 --- /dev/null +++ b/tests/QuerySpecification.AutoDiscovery.Tests/GlobalUsings.cs @@ -0,0 +1,4 @@ +global using FluentAssertions; +global using Pozitron.QuerySpecification; +global using System.Linq.Expressions; +global using Xunit; diff --git a/tests/QuerySpecification.AutoDiscovery.Tests/QuerySpecification.AutoDiscovery.Tests.csproj b/tests/QuerySpecification.AutoDiscovery.Tests/QuerySpecification.AutoDiscovery.Tests.csproj new file mode 100644 index 00000000..dfb30c3a --- /dev/null +++ b/tests/QuerySpecification.AutoDiscovery.Tests/QuerySpecification.AutoDiscovery.Tests.csproj @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/tests/QuerySpecification.AutoDiscovery.Tests/SpecificationEvaluatorTests.cs b/tests/QuerySpecification.AutoDiscovery.Tests/SpecificationEvaluatorTests.cs new file mode 100644 index 00000000..162e096b --- /dev/null +++ b/tests/QuerySpecification.AutoDiscovery.Tests/SpecificationEvaluatorTests.cs @@ -0,0 +1,39 @@ +using System.Runtime.CompilerServices; + +namespace Tests; + +public class SpecificationEvaluatorTests +{ + [Fact] + public void DefaultSingleton_ScansEvaluators_GivenAutoDiscoveryEnabled() + { + var evaluator = SpecificationEvaluator.Default; + + var result = EvaluatorsOf(evaluator); + + result.Should().HaveCountGreaterThan(1); + result.Should().ContainSingle(x => x is TestEvaluator); + } + + [Fact] + public void Constructor_ScansEvaluators_GivenAutoDiscoveryEnabled() + { + var evaluator = new SpecificationEvaluator(); + + var result = EvaluatorsOf(evaluator); + + result.Should().HaveCountGreaterThan(1); + result.Should().ContainSingle(x => x is TestEvaluator); + } + + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "k__BackingField")] + public static extern ref List EvaluatorsOf(SpecificationEvaluator @this); + + public class TestEvaluator : IEvaluator + { + public IQueryable Evaluate(IQueryable source, Specification specification) where T : class + { + return source; + } + } +} diff --git a/tests/QuerySpecification.AutoDiscovery.Tests/SpecificationMemoryEvaluatorTests.cs b/tests/QuerySpecification.AutoDiscovery.Tests/SpecificationMemoryEvaluatorTests.cs new file mode 100644 index 00000000..e6abb580 --- /dev/null +++ b/tests/QuerySpecification.AutoDiscovery.Tests/SpecificationMemoryEvaluatorTests.cs @@ -0,0 +1,39 @@ +using System.Runtime.CompilerServices; + +namespace Tests; + +public class SpecificationMemoryEvaluatorTests +{ + [Fact] + public void DefaultSingleton_ScansEvaluators_GivenAutoDiscoveryEnabled() + { + var evaluator = SpecificationMemoryEvaluator.Default; + + var result = EvaluatorsOf(evaluator); + + result.Should().HaveCountGreaterThan(1); + result.Should().ContainSingle(x => x is TestMemoryEvaluator); + } + + [Fact] + public void Constructor_ScansEvaluators_GivenAutoDiscoveryEnabled() + { + var evaluator = new SpecificationMemoryEvaluator(); + + var result = EvaluatorsOf(evaluator); + + result.Should().HaveCountGreaterThan(1); + result.Should().ContainSingle(x => x is TestMemoryEvaluator); + } + + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "k__BackingField")] + public static extern ref List EvaluatorsOf(SpecificationMemoryEvaluator @this); + + public class TestMemoryEvaluator : IMemoryEvaluator + { + public IEnumerable Evaluate(IEnumerable source, Specification specification) + { + return source; + } + } +} diff --git a/tests/QuerySpecification.AutoDiscovery.Tests/SpecificationValidatorTests.cs b/tests/QuerySpecification.AutoDiscovery.Tests/SpecificationValidatorTests.cs new file mode 100644 index 00000000..b97f8ea8 --- /dev/null +++ b/tests/QuerySpecification.AutoDiscovery.Tests/SpecificationValidatorTests.cs @@ -0,0 +1,39 @@ +using System.Runtime.CompilerServices; + +namespace Tests; + +public class SpecificationValidatorTests +{ + [Fact] + public void DefaultSingleton_ScansValidators_GivenAutoDiscoveryEnabled() + { + var validators = SpecificationValidator.Default; + + var result = ValidatorsOf(validators); + + result.Should().HaveCountGreaterThan(1); + result.Should().ContainSingle(x => x is TestValidator); + } + + [Fact] + public void Constructor_ScansValidators_GivenAutoDiscoveryEnabled() + { + var validators = new SpecificationValidator(); + + var result = ValidatorsOf(validators); + + result.Should().HaveCountGreaterThan(1); + result.Should().ContainSingle(x => x is TestValidator); + } + + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "k__BackingField")] + public static extern ref List ValidatorsOf(SpecificationValidator @this); + + public class TestValidator : IValidator + { + public bool IsValid(T entity, Specification specification) + { + return true; + } + } +} diff --git a/tests/QuerySpecification.AutoDiscovery.Tests/TypeDiscoveryTests.cs b/tests/QuerySpecification.AutoDiscovery.Tests/TypeDiscoveryTests.cs new file mode 100644 index 00000000..9ea29d62 --- /dev/null +++ b/tests/QuerySpecification.AutoDiscovery.Tests/TypeDiscoveryTests.cs @@ -0,0 +1,53 @@ +[assembly: AutoDiscovery] + +namespace Tests; + +public class TypeDiscoveryTests +{ + [Fact] + public void GetMemoryEvaluators_IncludesCustom() + { + var allEvaluators = TypeDiscovery.GetMemoryEvaluators(); + allEvaluators.Should().ContainSingle(x => x is TestMemoryEvaluator); + } + + [Fact] + public void GetEvaluators_IncludesCustom() + { + var allEvaluators = TypeDiscovery.GetEvaluators(); + allEvaluators.Should().ContainSingle(x => x is TestEvaluator); + } + + [Fact] + public void GetValidators_IncludesCustom() + { + var allValidators = TypeDiscovery.GetValidators(); + allValidators.Should().ContainSingle(x => x is TestValidator); + } + + // Custom user evaluators and validators + public class TestEvaluator : IEvaluator + { + public IQueryable Evaluate(IQueryable source, Specification specification) where T : class + { + return source; + } + } + + public class TestMemoryEvaluator : IMemoryEvaluator + { + public IEnumerable Evaluate(IEnumerable source, Specification specification) + { + return source; + } + } + + public class TestValidator : IValidator + { + public bool IsValid(T entity, Specification specification) + { + return true; + } + } +} + From 4c105df8fae5feffaf4ae16bd7d61e67fd318318 Mon Sep 17 00:00:00 2001 From: Fati Iseni Date: Sat, 7 Jun 2025 23:11:59 +0200 Subject: [PATCH 09/11] Renamed the assembly attribute. --- src/QuerySpecification/DiscoveryAttribute.cs | 2 +- src/QuerySpecification/Internals/TypeDiscovery.cs | 2 +- .../TypeDiscoveryTests.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/QuerySpecification/DiscoveryAttribute.cs b/src/QuerySpecification/DiscoveryAttribute.cs index a9c7cd58..a1a01b96 100644 --- a/src/QuerySpecification/DiscoveryAttribute.cs +++ b/src/QuerySpecification/DiscoveryAttribute.cs @@ -4,7 +4,7 @@ /// Specifies whether auto discovery for evaluators and validators is enabled. /// [AttributeUsage(AttributeTargets.Assembly)] -public sealed class AutoDiscoveryAttribute : Attribute +public sealed class SpecAutoDiscoveryAttribute : Attribute { } diff --git a/src/QuerySpecification/Internals/TypeDiscovery.cs b/src/QuerySpecification/Internals/TypeDiscovery.cs index 29303083..8e49bc66 100644 --- a/src/QuerySpecification/Internals/TypeDiscovery.cs +++ b/src/QuerySpecification/Internals/TypeDiscovery.cs @@ -34,7 +34,7 @@ internal static class TypeDiscovery { return _loadedAssemblies .Value - .Any(x => x.GetCustomAttributes().Any(attr => attr.GetType().Equals(typeof(AutoDiscoveryAttribute)))); + .Any(x => x.GetCustomAttributes().Any(attr => attr.GetType().Equals(typeof(SpecAutoDiscoveryAttribute)))); } catch { diff --git a/tests/QuerySpecification.AutoDiscovery.Tests/TypeDiscoveryTests.cs b/tests/QuerySpecification.AutoDiscovery.Tests/TypeDiscoveryTests.cs index 9ea29d62..3b3757bb 100644 --- a/tests/QuerySpecification.AutoDiscovery.Tests/TypeDiscoveryTests.cs +++ b/tests/QuerySpecification.AutoDiscovery.Tests/TypeDiscoveryTests.cs @@ -1,4 +1,4 @@ -[assembly: AutoDiscovery] +[assembly: SpecAutoDiscovery] namespace Tests; From 2ef72834c3835b0075f2909149cd1c93948440be Mon Sep 17 00:00:00 2001 From: Fati Iseni Date: Sat, 7 Jun 2025 23:27:47 +0200 Subject: [PATCH 10/11] Added build target. --- src/QuerySpecification/QuerySpecification.csproj | 4 ++++ .../build/Pozitron.QuerySpecification.targets | 14 ++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 src/QuerySpecification/build/Pozitron.QuerySpecification.targets diff --git a/src/QuerySpecification/QuerySpecification.csproj b/src/QuerySpecification/QuerySpecification.csproj index 91ed2d30..a5c893d4 100644 --- a/src/QuerySpecification/QuerySpecification.csproj +++ b/src/QuerySpecification/QuerySpecification.csproj @@ -15,6 +15,10 @@ + + + + diff --git a/src/QuerySpecification/build/Pozitron.QuerySpecification.targets b/src/QuerySpecification/build/Pozitron.QuerySpecification.targets new file mode 100644 index 00000000..05ebb93d --- /dev/null +++ b/src/QuerySpecification/build/Pozitron.QuerySpecification.targets @@ -0,0 +1,14 @@ + + + + + <_SpecAutoDiscoveryLower>$([System.String]::Copy('$(SpecAutoDiscovery)').ToLowerInvariant()) + + + + + + + + + From 8c13d494855cc3994eb7c8357498026a8619da92 Mon Sep 17 00:00:00 2001 From: Fati Iseni Date: Sat, 7 Jun 2025 23:29:34 +0200 Subject: [PATCH 11/11] Updated XML comments. --- src/QuerySpecification/DiscoveryAttribute.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/QuerySpecification/DiscoveryAttribute.cs b/src/QuerySpecification/DiscoveryAttribute.cs index a1a01b96..e61d8f2f 100644 --- a/src/QuerySpecification/DiscoveryAttribute.cs +++ b/src/QuerySpecification/DiscoveryAttribute.cs @@ -9,31 +9,31 @@ public sealed class SpecAutoDiscoveryAttribute : Attribute } /// -/// Specifies discovery options for evaluators and validators, such as order and whether discovery is enabled. +/// Specifies discovery options for evaluators and validators, such as the order and whether discovery is enabled. /// [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public class DiscoveryAttribute : Attribute { /// - /// Gets the order in which the evaluator should be applied. Lower values are applied first. + /// Gets the order in which the evaluator/validator should be applied. Lower values are applied first. /// public int Order { get; set; } = int.MaxValue; /// - /// Gets a value indicating whether the evaluator is discoverable. + /// Gets a value indicating whether the evaluator/validator is discoverable. /// public bool Enable { get; set; } = true; } /// -/// Specifies discovery options for evaluators, such as order and whether discovery is enabled. +/// Specifies discovery options for evaluators, such as the order and whether discovery is enabled. /// public sealed class EvaluatorDiscoveryAttribute : DiscoveryAttribute { } /// -/// Specifies discovery options for validators, such as order and whether discovery is enabled. +/// Specifies discovery options for validators, such as the order and whether discovery is enabled. /// public sealed class ValidatorDiscoveryAttribute : DiscoveryAttribute {