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/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/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/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.EntityFrameworkCore/Evaluators/AsNoTrackingEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsNoTrackingEvaluator.cs index be517e36..cec76c30 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 = 100)] public sealed class AsNoTrackingEvaluator : IEvaluator { /// diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsNoTrackingWithIdentityResolutionEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsNoTrackingWithIdentityResolutionEvaluator.cs index 5232a1cb..95eca9e6 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 = 110)] public sealed class AsNoTrackingWithIdentityResolutionEvaluator : IEvaluator { /// diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsSplitQueryEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsSplitQueryEvaluator.cs index a831452a..04eb9847 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 = 90)] public sealed class AsSplitQueryEvaluator : IEvaluator { /// diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsTrackingEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/AsTrackingEvaluator.cs index 64d09d5e..da3c64f0 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 = 120)] public sealed class AsTrackingEvaluator : IEvaluator { /// diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/IgnoreAutoIncludesEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/IgnoreAutoIncludesEvaluator.cs index 3d528746..c0f023ae 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..8757064e 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 = 80)] public sealed class IgnoreQueryFiltersEvaluator : IEvaluator { diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/IncludeEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/IncludeEvaluator.cs index 5d8598c2..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,6 +8,7 @@ namespace Pozitron.QuerySpecification; /// /// Evaluates a specification to include navigation properties. /// +[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 ff7e4523..dc641053 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 = 30)] public sealed class IncludeStringEvaluator : IEvaluator { diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/LikeEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/LikeEvaluator.cs index e2592c3b..41fa04fa 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 = 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 5066f881..1dbf6e49 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 = 60)] public sealed class QueryTagEvaluator : IEvaluator { diff --git a/src/QuerySpecification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs b/src/QuerySpecification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs index aa3ab776..302b4942 100644 --- a/src/QuerySpecification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs +++ b/src/QuerySpecification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs @@ -20,21 +20,23 @@ public class SpecificationEvaluator /// public SpecificationEvaluator() { - 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 = 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 new file mode 100644 index 00000000..e61d8f2f --- /dev/null +++ b/src/QuerySpecification/DiscoveryAttribute.cs @@ -0,0 +1,40 @@ +namespace Pozitron.QuerySpecification; + +/// +/// Specifies whether auto discovery for evaluators and validators is enabled. +/// +[AttributeUsage(AttributeTargets.Assembly)] +public sealed class SpecAutoDiscoveryAttribute : Attribute +{ +} + +/// +/// 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/validator should be applied. Lower values are applied first. + /// + public int Order { get; set; } = int.MaxValue; + + /// + /// Gets a value indicating whether the evaluator/validator is discoverable. + /// + public bool Enable { get; set; } = true; +} + +/// +/// 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 the order and whether discovery is enabled. +/// +public sealed class ValidatorDiscoveryAttribute : DiscoveryAttribute +{ +} 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 9859d272..19ea86f2 100644 --- a/src/QuerySpecification/Evaluators/LikeMemoryEvaluator.cs +++ b/src/QuerySpecification/Evaluators/LikeMemoryEvaluator.cs @@ -14,13 +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. /// -public sealed class LikeMemoryEvaluator : IInMemoryEvaluator +[EvaluatorDiscovery(Order = 20)] +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 5351b0af..7c077753 100644 --- a/src/QuerySpecification/Evaluators/OrderEvaluator.cs +++ b/src/QuerySpecification/Evaluators/OrderEvaluator.cs @@ -3,7 +3,8 @@ /// /// Represents an evaluator for order expressions. /// -public sealed class OrderEvaluator : IEvaluator, IInMemoryEvaluator +[EvaluatorDiscovery(Order = 50)] +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 81% rename from src/QuerySpecification/Evaluators/SpecificationInMemoryEvaluator.cs rename to src/QuerySpecification/Evaluators/SpecificationMemoryEvaluator.cs index 9c6b6175..1783c7ed 100644 --- a/src/QuerySpecification/Evaluators/SpecificationInMemoryEvaluator.cs +++ b/src/QuerySpecification/Evaluators/SpecificationMemoryEvaluator.cs @@ -3,36 +3,38 @@ /// /// 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() + public SpecificationMemoryEvaluator() { - Evaluators = - [ - WhereEvaluator.Instance, - OrderEvaluator.Instance, - LikeMemoryEvaluator.Instance, - ]; + Evaluators = TypeDiscovery.IsAutoDiscoveryEnabled + ? TypeDiscovery.GetMemoryEvaluators() + : + [ + WhereEvaluator.Instance, + LikeMemoryEvaluator.Instance, + OrderEvaluator.Instance, + ]; } /// - /// 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 a1629496..1c69dd58 100644 --- a/src/QuerySpecification/Evaluators/WhereEvaluator.cs +++ b/src/QuerySpecification/Evaluators/WhereEvaluator.cs @@ -3,7 +3,8 @@ /// /// Represents an evaluator for where expressions. /// -public sealed class WhereEvaluator : IEvaluator, IInMemoryEvaluator +[EvaluatorDiscovery(Order = 10)] +public sealed class WhereEvaluator : IEvaluator, IMemoryEvaluator { /// /// Gets the singleton instance of the class. diff --git a/src/QuerySpecification/Internals/TypeDiscovery.cs b/src/QuerySpecification/Internals/TypeDiscovery.cs new file mode 100644 index 00000000..8e49bc66 --- /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.GetCustomAttributes().Any(attr => attr.GetType().Equals(typeof(SpecAutoDiscoveryAttribute)))); + } + 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/QuerySpecification.csproj b/src/QuerySpecification/QuerySpecification.csproj index 8ea96e93..a5c893d4 100644 --- a/src/QuerySpecification/QuerySpecification.csproj +++ b/src/QuerySpecification/QuerySpecification.csproj @@ -15,10 +15,15 @@ + + + + + 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/src/QuerySpecification/Validators/LikeValidator.cs b/src/QuerySpecification/Validators/LikeValidator.cs index c86cabaf..bd9ee6b7 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 = 20)] public sealed class LikeValidator : IValidator { /// diff --git a/src/QuerySpecification/Validators/SpecificationValidator.cs b/src/QuerySpecification/Validators/SpecificationValidator.cs index e0900520..22b182e2 100644 --- a/src/QuerySpecification/Validators/SpecificationValidator.cs +++ b/src/QuerySpecification/Validators/SpecificationValidator.cs @@ -20,11 +20,13 @@ public class SpecificationValidator /// public SpecificationValidator() { - Validators = - [ - WhereValidator.Instance, - LikeValidator.Instance - ]; + Validators = TypeDiscovery.IsAutoDiscoveryEnabled + ? TypeDiscovery.GetValidators() + : + [ + WhereValidator.Instance, + LikeValidator.Instance, + ]; } /// diff --git a/src/QuerySpecification/Validators/WhereValidator.cs b/src/QuerySpecification/Validators/WhereValidator.cs index 9c760415..13062a86 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 = 10)] public sealed class WhereValidator : IValidator { /// 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()) + + + + + + + + + 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..3b3757bb --- /dev/null +++ b/tests/QuerySpecification.AutoDiscovery.Tests/TypeDiscoveryTests.cs @@ -0,0 +1,53 @@ +[assembly: SpecAutoDiscovery] + +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; + } + } +} + 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.EntityFrameworkCore.Tests/Evaluators/SpecificationEvaluatorTests.cs b/tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/SpecificationEvaluatorTests.cs index 7cd7101b..dfc0d69c 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(); @@ -385,26 +385,30 @@ 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); } private class SpecificationEvaluatorDerived : SpecificationEvaluator diff --git a/tests/QuerySpecification.Tests/Evaluators/SpecificationInMemoryEvaluatorTests.cs b/tests/QuerySpecification.Tests/Evaluators/SpecificationMemoryEvaluatorTests.cs similarity index 90% rename from tests/QuerySpecification.Tests/Evaluators/SpecificationInMemoryEvaluatorTests.cs rename to tests/QuerySpecification.Tests/Evaluators/SpecificationMemoryEvaluatorTests.cs index 4accc1b2..977e05d5 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); @@ -256,20 +256,24 @@ 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); } - private class SpecificationEvaluatorDerived : SpecificationInMemoryEvaluator + private class SpecificationEvaluatorDerived : SpecificationMemoryEvaluator { public SpecificationEvaluatorDerived() { @@ -279,5 +283,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); } diff --git a/tests/QuerySpecification.Tests/Internals/TypeDiscoveryTests.cs b/tests/QuerySpecification.Tests/Internals/TypeDiscoveryTests.cs new file mode 100644 index 00000000..d6a8bba6 --- /dev/null +++ b/tests/QuerySpecification.Tests/Internals/TypeDiscoveryTests.cs @@ -0,0 +1,167 @@ +using System.Reflection; + +namespace Tests.Internals; + +public class TypeDiscoveryTests +{ + 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 = TypeDiscovery.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 = TypeDiscovery.GetInstancesOf(_thisAssemblyArray); + + result.Should().ContainSingle(x=>x is TestCtorType); + } + + [Fact] + public void InstanceOf_ReturnsInstance_GivenSingletonFromStaticField() + { + var result = TypeDiscovery.GetInstancesOf(_thisAssemblyArray); + + result.Should().ContainSingle(x => x is TestFieldType && ReferenceEquals(x, TestFieldType.Instance)); + } + + [Fact] + public void InstanceOf_ReturnsInstance_GivenSingletonFromStaticProperty() + { + var result = TypeDiscovery.GetInstancesOf(_thisAssemblyArray); + + result.Should().ContainSingle(x => x is TestPropertyType && ReferenceEquals(x, TestPropertyType.Instance)); + } + + [Fact] + public void InstanceOf_DoesNotReturnsInstance_GivenPrivateConstructorAndNoSingleton() + { + var result = TypeDiscovery.GetInstancesOf(_thisAssemblyArray); + + result.Should().NotContain(x => x is TestPrivateCtorType); + } + + [Fact] + public void InstanceOf_SkipsTypesWithDisabledDiscovery() + { + var result = TypeDiscovery.GetInstancesOf(_thisAssemblyArray); + + result.Should().NotContain(x => x is TestDisabledType); + } + + [Fact] + public void InstanceOf_SkipsAbstractAndNonAssignableTypes() + { + var result = TypeDiscovery.GetInstancesOf(_thisAssemblyArray); + + result.Should().NotContain(x => x is TestAbstractType); + result.Should().NotContain(x => x is NotAssignableType); + } + + [Fact] + public void InstanceOf_OrdersByOrderThenTypeName() + { + 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 { } + + 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/Validators/SpecificationValidatorTests.cs b/tests/QuerySpecification.Tests/Validators/SpecificationValidatorTests.cs index 5516831d..971e15e6 100644 --- a/tests/QuerySpecification.Tests/Validators/SpecificationValidatorTests.cs +++ b/tests/QuerySpecification.Tests/Validators/SpecificationValidatorTests.cs @@ -96,14 +96,18 @@ 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); } private class SpecificationValidatorDerived : SpecificationValidator