Skip to content

Commit

Permalink
Merge branch 'main' into unique-attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
Corniel authored Nov 16, 2024
2 parents 726465e + 123d1f3 commit 5ea3d32
Show file tree
Hide file tree
Showing 16 changed files with 254 additions and 23 deletions.
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="AsyncFixer" Version="*" />
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
<PackageVersion Include="coverlet.collector" Version="*" />
<PackageVersion Include="DotNetProjectFile.Analyzers" Version="*" />
<PackageVersion Include="DotNetProjectFile.Analyzers.Sdk" Version="*" />
Expand Down
8 changes: 7 additions & 1 deletion Qowaiv.Validation.sln
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.32328.378
Expand All @@ -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
Expand Down Expand Up @@ -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}
Expand Down
35 changes: 35 additions & 0 deletions specs/Benchmarks/AllowedValues.cs
Original file line number Diff line number Diff line change
@@ -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<Country> 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;
}
}
23 changes: 23 additions & 0 deletions specs/Benchmarks/Benchmarks.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Qowaiv.Validation.DataAnnotations\Qowaiv.Validation.DataAnnotations.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="nuget.config">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
11 changes: 11 additions & 0 deletions specs/Benchmarks/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using BenchmarkDotNet.Running;

namespace Benchmarks;

internal static class Program
{
public static void Main(string[] args)
{
BenchmarkRunner.Run<AllowedValues>();
}
}
8 changes: 8 additions & 0 deletions specs/Benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Qowaiv validation benchmarks

## Allowed Values attribute

| Method | Mean | Ratio |
|----------------- |-----------:|------:|
| AllowedValues | 321.043 us | 1.000 |
| Allowed&lt;T&gt; | 2.775 us | 0.009 |
7 changes: 7 additions & 0 deletions specs/Benchmarks/nuget.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="NuGet" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>
Original file line number Diff line number Diff line change
@@ -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<Country>("DE", "FR", "GB").IsValid(null).Should().BeTrue();

[Test]
public void value_in_allowed_values()
=> new AllowedAttribute<Country>("DE", "FR", "GB").IsValid(Country.GB).Should().BeTrue();

[Test]
public void based_on_other_than_string()
=> new AllowedAttribute<Amount>(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<Country>("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
Expand All @@ -32,7 +63,7 @@ public void culture_dependent(CultureInfo culture, string message)
}
internal class Model
{
[AllowedValues("DE", "FR", "GB")]
[Allowed<Country>("DE", "FR", "GB")]
public Country Country { get; set; } = Country.NL;
}
}
Original file line number Diff line number Diff line change
@@ -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<Country>("DE", "FR", "GB").IsValid(null).Should().BeTrue();

[Test]
public void value_not_in_allowed_values()
=> new ForbiddenAttribute<Country>("DE", "FR", "GB").IsValid(Country.TR).Should().BeTrue();

[Test]
public void based_on_other_than_string()
=> new ForbiddenAttribute<Amount>(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<Country>("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
Expand All @@ -32,7 +63,7 @@ public void culture_dependent(CultureInfo culture, string message)
}
internal class Model
{
[ForbiddenValues("DE", "FR", "GB")]
[Forbidden<Country>("DE", "FR", "GB")]
public Country Country { get; set; } = Country.DE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Qowaiv.Validation.DataAnnotations;

/// <summary>Validates if the decorated item has a value that is specified in the allowed values.</summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
[CLSCompliant(false)]
public sealed class AllowedAttribute<TValue> : SetOfAttribute<TValue>
{
/// <summary>Initializes a new instance of the <see cref="AllowedAttribute{TValue}"/> class.</summary>
/// <param name="values">
/// String representations of the allowed values.
/// </param>
public AllowedAttribute(params object[] values)
: base(values) => Do.Nothing();

/// <summary>Return true the value of <see cref="SetOfAttribute{TValue}.IsValid(object)"/>
/// equals one of the values of the <see cref="SetOfAttribute{TValue}" />.
/// </summary>
protected override bool OnEqual => true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace Qowaiv.Validation.DataAnnotations;
/// <summary>Validates if the decorated item has a value that is specified in the allowed values.</summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
[CLSCompliant(false)]
[Obsolete("Use AllowedAttribute<T> instead.")]
public sealed class AllowedValuesAttribute : SetOfValuesAttribute
{
/// <summary>Initializes a new instance of the <see cref="AllowedValuesAttribute"/> class.</summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Qowaiv.Validation.DataAnnotations;

/// <summary>Validates if the decorated item has a value that is specified in the forbidden values.</summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
[CLSCompliant(false)]
public sealed class ForbiddenAttribute<TValue> : SetOfAttribute<TValue>
{
/// <summary>Initializes a new instance of the <see cref="ForbiddenAttribute{TValue}"/> class.</summary>
/// <param name="values">
/// String representations of the forbidden values.
/// </param>
public ForbiddenAttribute(params object[] values)
: base(values) => Do.Nothing();

/// <summary>Return false if the value of <see cref="SetOfValuesAttribute.IsValid(object)"/>
/// equals one of the values of the <see cref="ForbiddenAttribute{TValue}"/>.
/// </summary>
protected override bool OnEqual => false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace Qowaiv.Validation.DataAnnotations;
/// <summary>Validates if the decorated item has a value that is specified in the forbidden values.</summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
[CLSCompliant(false)]
[Obsolete("Use ForbiddenAttribute<T> instead.")]
public sealed class ForbiddenValuesAttribute : SetOfValuesAttribute
{
/// <summary>Initializes a new instance of the <see cref="ForbiddenValuesAttribute"/> class.</summary>
Expand Down
35 changes: 35 additions & 0 deletions src/Qowaiv.Validation.DataAnnotations/Attributes/SetOfAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace Qowaiv.Validation.DataAnnotations;

/// <summary>Base <see cref="ValidationAttribute"/> for allowing or forbidding a set of values.</summary>
/// <typeparam name="TValue">
/// The type of the value.
/// </typeparam>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
[CLSCompliant(false)]
public abstract class SetOfAttribute<TValue> : ValidationAttribute
{
/// <summary>Initializes a new instance of the <see cref="SetOfAttribute{TValue}"/> class.</summary>
/// <param name="values">
/// String representations of the values.
/// </param>
protected SetOfAttribute(params object[] values)
: base(() => QowaivValidationMessages.AllowedValuesAttribute_ValidationError)
{
var converter = TypeDescriptor.GetConverter(typeof(TValue));
Values = new HashSet<TValue>(values.Select(converter.ConvertFrom).OfType<TValue>());
}

/// <summary>The result to return when the value of <see cref="IsValid(object)"/>
/// equals one of the values of the <see cref="SetOfValuesAttribute"/>.
/// </summary>
protected abstract bool OnEqual { get; }

/// <summary>Gets the values.</summary>
public IReadOnlyCollection<TValue> Values { get; }

/// <summary>Returns true if the value is allowed.</summary>
[Pure]
public sealed override bool IsValid(object? value)
=> value is null
|| OnEqual == Values.Contains((TValue)value);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@
<Import Project="../../props/package.props" />

<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;net8.0;net9.0</TargetFrameworks>
<Version>3.0.0</Version>
<PackageId>Qowaiv.Validation.DataAnnotations</PackageId>
<PackageReleaseNotes>
<![CDATA[
v3.0.0
- Introduction of AllowedAttribute<T> as alternative to AllowedValuesAttribute.
- Introduction of ForbiddenAttribute<T> as alternative to FobiddenValuesAttribute.
- Introduction of UniqueAttribute<T> as alternative to DistinctValuesAttribute.
- Marked AllowedValuesAttribute as obsolete.
- Marked FobiddenValuesAttribute as obsolete.
- Marked DistinctValuesAttribute as obsolete.
- Introduction of UniqueAttribute<T>
- Add .NET 9.0 target.
- Drop .NET 5.0, NET6.0, NET7.0 targets. (BREAKING)
- Drop binary serialization on exceptions. (BREAKING)
Expand Down
Loading

0 comments on commit 5ea3d32

Please sign in to comment.