feat: add request validation

This commit is contained in:
Louis Seubert 2026-05-16 09:49:04 +02:00
commit de2d0e2693
Signed by: louis9902
GPG key ID: 4B9DB28F826553BD
32 changed files with 1829 additions and 1 deletions

View file

@ -1,6 +1,8 @@
<Solution> <Solution>
<Project Path="src/request.validation/Geekeey.Request.Validation.csproj" />
<Project Path="src/request.validation.tests/Geekeey.Request.Validation.Tests.csproj" />
<Project Path="src/request/Geekeey.Request.csproj" /> <Project Path="src/request/Geekeey.Request.csproj" />
<Project Path="src/request.tests/Geekeey.Request.Tests.csproj" /> <Project Path="src/request.tests/Geekeey.Request.Tests.csproj" />
<Project Path="src/request.result/Geekeey.Request.Result.csproj" /> <Project Path="src/request.result/Geekeey.Request.Result.csproj" />
<Project Path="src/request.result.tests/Geekeey.Request.Result.Tests.csproj" /> <Project Path="src/request.result.tests/Geekeey.Request.Result.Tests.csproj" />
</Solution> </Solution>

View file

@ -0,0 +1,15 @@
[*.{cs,vb}]
# disable CA1822: Mark members as static
# -> TUnit requiring instance methods for test cases
dotnet_diagnostic.CA1822.severity = none
# disable CA1707: Identifiers should not contain underscores
dotnet_diagnostic.CA1707.severity = none
# disable IDE0060: Remove unused parameter
dotnet_diagnostic.IDE0060.severity = none
# disable IDE0005: Unnecessary using directive
dotnet_diagnostic.IDE0005.severity = none
# disable IDE0390: Method can be made synchronous
dotnet_diagnostic.IDE0390.severity = none
# disable IDE0391: Method can be made synchronous
dotnet_diagnostic.IDE0391.severity = none

View file

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="TUnit" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\request.validation\Geekeey.Request.Validation.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,326 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Text.RegularExpressions;
namespace Geekeey.Request.Validation.Tests;
internal sealed class RuleBuilderExtensionsTests
{
[Test]
public async Task I_can_validate_not_null_for_reference_types()
{
var validator = new PropertyValidator<ReferenceValueModel, object?>(model =>
model.Value, rule => rule.NotNull());
var invalid = validator.Validate(new ReferenceValueModel());
var valid = validator.Validate(new ReferenceValueModel { Value = new object() });
await AssertSingleProblem(invalid, nameof(ReferenceValueModel.Value), "Value is required.");
await Assert.That(valid.IsValid).IsTrue();
}
[Test]
public async Task I_can_validate_not_null_for_nullable_value_types()
{
var validator = new PropertyValidator<NullableIntValueModel, int?>(model =>
model.Value, rule => rule.NotNull());
var invalid = validator.Validate(new NullableIntValueModel());
var valid = validator.Validate(new NullableIntValueModel { Value = 1 });
await AssertSingleProblem(invalid, nameof(NullableIntValueModel.Value), "Value is required.");
await Assert.That(valid.IsValid).IsTrue();
}
[Test]
public async Task I_can_validate_not_empty_for_strings()
{
var validator = new PropertyValidator<StringValueModel, string?>(model =>
model.Value, rule => rule.NotEmpty());
var invalid = validator.Validate(new StringValueModel { Value = " " });
var valid = validator.Validate(new StringValueModel { Value = "abc" });
await AssertSingleProblem(invalid, nameof(StringValueModel.Value), "Value is required.");
await Assert.That(valid.IsValid).IsTrue();
}
[Test]
public async Task I_can_validate_not_empty_for_collections()
{
var validator = new PropertyValidator<CollectionValueModel, IEnumerable<string>?>(model =>
model.Value, rule => rule.NotEmpty());
var invalid = validator.Validate(new CollectionValueModel { Value = [] });
var valid = validator.Validate(new CollectionValueModel { Value = ["item"] });
await AssertSingleProblem(invalid, nameof(CollectionValueModel.Value), "Value is required.");
await Assert.That(valid.IsValid).IsTrue();
}
[Test]
public async Task I_can_validate_min_length()
{
var validator =
new PropertyValidator<StringValueModel, string?>(model =>
model.Value, rule => rule.MinLength(3));
var invalid = validator.Validate(new StringValueModel { Value = "ab" });
var valid = validator.Validate(new StringValueModel { Value = "abc" });
var ignoredNull = validator.Validate(new StringValueModel());
await AssertSingleProblem(invalid, nameof(StringValueModel.Value), "Value must be at least 3 characters long.");
await Assert.That(valid.IsValid).IsTrue();
await Assert.That(ignoredNull.IsValid).IsTrue();
}
[Test]
public async Task I_can_validate_max_length()
{
var validator = new PropertyValidator<StringValueModel, string?>(model
=> model.Value, rule => rule.MaxLength(5));
var invalid = validator.Validate(new StringValueModel { Value = "abcdef" });
var valid = validator.Validate(new StringValueModel { Value = "abcde" });
var ignoredNull = validator.Validate(new StringValueModel());
await AssertSingleProblem(invalid, nameof(StringValueModel.Value), "Value must be at most 5 characters long.");
await Assert.That(valid.IsValid).IsTrue();
await Assert.That(ignoredNull.IsValid).IsTrue();
}
[Test]
public async Task I_can_validate_length()
{
var validator = new PropertyValidator<StringValueModel, string?>(model
=> model.Value, rule => rule.Length(2, 4));
var invalid = validator.Validate(new StringValueModel { Value = "a" });
var valid = validator.Validate(new StringValueModel { Value = "ab" });
var ignoredNull = validator.Validate(new StringValueModel());
await AssertSingleProblem(invalid, nameof(StringValueModel.Value),
"Value must be between 2 and 4 characters long.");
await Assert.That(valid.IsValid).IsTrue();
await Assert.That(ignoredNull.IsValid).IsTrue();
}
[Test]
public async Task I_can_validate_greater_than()
{
var validator = new PropertyValidator<IntValueModel, int>(model
=> model.Value, rule => rule.GreaterThan(18));
var invalid = validator.Validate(new IntValueModel { Value = 18 });
var valid = validator.Validate(new IntValueModel { Value = 19 });
await AssertSingleProblem(invalid, nameof(IntValueModel.Value), "Value must be greater than 18.");
await Assert.That(valid.IsValid).IsTrue();
}
[Test]
public async Task I_can_validate_greater_than_or_equal_to()
{
var validator = new PropertyValidator<IntValueModel, int>(model
=> model.Value, rule => rule.GreaterThanOrEqualTo(1));
var invalid = validator.Validate(new IntValueModel { Value = 0 });
var valid = validator.Validate(new IntValueModel { Value = 1 });
await AssertSingleProblem(invalid, nameof(IntValueModel.Value), "Value must be greater than or equal to 1.");
await Assert.That(valid.IsValid).IsTrue();
}
[Test]
public async Task I_can_validate_less_than()
{
var validator = new PropertyValidator<IntValueModel, int>(model
=> model.Value, rule => rule.LessThan(10));
var invalid = validator.Validate(new IntValueModel { Value = 10 });
var valid = validator.Validate(new IntValueModel { Value = 9 });
await AssertSingleProblem(invalid, nameof(IntValueModel.Value), "Value must be less than 10.");
await Assert.That(valid.IsValid).IsTrue();
}
[Test]
public async Task I_can_validate_less_than_or_equal_to()
{
var validator = new PropertyValidator<IntValueModel, int>(model
=> model.Value, rule => rule.LessThanOrEqualTo(5));
var invalid = validator.Validate(new IntValueModel { Value = 6 });
var valid = validator.Validate(new IntValueModel { Value = 5 });
await AssertSingleProblem(invalid, nameof(IntValueModel.Value), "Value must be less than or equal to 5.");
await Assert.That(valid.IsValid).IsTrue();
}
[Test]
public async Task I_can_validate_between()
{
var validator = new PropertyValidator<IntValueModel, int>(model
=> model.Value, rule => rule.Between(0, 100));
var invalid = validator.Validate(new IntValueModel { Value = 101 });
var valid = validator.Validate(new IntValueModel { Value = 100 });
await AssertSingleProblem(invalid, nameof(IntValueModel.Value), "Value must be between 0 and 100.");
await Assert.That(valid.IsValid).IsTrue();
}
[Test]
public async Task I_can_validate_equal()
{
var validator = new PropertyValidator<StringValueModel, string?>(model
=> model.Value, rule => rule.Equal("ACTIVE"));
var invalid = validator.Validate(new StringValueModel { Value = "INACTIVE" });
var valid = validator.Validate(new StringValueModel { Value = "ACTIVE" });
await AssertSingleProblem(invalid, nameof(StringValueModel.Value), "Value must be equal to ACTIVE.");
await Assert.That(valid.IsValid).IsTrue();
}
[Test]
public async Task I_can_validate_not_equal()
{
var validator = new PropertyValidator<StringValueModel, string?>(model
=> model.Value, rule => rule.NotEqual("BANNED"));
var invalid = validator.Validate(new StringValueModel { Value = "BANNED" });
var valid = validator.Validate(new StringValueModel { Value = "ALLOWED" });
await AssertSingleProblem(invalid, nameof(StringValueModel.Value), "Value must not be equal to BANNED.");
await Assert.That(valid.IsValid).IsTrue();
}
[Test]
public async Task I_can_validate_equal_with_null_reference_values()
{
var validator = new PropertyValidator<StringValueModel, string?>(model
=> model.Value, rule => rule.Equal("ACTIVE"));
var invalid = validator.Validate(new StringValueModel { Value = "INACTIVE" });
var valid = validator.Validate(new StringValueModel { Value = "ACTIVE" });
var nullValue = validator.Validate(new StringValueModel());
await AssertSingleProblem(invalid, nameof(StringValueModel.Value), "Value must be equal to ACTIVE.");
await Assert.That(valid.IsValid).IsTrue();
await Assert.That(nullValue.IsValid).IsFalse();
}
[Test]
public async Task I_can_validate_not_equal_with_null_reference_values()
{
var validator = new PropertyValidator<StringValueModel, string?>(model
=> model.Value, rule => rule.NotEqual("BANNED"));
var invalid = validator.Validate(new StringValueModel { Value = "BANNED" });
var valid = validator.Validate(new StringValueModel { Value = "ALLOWED" });
var nullValue = validator.Validate(new StringValueModel());
await AssertSingleProblem(invalid, nameof(StringValueModel.Value), "Value must not be equal to BANNED.");
await Assert.That(valid.IsValid).IsTrue();
await Assert.That(nullValue.IsValid).IsTrue();
}
[Test]
public async Task I_can_skip_null_values_for_reference_greater_than()
{
var validator = new PropertyValidator<StringValueModel, string?>(model
=> model.Value, rule => rule.GreaterThan("B"));
var invalid = validator.Validate(new StringValueModel { Value = "A" });
var valid = validator.Validate(new StringValueModel { Value = "C" });
var ignoredNull = validator.Validate(new StringValueModel());
await AssertSingleProblem(invalid, nameof(StringValueModel.Value), "Value must be greater than B.");
await Assert.That(valid.IsValid).IsTrue();
await Assert.That(ignoredNull.IsValid).IsTrue();
}
[Test]
public async Task I_can_skip_null_values_for_reference_between()
{
var validator = new PropertyValidator<StringValueModel, string?>(model
=> model.Value, rule => rule.Between("A", "C"));
var invalid = validator.Validate(new StringValueModel { Value = "D" });
var valid = validator.Validate(new StringValueModel { Value = "C" });
var ignoredNull = validator.Validate(new StringValueModel());
await AssertSingleProblem(invalid, nameof(StringValueModel.Value), "Value must be between A and C.");
await Assert.That(valid.IsValid).IsTrue();
await Assert.That(ignoredNull.IsValid).IsTrue();
}
[Test]
public async Task I_can_validate_matches_with_a_pattern()
{
var validator = new PropertyValidator<StringValueModel, string?>(model
=> model.Value, rule => rule.Matches("^[A-Z]+$"));
var invalid = validator.Validate(new StringValueModel { Value = "Abc" });
var valid = validator.Validate(new StringValueModel { Value = "ABC" });
var ignoredNull = validator.Validate(new StringValueModel());
await AssertSingleProblem(invalid, nameof(StringValueModel.Value), "Value is not in the correct format.");
await Assert.That(valid.IsValid).IsTrue();
await Assert.That(ignoredNull.IsValid).IsTrue();
}
[Test]
public async Task I_can_validate_matches_with_a_regex()
{
var validator = new PropertyValidator<StringValueModel, string?>(model
=> model.Value, rule => rule.Matches(new Regex(@"^\d{4}$", RegexOptions.CultureInvariant)));
var invalid = validator.Validate(new StringValueModel { Value = "12AB" });
var valid = validator.Validate(new StringValueModel { Value = "1234" });
var ignoredNull = validator.Validate(new StringValueModel());
await AssertSingleProblem(invalid, nameof(StringValueModel.Value), "Value is not in the correct format.");
await Assert.That(valid.IsValid).IsTrue();
await Assert.That(ignoredNull.IsValid).IsTrue();
}
[Test]
public async Task I_see_it_throw_for_invalid_length_configuration()
{
await Assert.That(() => new PropertyValidator<StringValueModel, string?>(model
=> model.Value, rule => rule.Length(5, 4)))
.Throws<ArgumentOutOfRangeException>();
}
[Test]
public async Task I_see_it_throw_for_negative_min_length_configuration()
{
await Assert.That(() => new PropertyValidator<StringValueModel, string?>(model
=> model.Value, rule => rule.MinLength(-1)))
.Throws<ArgumentOutOfRangeException>();
}
[Test]
public async Task I_see_it_throw_for_invalid_between_configuration()
{
await Assert.That(() => new PropertyValidator<IntValueModel, int>(model
=> model.Value, rule => rule.Between(10, 0)))
.Throws<ArgumentOutOfRangeException>();
}
private static async Task AssertSingleProblem(Validation validation, string propertyName, string message)
{
await Assert.That(validation.Problems).Count().IsEqualTo(1);
var problem = validation.Problems.Single();
using (Assert.Multiple())
{
await Assert.That(problem.PropertyName).IsEqualTo(propertyName);
await Assert.That(problem.Message).IsEqualTo(message);
}
}
}

View file

@ -0,0 +1,279 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using Microsoft.Extensions.DependencyInjection;
namespace Geekeey.Request.Validation.Tests;
internal sealed class ValidatorTests
{
[Test]
public async Task I_can_validate_a_property_rule_with_metadata()
{
var validator = CreatePersonValidator();
var result = validator.Validate(new Person { Name = "" });
await Assert.That(result.IsValid).IsFalse();
await Assert.That(result.Problems).Count().IsEqualTo(1);
var problem = result.Problems.Single();
using (Assert.Multiple())
{
await Assert.That(problem.PropertyName).IsEqualTo(nameof(Person.Name));
await Assert.That(problem.Message).IsEqualTo("Name is required.");
await Assert.That(problem.Code).IsEqualTo("NAME_REQUIRED");
await Assert.That(problem.Severity).IsEqualTo(Severity.Warning);
await Assert.That(problem.AttemptedValue).IsEqualTo("");
}
}
[Test]
public async Task I_validate_all_rules_on_every_call()
{
var validator = CreatePersonValidator();
var invalid = validator.Validate(new Person { Name = "" });
var valid = validator.Validate(new Person { Name = "Louis" });
using (Assert.Multiple())
{
await Assert.That(invalid.IsValid).IsFalse();
await Assert.That(valid.IsValid).IsTrue();
await Assert.That(valid.Problems).IsEmpty();
}
}
[Test]
public async Task I_can_compose_nested_validators_and_aggregate_property_paths()
{
var validator = new PropertyValidator<PersonWithAddress, Address?>(
person => person.Address,
rule => rule.SetValidator(CreateAddressValidator()));
var result = validator.Validate(new PersonWithAddress
{
Address = new Address { Street = "" },
});
await Assert.That(result.Problems).Count().IsEqualTo(1);
await Assert.That(result.Problems.Single().PropertyName).IsEqualTo("Address.Street");
}
[Test]
public async Task I_skip_null_nested_values_for_composed_validators()
{
var validator = new PropertyValidator<PersonWithAddress, Address?>(
person => person.Address,
rule => rule.SetValidator(CreateAddressValidator()));
var result = validator.Validate(new PersonWithAddress());
await Assert.That(result.IsValid).IsTrue();
await Assert.That(result.Problems).IsEmpty();
}
[Test]
public async Task I_can_validate_each_collection_element_and_include_the_index()
{
var validator = new CollectionValidator<Team, Member>(
team => team.Members,
rule => rule.SetValidator(CreateMemberValidator()));
var result = validator.Validate(new Team
{
Members = [new Member { Name = "Ada" }, new Member { Name = "" }],
});
await Assert.That(result.Problems).Count().IsEqualTo(1);
await Assert.That(result.Problems.Single().PropertyName).IsEqualTo("Members[1].Name");
}
[Test]
public async Task I_can_build_property_paths_for_nested_member_access()
{
var validator = new PropertyValidator<PersonWithAddress, string?>(
person => person.Address!.Street,
rule => rule.NotEmpty());
var result = validator.Validate(new PersonWithAddress { Address = new Address { Street = "" } });
await Assert.That(result.Problems).Count().IsEqualTo(1);
var problem = result.Problems.Single();
using (Assert.Multiple())
{
await Assert.That(problem.PropertyName).IsEqualTo("Address.Street");
await Assert.That(problem.Message).IsEqualTo("Value is required.");
}
}
[Test]
public async Task I_can_build_property_paths_for_converted_expressions()
{
var validator = new PropertyValidator<Person, object?>(
person => person.Name,
rule => rule.NotNull());
var result = validator.Validate(new Person());
await Assert.That(result.Problems).Count().IsEqualTo(1);
var problem = result.Problems.Single();
using (Assert.Multiple())
{
await Assert.That(problem.PropertyName).IsEqualTo(nameof(Person.Name));
await Assert.That(problem.Message).IsEqualTo("Value is required.");
}
}
[Test]
public async Task I_see_it_throw_for_non_member_expressions()
{
await Assert.That(() => new PropertyValidator<Person, int>(
person => person.Name!.Trim().Length,
rule => rule.GreaterThan(0)))
.Throws<InvalidOperationException>();
}
[Test]
public async Task I_aggregate_all_failures_in_rule_order()
{
var validator = new OrderedFailuresValidator();
var result = validator.Validate(new StringValueModel());
await Assert.That(result.Problems).Count().IsEqualTo(3);
await Assert.That(result.Problems[0].Message).IsEqualTo("First failure.");
await Assert.That(result.Problems[1].Message).IsEqualTo("Second failure.");
await Assert.That(result.Problems[2].Message).IsEqualTo("Third failure.");
}
[Test]
public async Task I_apply_default_problem_metadata()
{
var validator = new PropertyValidator<StringValueModel, string?>(
model => model.Value,
rule => rule.NotEmpty());
var result = validator.Validate(new StringValueModel { Value = " " });
var problem = result.Problems.Single();
using (Assert.Multiple())
{
await Assert.That(problem.Code).IsNull();
await Assert.That(problem.Severity).IsEqualTo(Severity.Error);
await Assert.That(problem.AttemptedValue).IsEqualTo(" ");
}
}
[Test]
public async Task I_skip_null_collections_for_collection_validators()
{
var validator = new CollectionValidator<NullableTeam, Member>(
team => team.Members!,
rule => rule.SetValidator(CreateMemberValidator()));
var result = validator.Validate(new NullableTeam());
await Assert.That(result.IsValid).IsTrue();
await Assert.That(result.Problems).IsEmpty();
}
[Test]
public async Task I_skip_empty_collections_for_collection_validators()
{
var validator = new CollectionValidator<Team, Member>(
team => team.Members,
rule => rule.SetValidator(CreateMemberValidator()));
var result = validator.Validate(new Team());
await Assert.That(result.IsValid).IsTrue();
await Assert.That(result.Problems).IsEmpty();
}
[Test]
public async Task I_can_aggregate_problems_for_multiple_invalid_collection_elements()
{
var validator = new CollectionValidator<Team, Member>(
team => team.Members,
rule => rule.SetValidator(CreateMemberValidator()));
var result = validator.Validate(new Team
{
Members = [new Member { Name = "" }, new Member { Name = "Ada" }, new Member { Name = "" }],
});
await Assert.That(result.Problems).Count().IsEqualTo(2);
await Assert.That(result.Problems[0].PropertyName).IsEqualTo("Members[0].Name");
await Assert.That(result.Problems[1].PropertyName).IsEqualTo("Members[2].Name");
}
[Test]
public async Task I_can_resolve_nested_validators_from_the_context_service_provider()
{
var addressValidator = CreateAddressValidator();
var services = new ServiceCollection()
.AddSingleton(addressValidator)
.BuildServiceProvider();
var validator = new PropertyValidator<PersonWithAddress, Address?>(
person => person.Address,
rule => rule.SetValidator<PropertyValidator<Address, string?>>());
var context = new ValidationContext<PersonWithAddress>(
new PersonWithAddress { Address = new Address { Street = "" } },
services);
var result = validator.Validate(context);
await Assert.That(result.Problems).Count().IsEqualTo(1);
await Assert.That(result.Problems.Single().PropertyName).IsEqualTo("Address.Street");
}
[Test]
public async Task I_see_it_throw_for_type_mismatch()
{
var validator = CreatePersonValidator();
var context = new ValidationContext<object>(new object());
await Assert.That(() => validator.Validate(context)).Throws<InvalidOperationException>();
}
[Test]
public async Task I_see_it_throw_when_a_context_resolved_validator_cannot_be_resolved()
{
var validator = new PropertyValidator<PersonWithAddress, Address?>(
person => person.Address,
rule => rule.SetValidator<PropertyValidator<Address, string?>>());
var context = new ValidationContext<PersonWithAddress>(
new PersonWithAddress { Address = new Address { Street = "" } });
await Assert.That(() => validator.Validate(context)).Throws<InvalidOperationException>();
}
private static PropertyValidator<Person, string?> CreatePersonValidator()
{
return new PropertyValidator<Person, string?>(
person => person.Name,
rule => rule.Must(name => !string.IsNullOrWhiteSpace(name), "Name is required.")
.WithCode("NAME_REQUIRED")
.WithSeverity(Severity.Warning));
}
private static PropertyValidator<Address, string?> CreateAddressValidator()
{
return new PropertyValidator<Address, string?>(
address => address.Street,
rule => rule.Must(street => !string.IsNullOrWhiteSpace(street), "Street is required."));
}
private static PropertyValidator<Member, string?> CreateMemberValidator()
{
return new PropertyValidator<Member, string?>(
member => member.Name,
rule => rule.Must(name => !string.IsNullOrWhiteSpace(name), "Member name is required."));
}
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Validation.Tests;
internal sealed class Address
{
public string? Street { get; init; }
}

View file

@ -0,0 +1,19 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Linq.Expressions;
namespace Geekeey.Request.Validation.Tests;
internal sealed class CollectionValidator<T, TElement> : Validator<T>
{
public CollectionValidator(
Expression<Func<T, IEnumerable<TElement>>> expression,
Action<IPropertyRuleBuilder<T, TElement>> configure)
{
ArgumentNullException.ThrowIfNull(expression);
ArgumentNullException.ThrowIfNull(configure);
configure(RuleForEach(expression));
}
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Validation.Tests;
internal sealed class CollectionValueModel
{
public IEnumerable<string>? Value { get; init; }
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Validation.Tests;
internal sealed class IntValueModel
{
public int Value { get; init; }
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Validation.Tests;
internal sealed class Member
{
public string? Name { get; init; }
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Validation.Tests;
internal sealed class NullableIntValueModel
{
public int? Value { get; init; }
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Validation.Tests;
internal sealed class NullableTeam
{
public IEnumerable<Member>? Members { get; init; }
}

View file

@ -0,0 +1,17 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Validation.Tests;
internal sealed class OrderedFailuresValidator : Validator<StringValueModel>
{
public OrderedFailuresValidator()
{
RuleFor(model => model.Value)
.Must(_ => false, "First failure.")
.Must(_ => false, "Second failure.");
RuleFor(model => model.Value)
.Must(_ => false, "Third failure.");
}
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Validation.Tests;
internal sealed class Person
{
public string? Name { get; init; }
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Validation.Tests;
internal sealed class PersonWithAddress
{
public Address? Address { get; init; }
}

View file

@ -0,0 +1,19 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Linq.Expressions;
namespace Geekeey.Request.Validation.Tests;
internal sealed class PropertyValidator<T, TProperty> : Validator<T>
{
public PropertyValidator(
Expression<Func<T, TProperty>> expression,
Action<IPropertyRuleBuilder<T, TProperty>> configure)
{
ArgumentNullException.ThrowIfNull(expression);
ArgumentNullException.ThrowIfNull(configure);
configure(RuleFor(expression));
}
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Validation.Tests;
internal sealed class ReferenceValueModel
{
public object? Value { get; init; }
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Validation.Tests;
internal sealed class StringValueModel
{
public string? Value { get; init; }
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Validation.Tests;
internal sealed class Team
{
public IEnumerable<Member> Members { get; init; } = [];
}

View file

@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>true</IsPackable>
</PropertyGroup>
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
<InternalsVisibleTo Include="Geekeey.Request.Validation.Tests" />
</ItemGroup>
<PropertyGroup>
<PackageReadmeFile>package-readme.md</PackageReadmeFile>
<PackageIcon>package-icon.png</PackageIcon>
<PackageProjectUrl>https://code.geekeey.de/geekeey/request/src/branch/main/src/request.validation</PackageProjectUrl>
<PackageLicenseExpression>EUPL-1.2</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<None Include=".\package-icon.png" Pack="true" PackagePath="\" Visible="false" />
<None Include=".\package-readme.md" Pack="true" PackagePath="\" Visible="false" />
<None Include="..\..\LICENSE.md" Pack="true" PackagePath="\" Visible="false" />
</ItemGroup>
</Project>

Some files were not shown because too many files have changed in this diff Show more