From 123d1f3a5cb86a4e85be3dc656e99cfd27643de3 Mon Sep 17 00:00:00 2001 From: Corniel Nobel Date: Sat, 16 Nov 2024 20:19:03 +0100 Subject: [PATCH] Search values (#83) --- Directory.Packages.props | 1 + Qowaiv.Validation.sln | 8 ++- specs/Benchmarks/AllowedValues.cs | 35 +++++++++++++ specs/Benchmarks/Benchmarks.csproj | 23 +++++++++ specs/Benchmarks/Program.cs | 11 +++++ specs/Benchmarks/README.md | 8 +++ specs/Benchmarks/nuget.config | 7 +++ .../Attributes/Allowed_values_specs.cs | 49 +++++++++++++++---- .../Attributes/Forbidden_values_specs.cs | 49 +++++++++++++++---- .../Attributes/AllowedAttribute.cs | 19 +++++++ .../Attributes/AllowedValuesAttribute.cs | 1 + .../Attributes/ForbiddenAttribute.cs | 19 +++++++ .../Attributes/ForbiddenValuesAttribute.cs | 1 + .../Attributes/SetOfAttribute.cs | 35 +++++++++++++ .../Qowaiv.Validation.DataAnnotations.csproj | 6 ++- .../README.md | 4 +- 16 files changed, 254 insertions(+), 22 deletions(-) create mode 100644 specs/Benchmarks/AllowedValues.cs create mode 100644 specs/Benchmarks/Benchmarks.csproj create mode 100644 specs/Benchmarks/Program.cs create mode 100644 specs/Benchmarks/README.md create mode 100644 specs/Benchmarks/nuget.config create mode 100644 src/Qowaiv.Validation.DataAnnotations/Attributes/AllowedAttribute.cs create mode 100644 src/Qowaiv.Validation.DataAnnotations/Attributes/ForbiddenAttribute.cs create mode 100644 src/Qowaiv.Validation.DataAnnotations/Attributes/SetOfAttribute.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 10487db..567cbea 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,6 +7,7 @@ + diff --git a/Qowaiv.Validation.sln b/Qowaiv.Validation.sln index 2d2ae5f..a759a24 100644 --- a/Qowaiv.Validation.sln +++ b/Qowaiv.Validation.sln @@ -1,4 +1,3 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.1.32328.378 @@ -23,6 +22,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Qowaiv.Validation.Xml", "sr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = ".net", ".net.csproj", "{3DEEF29D-13A3-4739-80CD-1A60BDEC30A1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "specs\Benchmarks\Benchmarks.csproj", "{E206C651-BA32-4D38-8A6C-74D57A11BEE5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -65,12 +66,17 @@ Global {3DEEF29D-13A3-4739-80CD-1A60BDEC30A1}.Debug|Any CPU.Build.0 = Debug|Any CPU {3DEEF29D-13A3-4739-80CD-1A60BDEC30A1}.Release|Any CPU.ActiveCfg = Release|Any CPU {3DEEF29D-13A3-4739-80CD-1A60BDEC30A1}.Release|Any CPU.Build.0 = Release|Any CPU + {E206C651-BA32-4D38-8A6C-74D57A11BEE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E206C651-BA32-4D38-8A6C-74D57A11BEE5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E206C651-BA32-4D38-8A6C-74D57A11BEE5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E206C651-BA32-4D38-8A6C-74D57A11BEE5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {1FC5B4EB-46D8-49AA-963A-495CBD39807B} = {40C1E942-8DB1-4E58-8FB4-71F5EDA66029} + {E206C651-BA32-4D38-8A6C-74D57A11BEE5} = {40C1E942-8DB1-4E58-8FB4-71F5EDA66029} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9B55A336-E2D3-4656-9840-D519B3E1159B} diff --git a/specs/Benchmarks/AllowedValues.cs b/specs/Benchmarks/AllowedValues.cs new file mode 100644 index 0000000..0e46bed --- /dev/null +++ b/specs/Benchmarks/AllowedValues.cs @@ -0,0 +1,35 @@ +using BenchmarkDotNet.Attributes; +using Qowaiv.Diagnostics.Contracts; +using Qowaiv.Globalization; +using Qowaiv.Validation.DataAnnotations; + +namespace Benchmarks; + +[Inheritable] +public class AllowedValues +{ + private readonly AllowedValuesAttribute NonGeneric = new("NL", "BE", "LU", "DE", "FR"); + private readonly AllowedAttribute WithGenerics = new("NL", "BE", "LU", "DE", "FR"); + + [Benchmark(Baseline = true)] + public bool non_generic() + { + var result = false; + foreach(var country in Country.All) + { + result |= NonGeneric.IsValid(country); + } + return result; + } + + [Benchmark] + public bool generic() + { + var result = false; + foreach (var country in Country.All) + { + result |= WithGenerics.IsValid(country); + } + return result; + } +} diff --git a/specs/Benchmarks/Benchmarks.csproj b/specs/Benchmarks/Benchmarks.csproj new file mode 100644 index 0000000..a005367 --- /dev/null +++ b/specs/Benchmarks/Benchmarks.csproj @@ -0,0 +1,23 @@ + + + + Exe + net9.0 + enable + + + + + + + + + + + + + Always + + + + diff --git a/specs/Benchmarks/Program.cs b/specs/Benchmarks/Program.cs new file mode 100644 index 0000000..4dc0ef0 --- /dev/null +++ b/specs/Benchmarks/Program.cs @@ -0,0 +1,11 @@ +using BenchmarkDotNet.Running; + +namespace Benchmarks; + +internal static class Program +{ + public static void Main(string[] args) + { + BenchmarkRunner.Run(); + } +} diff --git a/specs/Benchmarks/README.md b/specs/Benchmarks/README.md new file mode 100644 index 0000000..43fa752 --- /dev/null +++ b/specs/Benchmarks/README.md @@ -0,0 +1,8 @@ +# Qowaiv validation benchmarks + +## Allowed Values attribute + +| Method | Mean | Ratio | +|----------------- |-----------:|------:| +| AllowedValues | 321.043 us | 1.000 | +| Allowed<T> | 2.775 us | 0.009 | diff --git a/specs/Benchmarks/nuget.config b/specs/Benchmarks/nuget.config new file mode 100644 index 0000000..cc1532a --- /dev/null +++ b/specs/Benchmarks/nuget.config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/specs/Qowaiv.Validation.Specs/DataAnnotations/Attributes/Allowed_values_specs.cs b/specs/Qowaiv.Validation.Specs/DataAnnotations/Attributes/Allowed_values_specs.cs index a2eefb2..1034e5e 100644 --- a/specs/Qowaiv.Validation.Specs/DataAnnotations/Attributes/Allowed_values_specs.cs +++ b/specs/Qowaiv.Validation.Specs/DataAnnotations/Attributes/Allowed_values_specs.cs @@ -1,23 +1,54 @@ +using Qowaiv.Financial; using Qowaiv.Validation.DataAnnotations; namespace Data_annotations.Attributes.Allowed_values_specs; public class Is_valid_for { - [Test] - public void Null() - => new AllowedValuesAttribute("DE", "FR", "GB").IsValid(null).Should().BeTrue(); + public class Generic + { + [Test] + public void Null() + => new AllowedAttribute("DE", "FR", "GB").IsValid(null).Should().BeTrue(); + + [Test] + public void value_in_allowed_values() + => new AllowedAttribute("DE", "FR", "GB").IsValid(Country.GB).Should().BeTrue(); + + [Test] + public void based_on_other_than_string() + => new AllowedAttribute(12.00, 17.23).IsValid(17.23.Amount()).Should().BeTrue(); + } + + [Obsolete("Will be dropped with next major.")] + public class Non_generic + { + [Test] + public void Null() + => new AllowedValuesAttribute("DE", "FR", "GB").IsValid(null).Should().BeTrue(); - [Test] - public void value_in_allowed_values() - => new AllowedValuesAttribute("DE", "FR", "GB").IsValid(Country.GB).Should().BeTrue(); + [Test] + public void value_in_allowed_values() + => new AllowedValuesAttribute("DE", "FR", "GB").IsValid(Country.GB).Should().BeTrue(); + } } public class Is_not_valid_for { - [Test] - public void value_not_in_allowed_values() + public class Generic + { + [Test] + public void value_not_in_allowed_values() + => new AllowedAttribute("DE", "FR", "GB").IsValid(Country.TR).Should().BeFalse(); + } + + [Obsolete("Will be dropped with next major.")] + public class Non_generic + { + [Test] + public void value_not_in_allowed_values() => new AllowedValuesAttribute("DE", "FR", "GB").IsValid(Country.TR).Should().BeFalse(); + } } public class With_message @@ -32,7 +63,7 @@ public void culture_dependent(CultureInfo culture, string message) } internal class Model { - [AllowedValues("DE", "FR", "GB")] + [Allowed("DE", "FR", "GB")] public Country Country { get; set; } = Country.NL; } } diff --git a/specs/Qowaiv.Validation.Specs/DataAnnotations/Attributes/Forbidden_values_specs.cs b/specs/Qowaiv.Validation.Specs/DataAnnotations/Attributes/Forbidden_values_specs.cs index 9d9424b..30849c4 100644 --- a/specs/Qowaiv.Validation.Specs/DataAnnotations/Attributes/Forbidden_values_specs.cs +++ b/specs/Qowaiv.Validation.Specs/DataAnnotations/Attributes/Forbidden_values_specs.cs @@ -1,23 +1,54 @@ +using Qowaiv.Financial; using Qowaiv.Validation.DataAnnotations; namespace Data_annotations.Attributes.Forbidden_values_specs; public class Is_valid_for { - [Test] - public void Null() - => new ForbiddenValuesAttribute("DE", "FR", "GB").IsValid(null).Should().BeTrue(); + public class Generic + { + [Test] + public void Null() + => new ForbiddenAttribute("DE", "FR", "GB").IsValid(null).Should().BeTrue(); + + [Test] + public void value_not_in_allowed_values() + => new ForbiddenAttribute("DE", "FR", "GB").IsValid(Country.TR).Should().BeTrue(); + + [Test] + public void based_on_other_than_string() + => new ForbiddenAttribute(12.00, 17.23).IsValid(64.28.Amount()).Should().BeTrue(); + } + + [Obsolete("Will be dropped with next major.")] + public class Non_generic + { + [Test] + public void Null() + => new ForbiddenValuesAttribute("DE", "FR", "GB").IsValid(null).Should().BeTrue(); - [Test] - public void value_not_in_allowed_values() - => new ForbiddenValuesAttribute("DE", "FR", "GB").IsValid(Country.TR).Should().BeTrue(); + [Test] + public void value_not_in_allowed_values() + => new ForbiddenValuesAttribute("DE", "FR", "GB").IsValid(Country.TR).Should().BeTrue(); + } } public class Is_not_valid_for { - [Test] - public void value_in_allowed_values() + public class Generic + { + [Test] + public void value_in_allowed_values() + => new ForbiddenAttribute("DE", "FR", "GB").IsValid(Country.GB).Should().BeFalse(); + } + + [Obsolete("Will be dropped with next major.")] + public class Non_generic + { + [Test] + public void value_in_allowed_values() => new ForbiddenValuesAttribute("DE", "FR", "GB").IsValid(Country.GB).Should().BeFalse(); + } } public class With_message @@ -32,7 +63,7 @@ public void culture_dependent(CultureInfo culture, string message) } internal class Model { - [ForbiddenValues("DE", "FR", "GB")] + [Forbidden("DE", "FR", "GB")] public Country Country { get; set; } = Country.DE; } } diff --git a/src/Qowaiv.Validation.DataAnnotations/Attributes/AllowedAttribute.cs b/src/Qowaiv.Validation.DataAnnotations/Attributes/AllowedAttribute.cs new file mode 100644 index 0000000..29c4bff --- /dev/null +++ b/src/Qowaiv.Validation.DataAnnotations/Attributes/AllowedAttribute.cs @@ -0,0 +1,19 @@ +namespace Qowaiv.Validation.DataAnnotations; + +/// Validates if the decorated item has a value that is specified in the allowed values. +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] +[CLSCompliant(false)] +public sealed class AllowedAttribute : SetOfAttribute +{ + /// Initializes a new instance of the class. + /// + /// String representations of the allowed values. + /// + public AllowedAttribute(params object[] values) + : base(values) => Do.Nothing(); + + /// Return true the value of + /// equals one of the values of the . + /// + protected override bool OnEqual => true; +} diff --git a/src/Qowaiv.Validation.DataAnnotations/Attributes/AllowedValuesAttribute.cs b/src/Qowaiv.Validation.DataAnnotations/Attributes/AllowedValuesAttribute.cs index 90283b8..a4183ef 100644 --- a/src/Qowaiv.Validation.DataAnnotations/Attributes/AllowedValuesAttribute.cs +++ b/src/Qowaiv.Validation.DataAnnotations/Attributes/AllowedValuesAttribute.cs @@ -3,6 +3,7 @@ namespace Qowaiv.Validation.DataAnnotations; /// Validates if the decorated item has a value that is specified in the allowed values. [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] [CLSCompliant(false)] +[Obsolete("Use AllowedAttribute instead.")] public sealed class AllowedValuesAttribute : SetOfValuesAttribute { /// Initializes a new instance of the class. diff --git a/src/Qowaiv.Validation.DataAnnotations/Attributes/ForbiddenAttribute.cs b/src/Qowaiv.Validation.DataAnnotations/Attributes/ForbiddenAttribute.cs new file mode 100644 index 0000000..98f9e49 --- /dev/null +++ b/src/Qowaiv.Validation.DataAnnotations/Attributes/ForbiddenAttribute.cs @@ -0,0 +1,19 @@ +namespace Qowaiv.Validation.DataAnnotations; + +/// Validates if the decorated item has a value that is specified in the forbidden values. +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] +[CLSCompliant(false)] +public sealed class ForbiddenAttribute : SetOfAttribute +{ + /// Initializes a new instance of the class. + /// + /// String representations of the forbidden values. + /// + public ForbiddenAttribute(params object[] values) + : base(values) => Do.Nothing(); + + /// Return false if the value of + /// equals one of the values of the . + /// + protected override bool OnEqual => false; +} diff --git a/src/Qowaiv.Validation.DataAnnotations/Attributes/ForbiddenValuesAttribute.cs b/src/Qowaiv.Validation.DataAnnotations/Attributes/ForbiddenValuesAttribute.cs index aac035a..b281114 100644 --- a/src/Qowaiv.Validation.DataAnnotations/Attributes/ForbiddenValuesAttribute.cs +++ b/src/Qowaiv.Validation.DataAnnotations/Attributes/ForbiddenValuesAttribute.cs @@ -3,6 +3,7 @@ namespace Qowaiv.Validation.DataAnnotations; /// Validates if the decorated item has a value that is specified in the forbidden values. [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] [CLSCompliant(false)] +[Obsolete("Use ForbiddenAttribute instead.")] public sealed class ForbiddenValuesAttribute : SetOfValuesAttribute { /// Initializes a new instance of the class. diff --git a/src/Qowaiv.Validation.DataAnnotations/Attributes/SetOfAttribute.cs b/src/Qowaiv.Validation.DataAnnotations/Attributes/SetOfAttribute.cs new file mode 100644 index 0000000..2474e8e --- /dev/null +++ b/src/Qowaiv.Validation.DataAnnotations/Attributes/SetOfAttribute.cs @@ -0,0 +1,35 @@ +namespace Qowaiv.Validation.DataAnnotations; + +/// Base for allowing or forbidding a set of values. +/// +/// The type of the value. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] +[CLSCompliant(false)] +public abstract class SetOfAttribute : ValidationAttribute +{ + /// Initializes a new instance of the class. + /// + /// String representations of the values. + /// + protected SetOfAttribute(params object[] values) + : base(() => QowaivValidationMessages.AllowedValuesAttribute_ValidationError) + { + var converter = TypeDescriptor.GetConverter(typeof(TValue)); + Values = new HashSet(values.Select(converter.ConvertFrom).OfType()); + } + + /// The result to return when the value of + /// equals one of the values of the . + /// + protected abstract bool OnEqual { get; } + + /// Gets the values. + public IReadOnlyCollection Values { get; } + + /// Returns true if the value is allowed. + [Pure] + public sealed override bool IsValid(object? value) + => value is null + || OnEqual == Values.Contains((TValue)value); +} diff --git a/src/Qowaiv.Validation.DataAnnotations/Qowaiv.Validation.DataAnnotations.csproj b/src/Qowaiv.Validation.DataAnnotations/Qowaiv.Validation.DataAnnotations.csproj index 784f3ca..f8770ca 100644 --- a/src/Qowaiv.Validation.DataAnnotations/Qowaiv.Validation.DataAnnotations.csproj +++ b/src/Qowaiv.Validation.DataAnnotations/Qowaiv.Validation.DataAnnotations.csproj @@ -3,12 +3,16 @@ - netstandard2.0;net6.0;net8.0 + netstandard2.0;net8.0;net9.0 3.0.0 Qowaiv.Validation.DataAnnotations as alternative to AllowedValuesAttribute. +- Introduction of ForbiddenAttribute as alternative to FobiddenValuesAttribute. +- Marked AllowedValuesAttribute as obsolete. +- Marked FobiddenValuesAttribute as obsolete. - Add .NET 9.0 target. - Drop .NET 5.0, NET6.0, NET7.0 targets. (BREAKING) - Drop binary serialization on exceptions. (BREAKING) diff --git a/src/Qowaiv.Validation.DataAnnotations/README.md b/src/Qowaiv.Validation.DataAnnotations/README.md index 4ce1679..6c1c1d4 100644 --- a/src/Qowaiv.Validation.DataAnnotations/README.md +++ b/src/Qowaiv.Validation.DataAnnotations/README.md @@ -62,7 +62,7 @@ supports type converters to get the allowed values based on a string value. ``` C# public class Model { - [AllowedValues("DE", "FR", "GB")] + [Allowed("DE", "FR", "GB")] public Country CountryOfBirth { get; set; } } ``` @@ -74,7 +74,7 @@ supports type converters to get the forbidden values based on a string value. ``` C# public class Model { - [ForbiddenValues("US", "IR")] + [Forbidden("US", "IR")] public Country CountryOfBirth { get; set; } } ```