diff --git a/CHANGELOG.md b/CHANGELOG.md index 240c251..5c26b92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,42 +12,13 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - **request.result:** Optional result wrapper types (`Result`) for structured responses - **request.validation:** Automatic validation pipeline behavior with FluentValidation integration -## [1.1.0] - 2026-05-28 - -### Added - -- **request.validation:** Dependency injection support for validators via `IServiceCollection.AddValidation()` -- **request.validation:** `IValidatorBuilder` for fluent validator registration and assembly scanning -- **request.validation:** Support for open generic validators and automatic closing during resolution -- **request.validation:** `Validate` extension method for simplified validator invocation - -### Changed - -- **request.dispatcher:** Reset type caches when reloading assemblies -- **request.validation:** Reset type caches when reloading assemblies - -## [2.0.0] - 2026-05-30 - -## Breaking changes - -To have a consistent experience across all packages, some public interfaces have been renamed. - -### Changed - -- **request.dispatcher:** Hide pipeline internals in stack frames -- **request.validation:** Rename `IValidatorBuilder` to `IRequestValidatorBuilder` incl. extensions methods - ## [Unreleased] ### Added -- **request.validation:** Support `PropertyPath` JSON converter for string values and dictionary property names - ### Changed ### Removed [1.0.0]: https://code.geekeey.de/geekeey/request/releases/tag/1.0.0 -[1.1.0]: https://code.geekeey.de/geekeey/request/releases/tag/1.1.0 -[2.0.0]: https://code.geekeey.de/geekeey/request/releases/tag/2.0.0 -[Unreleased]: https://code.geekeey.de/geekeey/request/compare/2.0.0...HEAD +[Unreleased]: https://code.geekeey.de/geekeey/request/compare/1.0.0...HEAD diff --git a/Directory.Build.props b/Directory.Build.props index 86d2794..0a0e05e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,8 +9,7 @@ - 2.1.0 - preview + 1.0.0 diff --git a/src/request.dispatcher.tests/StackTraceTests.cs b/src/request.dispatcher.tests/StackTraceTests.cs deleted file mode 100644 index 636dfb6..0000000 --- a/src/request.dispatcher.tests/StackTraceTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) The Geekeey Authors -// SPDX-License-Identifier: EUPL-1.2 - -using Microsoft.Extensions.DependencyInjection; - -namespace Geekeey.Request.Dispatcher.Tests; - -internal sealed class StackTraceTests -{ - [Test] - public async Task I_can_not_see_pipeline_internals_are_hidden_from_stack_trace_scalar() - { - // Arrange - var sc = new ServiceCollection(); - sc.AddSingleton(); - sc.AddRequestDispatcher(builder => builder - .Add(typeof(FailingScalarHandler)) - .Add(typeof(ScalarOpenBehavior<,>))); - - var provider = sc.BuildServiceProvider(); - var dispatcher = provider.GetRequiredService(); - var request = new FailingScalarRequest(); - - // Act - var exception = await Assert.ThrowsAsync(() => - dispatcher.DispatchAsync(request)); - - // Assert - await Assert.That(exception).IsNotNull(); - - var stackTrace = await Assert.That(exception.StackTrace).IsNotNull(); - - await Assert.That(stackTrace).Contains(nameof(FailingScalarHandler)); - await Assert.That(stackTrace).Contains(nameof(ScalarOpenBehavior<,>)); - - // 3. Verify that the internal lambda from ScalarRequestInvoker.Chain is HIDDEN. - // In C#, these lambdas usually appear as "ScalarRequestInvoker`2.<>c__DisplayClass..." or similar. - // Since we added [StackTraceHidden], this frame should be omitted. - await Assert.That(stackTrace).DoesNotContain("ScalarRequestInvoker+<>"); - } - - [Test] - public async Task I_can_not_see_pipeline_internals_are_hidden_from_stack_trace_stream() - { - // Arrange - var sc = new ServiceCollection(); - sc.AddSingleton(); - sc.AddRequestDispatcher(builder => builder - .Add(typeof(FailingStreamHandler)) - .Add(typeof(StreamOpenBehavior<,>))); - - var provider = sc.BuildServiceProvider(); - var dispatcher = provider.GetRequiredService(); - var request = new FailingStreamRequest(); - - // Act - var exception = await Assert.ThrowsAsync(() => - dispatcher.DispatchAsync(request).ToListAsync().AsTask()); - - // Assert - await Assert.That(exception).IsNotNull(); - - var stackTrace = await Assert.That(exception.StackTrace).IsNotNull(); - - await Assert.That(stackTrace).Contains(nameof(FailingStreamHandler)); - await Assert.That(stackTrace).Contains(nameof(StreamOpenBehavior<,>)); - - // 3. Verify that the internal lambda from ScalarRequestInvoker.Chain is HIDDEN. - // In C#, these lambdas usually appear as "ScalarRequestInvoker`2.<>c__DisplayClass..." or similar. - // Since we added [StackTraceHidden], this frame should be omitted. - await Assert.That(stackTrace).DoesNotContain("StreamRequestInvoker+<>"); - } -} diff --git a/src/request.dispatcher.tests/_fixtures/StreamOpenBehavior.cs b/src/request.dispatcher.tests/_fixtures/StreamOpenBehavior.cs index d5ded39..3d90be3 100644 --- a/src/request.dispatcher.tests/_fixtures/StreamOpenBehavior.cs +++ b/src/request.dispatcher.tests/_fixtures/StreamOpenBehavior.cs @@ -1,19 +1,14 @@ // Copyright (c) The Geekeey Authors // SPDX-License-Identifier: EUPL-1.2 -using System.Runtime.CompilerServices; - namespace Geekeey.Request.Dispatcher.Tests; public class StreamOpenBehavior(StreamTestTracker tracker) : IStreamRequestBehavior where TRequest : IStreamRequest { - public async IAsyncEnumerable HandleAsync(TRequest request, StreamHandlerDelegate next, [EnumeratorCancellation] CancellationToken cancellationToken) + public IAsyncEnumerable HandleAsync(TRequest request, StreamHandlerDelegate next, CancellationToken cancellationToken) { tracker.Executed = true; - await foreach (var response in next(request, cancellationToken)) - { - yield return response; - } + return next(request, cancellationToken); } } diff --git a/src/request.dispatcher/RequestDispatcher.cs b/src/request.dispatcher/RequestDispatcher.cs index 2acb426..c4083bd 100644 --- a/src/request.dispatcher/RequestDispatcher.cs +++ b/src/request.dispatcher/RequestDispatcher.cs @@ -20,12 +20,6 @@ internal sealed class RequestDispatcher : IRequestDispatcher _serviceProvider = serviceProvider; } - public static void ClearCache(Type[]? _) - { - ScalarRequestHandlers.Clear(); - StreamRequestHandlers.Clear(); - } - public Task DispatchAsync(IScalarRequest request, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); diff --git a/src/request.dispatcher/ScalarRequestInvoker.cs b/src/request.dispatcher/ScalarRequestInvoker.cs index 63de03e..feb02bf 100644 --- a/src/request.dispatcher/ScalarRequestInvoker.cs +++ b/src/request.dispatcher/ScalarRequestInvoker.cs @@ -1,8 +1,6 @@ // Copyright (c) The Geekeey Authors // SPDX-License-Identifier: EUPL-1.2 -using System.Diagnostics; - using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -38,7 +36,7 @@ internal sealed class ScalarRequestInvoker : ScalarRequestI static ScalarHandlerDelegate Chain(ScalarHandlerDelegate next, IScalarRequestBehavior filter) { - return [StackTraceHidden] (req, ct) => filter.HandleAsync((TRequest)req, next, ct); + return (req, ct) => filter.HandleAsync((TRequest)req, next, ct); } Task Head(IScalarRequest r, CancellationToken ct) diff --git a/src/request.dispatcher/StreamRequestInvoker.cs b/src/request.dispatcher/StreamRequestInvoker.cs index 882d538..6557146 100644 --- a/src/request.dispatcher/StreamRequestInvoker.cs +++ b/src/request.dispatcher/StreamRequestInvoker.cs @@ -1,7 +1,6 @@ // Copyright (c) The Geekeey Authors // SPDX-License-Identifier: EUPL-1.2 -using System.Diagnostics; using System.Runtime.CompilerServices; using Microsoft.Extensions.DependencyInjection; @@ -42,7 +41,7 @@ internal sealed class StreamRequestInvoker : StreamRequestI static StreamHandlerDelegate Chain(StreamHandlerDelegate next, IStreamRequestBehavior filter) { - return [StackTraceHidden] (req, ct) => filter.HandleAsync((TRequest)req, next, ct); + return (req, ct) => filter.HandleAsync((TRequest)req, next, ct); } IAsyncEnumerable Head(IStreamRequest r, CancellationToken ct) diff --git a/src/request.validation.tests/DependencyInjectionTests.cs b/src/request.validation.tests/DependencyInjectionTests.cs deleted file mode 100644 index ae98a7a..0000000 --- a/src/request.validation.tests/DependencyInjectionTests.cs +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright (c) The Geekeey Authors -// SPDX-License-Identifier: EUPL-1.2 - -using Microsoft.Extensions.DependencyInjection; - -namespace Geekeey.Request.Validation.Tests; - -internal sealed class DependencyInjectionTests -{ - [Test] - public async Task I_can_resolve_single_validator() - { - var services = new ServiceCollection(); - services.AddRequestValidation(builder => - { - builder.Add(ServiceLifetime.Transient); - }); - var provider = services.BuildServiceProvider(); - - var validator = provider.GetRequiredService(); - var result = validator.Validate(new StringValueModel()); - - await Assert.That(result.Problems).Count().IsEqualTo(3); - } - - [Test] - public async Task I_can_resolve_multiple_validators() - { - var services = new ServiceCollection(); - services.AddRequestValidation(builder => - { - builder.Add(ServiceLifetime.Transient); - builder.Add(ServiceLifetime.Transient); - }); - var provider = services.BuildServiceProvider(); - - var validator = provider.GetRequiredService(); - var result = validator.Validate(new Person()); - - await Assert.That(result.Problems).Count().IsEqualTo(2); - await Assert.That(result.Problems.Any(p => p.Message == "PersonValidator failure.")).IsTrue(); - await Assert.That(result.Problems.Any(p => p.Message == "AnotherPersonValidator failure.")).IsTrue(); - } - - [Test] - public async Task I_can_resolve_open_generic_validator() - { - var services = new ServiceCollection(); - services.AddRequestValidation(builder => - { - builder.Add(typeof(GenericValidator<>), ServiceLifetime.Transient); - }); - var provider = services.BuildServiceProvider(); - - var validator = provider.GetRequiredService(); - var result = validator.Validate(new Person()); - - await Assert.That(result.Problems).Count().IsEqualTo(1); - await Assert.That(result.Problems[0].Message).IsEqualTo("GenericValidator failure."); - } - - [Test] - public async Task I_can_resolve_multi_interface_validator() - { - var services = new ServiceCollection(); - services.AddRequestValidation(builder => - { - builder.Add(ServiceLifetime.Transient); - }); - var provider = services.BuildServiceProvider(); - - var validator = provider.GetRequiredService(); - - var personResult = validator.Validate(new Person()); - await Assert.That(personResult.Problems).Count().IsEqualTo(1); - await Assert.That(personResult.Problems[0].Message).IsEqualTo("MultiInterfaceValidator failure."); - - var addressResult = validator.Validate(new Address()); - await Assert.That(addressResult.Problems).Count().IsEqualTo(1); - await Assert.That(addressResult.Problems[0].Message).IsEqualTo("MultiInterfaceValidator failure."); - } - - [Test] - public async Task I_can_resolve_generic_validator_with_constraints() - { - var services = new ServiceCollection(); - services.AddRequestValidation(builder => - { - builder.Add(typeof(ConstrainedValidator<>), ServiceLifetime.Transient); - }); - var provider = services.BuildServiceProvider(); - - var validator = provider.GetRequiredService(); - - var teamResult = validator.Validate(new Team()); - await Assert.That(teamResult.Problems).Count().IsEqualTo(1); - await Assert.That(teamResult.Problems[0].Message).IsEqualTo("ConstrainedValidator failure."); - - var personResult = validator.Validate(new Person()); - await Assert.That(personResult.Problems).Count().IsEqualTo(0); - } - - [Test] - public async Task I_can_resolve_generic_wrapper_validator_with_constraints() - { - var services = new ServiceCollection(); - services.AddRequestValidation(builder => - { - builder.Add(typeof(ConstrainedWrapperValidator<>), ServiceLifetime.Transient); - }); - var provider = services.BuildServiceProvider(); - - var validator = provider.GetRequiredService(); - - var teamWrapperResult = validator.Validate(new Wrapper()); - await Assert.That(teamWrapperResult.Problems).Count().IsEqualTo(1); - await Assert.That(teamWrapperResult.Problems[0].Message).IsEqualTo("ConstrainedWrapperValidator failure."); - - var personWrapperResult = validator.Validate(new Wrapper()); - await Assert.That(personWrapperResult.Problems).Count().IsEqualTo(0); - } - - [Test] - public async Task I_can_resolve_aggregate_validator_directly() - { - var services = new ServiceCollection(); - services.AddRequestValidation(builder => - { - builder.Add(ServiceLifetime.Transient); - builder.Add(ServiceLifetime.Transient); - }); - var provider = services.BuildServiceProvider(); - - var validator = provider.GetRequiredService(); - - var person = new Person { Name = "John" }; - var result = validator.Validate(person); - await Assert.That(result.Problems).Count().IsEqualTo(2); - } - - [Test] - public async Task I_can_select_validators_from_base_classes_and_interfaces() - { - var services = new ServiceCollection(); - services.AddRequestValidation(builder => - { - builder.Add(ServiceLifetime.Transient); - builder.Add(ServiceLifetime.Transient); - builder.Add(ServiceLifetime.Transient); - }); - var provider = services.BuildServiceProvider(); - - var validator = provider.GetRequiredService(); - var dog = new Dog { Id = 0, Name = "", Breed = "" }; - var result = validator.Validate(dog); - - await Assert.That(result.Problems).Count().IsEqualTo(3); - } - - [Test] - public async Task I_can_select_validators_polymorphically_based_on_the_instance_type() - { - var services = new ServiceCollection(); - services.AddRequestValidation(builder => - { - builder.Add(ServiceLifetime.Transient); - builder.Add(ServiceLifetime.Transient); - }); - var provider = services.BuildServiceProvider(); - - var validator = provider.GetRequiredService(); - var dog = new Dog { Name = "", Breed = "" }; - var result = validator.Validate(new ValidationContext(dog)); - - await Assert.That(result.Problems).Count().IsEqualTo(2); - } -} diff --git a/src/request.validation.tests/PropertyPathTests.cs b/src/request.validation.tests/PropertyPathTests.cs index 2551450..bce4614 100644 --- a/src/request.validation.tests/PropertyPathTests.cs +++ b/src/request.validation.tests/PropertyPathTests.cs @@ -12,19 +12,6 @@ internal sealed class PropertyPathTests PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; - private static readonly JsonSerializerOptions CamelCaseDictionaryKeyJsonOptions = new() - { - DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, - }; - - [Test] - public async Task I_can_serialize_property_paths_as_json_strings_using_the_json_naming_policy() - { - var json = JsonSerializer.Serialize((PropertyPath)"Address.Street", CamelCaseJsonOptions); - - await Assert.That(json).IsEqualTo(/*lang=json,strict*/ "\"address.street\""); - } - [Test] public async Task I_can_serialize_property_paths_using_the_json_naming_policy() { @@ -67,32 +54,6 @@ internal sealed class PropertyPathTests await Assert.That(json).IsEqualTo(/*lang=json,strict*/ """{"propertyPath":"matrix[1][2].value","severity":0,"message":"Value is required.","code":null,"attemptedValue":null}"""); } - [Test] - public async Task I_can_serialize_property_paths_as_dictionary_keys_using_the_dictionary_key_policy() - { - Dictionary errors = new() - { - ["Address.Street"] = "Street is required.", - }; - - var json = JsonSerializer.Serialize(errors, CamelCaseDictionaryKeyJsonOptions); - - await Assert.That(json).IsEqualTo(/*lang=json,strict*/ """{"address.street":"Street is required."}"""); - } - - [Test] - public async Task I_can_deserialize_property_paths_from_dictionary_keys() - { - var json = /*lang=json,strict*/ """{"Address.Street":"Street is required."}"""; - - var errors = JsonSerializer.Deserialize>(json); - - await Assert.That(errors).IsNotNull(); - await Assert.That(errors!.Count).IsEqualTo(1); - await Assert.That(errors.ContainsKey("Address.Street")).IsTrue(); - await Assert.That(errors["Address.Street"]).IsEqualTo("Street is required."); - } - [Test] public async Task I_can_iterate_and_index_segments() { diff --git a/src/request.validation.tests/_fixtures/AnotherPersonValidator.cs b/src/request.validation.tests/_fixtures/AnotherPersonValidator.cs deleted file mode 100644 index be49f70..0000000 --- a/src/request.validation.tests/_fixtures/AnotherPersonValidator.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) The Geekeey Authors -// SPDX-License-Identifier: EUPL-1.2 - -namespace Geekeey.Request.Validation.Tests; - -internal sealed class AnotherPersonValidator : Validator -{ - public AnotherPersonValidator() - { - RuleFor(p => p.Name).Must(_ => false, "AnotherPersonValidator failure."); - } -} diff --git a/src/request.validation.tests/_fixtures/ConstrainedValidator.cs b/src/request.validation.tests/_fixtures/ConstrainedValidator.cs deleted file mode 100644 index 11e9833..0000000 --- a/src/request.validation.tests/_fixtures/ConstrainedValidator.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) The Geekeey Authors -// SPDX-License-Identifier: EUPL-1.2 - -namespace Geekeey.Request.Validation.Tests; - -internal sealed class ConstrainedValidator : Validator where T : ICustomConstraint -{ - public ConstrainedValidator() - { - RuleFor(x => x).Must(_ => false, "ConstrainedValidator failure."); - } -} diff --git a/src/request.validation.tests/_fixtures/ConstrainedWrapperValidator.cs b/src/request.validation.tests/_fixtures/ConstrainedWrapperValidator.cs deleted file mode 100644 index 4dd88e2..0000000 --- a/src/request.validation.tests/_fixtures/ConstrainedWrapperValidator.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) The Geekeey Authors -// SPDX-License-Identifier: EUPL-1.2 - -namespace Geekeey.Request.Validation.Tests; - -internal sealed class ConstrainedWrapperValidator : Validator> where T : ICustomConstraint -{ - public ConstrainedWrapperValidator() - { - RuleFor(x => x).Must(_ => false, "ConstrainedWrapperValidator failure."); - } -} diff --git a/src/request.validation.tests/_fixtures/GenericValidator.cs b/src/request.validation.tests/_fixtures/GenericValidator.cs deleted file mode 100644 index 6c9254e..0000000 --- a/src/request.validation.tests/_fixtures/GenericValidator.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) The Geekeey Authors -// SPDX-License-Identifier: EUPL-1.2 - -namespace Geekeey.Request.Validation.Tests; - -internal sealed class GenericValidator : Validator -{ - public GenericValidator() - { - RuleFor(x => x).Must(_ => false, "GenericValidator failure."); - } -} diff --git a/src/request.validation.tests/_fixtures/ICustomConstraint.cs b/src/request.validation.tests/_fixtures/ICustomConstraint.cs deleted file mode 100644 index 4b135a6..0000000 --- a/src/request.validation.tests/_fixtures/ICustomConstraint.cs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) The Geekeey Authors -// SPDX-License-Identifier: EUPL-1.2 - -namespace Geekeey.Request.Validation.Tests; - -internal interface ICustomConstraint { } diff --git a/src/request.validation.tests/_fixtures/MultiInterfaceValidator.cs b/src/request.validation.tests/_fixtures/MultiInterfaceValidator.cs deleted file mode 100644 index 5a1f732..0000000 --- a/src/request.validation.tests/_fixtures/MultiInterfaceValidator.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) The Geekeey Authors -// SPDX-License-Identifier: EUPL-1.2 - -namespace Geekeey.Request.Validation.Tests; - -internal sealed class MultiInterfaceValidator : IValidator, IValidator
-{ - public Validation Validate(Person instance) - { - return Validate(new ValidationContext(instance)); - } - - public Validation Validate(Address instance) - { - return Validate(new ValidationContext
(instance)); - } - - public Validation Validate(ValidationContext context) - { - if (context.Instance is Person) - { - return new Validation([new Problem { PropertyPath = new PropertyPath("Person"), Message = "MultiInterfaceValidator failure." }]); - } - - if (context.Instance is Address) - { - return new Validation([new Problem { PropertyPath = new PropertyPath("Address"), Message = "MultiInterfaceValidator failure." }]); - } - - return new Validation([]); - } -} diff --git a/src/request.validation.tests/_fixtures/PersonValidator.cs b/src/request.validation.tests/_fixtures/PersonValidator.cs deleted file mode 100644 index 788fdea..0000000 --- a/src/request.validation.tests/_fixtures/PersonValidator.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) The Geekeey Authors -// SPDX-License-Identifier: EUPL-1.2 - -namespace Geekeey.Request.Validation.Tests; - -internal sealed class PersonValidator : Validator -{ - public PersonValidator() - { - RuleFor(p => p.Name).Must(_ => false, "PersonValidator failure."); - } -} diff --git a/src/request.validation.tests/_fixtures/PolymorphicTypes.cs b/src/request.validation.tests/_fixtures/PolymorphicTypes.cs deleted file mode 100644 index fe060fa..0000000 --- a/src/request.validation.tests/_fixtures/PolymorphicTypes.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) The Geekeey Authors -// SPDX-License-Identifier: EUPL-1.2 - -namespace Geekeey.Request.Validation.Tests; - -public interface IEntity -{ - int Id { get; set; } -} - -public abstract class Animal : IEntity -{ - public int Id { get; set; } - public string? Name { get; set; } -} - -public class Dog : Animal -{ - public string? Breed { get; set; } -} - -internal sealed class EntityValidator : Validator -{ - public EntityValidator() - { - RuleFor(x => x.Id).GreaterThan(0); - } -} - -internal sealed class AnimalValidator : Validator -{ - public AnimalValidator() - { - RuleFor(x => x.Name).NotEmpty(); - } -} - -internal sealed class DogValidator : Validator -{ - public DogValidator() - { - RuleFor(x => x.Breed).NotEmpty(); - } -} diff --git a/src/request.validation.tests/_fixtures/Team.cs b/src/request.validation.tests/_fixtures/Team.cs index ce4e7b6..6b17c01 100644 --- a/src/request.validation.tests/_fixtures/Team.cs +++ b/src/request.validation.tests/_fixtures/Team.cs @@ -3,7 +3,7 @@ namespace Geekeey.Request.Validation.Tests; -internal sealed class Team : ICustomConstraint +internal sealed class Team { public IEnumerable Members { get; init; } = []; } diff --git a/src/request.validation.tests/_fixtures/Wrapper.cs b/src/request.validation.tests/_fixtures/Wrapper.cs deleted file mode 100644 index ecb0feb..0000000 --- a/src/request.validation.tests/_fixtures/Wrapper.cs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) The Geekeey Authors -// SPDX-License-Identifier: EUPL-1.2 - -namespace Geekeey.Request.Validation.Tests; - -internal sealed class Wrapper { } diff --git a/src/request.validation/DispatchingValidator.cs b/src/request.validation/DispatchingValidator.cs deleted file mode 100644 index 3c3db7f..0000000 --- a/src/request.validation/DispatchingValidator.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) The Geekeey Authors -// SPDX-License-Identifier: EUPL-1.2 - -using System.Collections.Concurrent; -using System.Reflection.Metadata; - -[assembly: MetadataUpdateHandler(typeof(Geekeey.Request.Validation.DispatchingValidator))] - -namespace Geekeey.Request.Validation; - -internal sealed partial class DispatchingValidator : IValidator -{ - private static readonly ConcurrentDictionary ValidatorsHandlers = new(); - - private readonly IServiceProvider _serviceProvider; - - public DispatchingValidator(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - - public static void ClearCache(Type[]? _) - { - ValidatorsHandlers.Clear(); - } - - public Validation Validate(ValidationContext context) - { - ArgumentNullException.ThrowIfNull(context); - - if (context.Instance is null) - { - return new Validation([]); - } - - var handler = ValidatorsHandlers.GetOrAdd(context.Instance.GetType(), static key => - { - var type = typeof(ValidatorInvoker<>).MakeGenericType(key); - return (ValidatorInvoker)Activator.CreateInstance(type)!; - }); - - return handler.Validate(context, _serviceProvider); - } -} diff --git a/src/request.validation/Geekeey.Request.Validation.csproj b/src/request.validation/Geekeey.Request.Validation.csproj index 33bea6b..098287f 100644 --- a/src/request.validation/Geekeey.Request.Validation.csproj +++ b/src/request.validation/Geekeey.Request.Validation.csproj @@ -28,9 +28,4 @@ - - - - - diff --git a/src/request.validation/IRequestValidatorBuilder.cs b/src/request.validation/IRequestValidatorBuilder.cs deleted file mode 100644 index f699fbc..0000000 --- a/src/request.validation/IRequestValidatorBuilder.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) The Geekeey Authors -// SPDX-License-Identifier: EUPL-1.2 - -using Microsoft.Extensions.DependencyInjection; - -namespace Geekeey.Request.Validation; - -/// -/// Defines a builder for configuring validator registrations. -/// -public interface IRequestValidatorBuilder -{ - /// - /// Gets the service collection where the validators are registered. - /// - IServiceCollection Services { get; } -} diff --git a/src/request.validation/PropertyPath.cs b/src/request.validation/PropertyPath.cs index 4b9bd50..4bf0e62 100644 --- a/src/request.validation/PropertyPath.cs +++ b/src/request.validation/PropertyPath.cs @@ -399,26 +399,9 @@ internal sealed class PropertyPathJsonConverter : JsonConverter throw new JsonException($"Expected {nameof(JsonTokenType.String)} but got {reader.TokenType}."); } - /// - public override PropertyPath ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType is JsonTokenType.PropertyName) - { - return new PropertyPath(reader.GetString()!); - } - - throw new JsonException($"Expected {nameof(JsonTokenType.PropertyName)} but got {reader.TokenType}."); - } - /// public override void Write(Utf8JsonWriter writer, PropertyPath value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToJsonName(options.PropertyNamingPolicy)); } - - /// - public override void WriteAsPropertyName(Utf8JsonWriter writer, PropertyPath value, JsonSerializerOptions options) - { - writer.WritePropertyName(value.ToJsonName(options.DictionaryKeyPolicy)); - } } diff --git a/src/request.validation/RequestValidatorBuilderExtensions.cs b/src/request.validation/RequestValidatorBuilderExtensions.cs deleted file mode 100644 index 054da7c..0000000 --- a/src/request.validation/RequestValidatorBuilderExtensions.cs +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright (c) The Geekeey Authors -// SPDX-License-Identifier: EUPL-1.2 - -using System.Reflection; - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -using static Geekeey.Request.Validation.RequestValidatorOptions; - -namespace Geekeey.Request.Validation; - -/// -/// Provides extension methods for configuring -/// with additional capabilities such as searching and registering validators in assemblies or adding types directly. -/// -public static class RequestValidatorBuilderExtensions -{ - /// - /// Searches for validator types within the specified assembly and adds them to the validator - /// configuration. - /// - /// The to configure. - /// The assembly to search for validator types. - /// The instance for further configuration. - public static IRequestValidatorBuilder SearchInAssembly(this IRequestValidatorBuilder builder, Assembly assembly) - { - ArgumentNullException.ThrowIfNull(builder); - - var exports = assembly.GetTypes() - .Where(type => type is { IsClass: true, IsAbstract: false }) - .Where(IsValidatorImplementationType); - - builder.Add(exports); - - return builder; - } - - /// - /// Searches for validator types within the specified assembly and adds them to the validator - /// configuration with the given service lifetime. - /// - /// The to configure. - /// The assembly to search for validator types. - /// The lifetime with which the validators are registered in the dependency injection container. - /// The instance for further configuration. - public static IRequestValidatorBuilder SearchInAssembly(this IRequestValidatorBuilder builder, Assembly assembly, ServiceLifetime lifetime) - { - ArgumentNullException.ThrowIfNull(builder); - - var exports = assembly.GetTypes() - .Where(type => type is { IsClass: true, IsAbstract: false }) - .Where(IsValidatorImplementationType); - - builder.Add(exports, lifetime); - - return builder; - } - - /// - /// Adds the specified type to the validator configuration for inspection. - /// - /// The to configure. - /// The type to be added to the validator configuration. - /// The instance for further configuration. - public static IRequestValidatorBuilder Add(this IRequestValidatorBuilder builder, Type type) - { - ArgumentNullException.ThrowIfNull(builder); - - builder.Services.AddOptions() - .Configure(options => options.Inspect([type])); - - return builder; - } - - /// - /// Adds the specified type to the validator configuration. - /// This also adds the type to the service collection with the specified lifetime, - /// allowing it to be resolved as a dependency. - /// - /// The used to configure the validators. - /// The type to be added to the validator configuration. - /// The lifetime scope of the type in the service container. - /// The instance for further configuration. - public static IRequestValidatorBuilder Add(this IRequestValidatorBuilder builder, Type type, ServiceLifetime lifetime) - { - ArgumentNullException.ThrowIfNull(builder); - - builder.Services.AddOptions() - .Configure(options => options.Inspect([type])); - - builder.Services.Add(new ServiceDescriptor(type, type, lifetime)); - - return builder; - } - - /// - /// Adds the specified collection of types to the validator configuration for inspection. - /// - /// The to configure. - /// The collection of types to be added to the validator configuration. - /// The instance for further configuration. - public static IRequestValidatorBuilder Add(this IRequestValidatorBuilder builder, IEnumerable types) - { - ArgumentNullException.ThrowIfNull(builder); - - var typeList = types.ToList(); - - builder.Services.AddOptions() - .Configure(options => options.Inspect(typeList)); - - return builder; - } - - /// - /// Adds the specified collection of types to the validator configuration for inspection. - /// This also adds the specified collection of types to the service collection with the specified lifetime, - /// allowing it to be resolved as a dependency. - /// - /// The to configure. - /// The collection of types to be added to the validator configuration. - /// The lifetime scope of the types in the service container. - /// The instance for further configuration. - public static IRequestValidatorBuilder Add(this IRequestValidatorBuilder builder, IEnumerable types, ServiceLifetime lifetime) - { - ArgumentNullException.ThrowIfNull(builder); - - var typeList = types.ToList(); - - builder.Services.AddOptions() - .Configure(options => options.Inspect(typeList)); - - builder.Services.Add(typeList.Select(export => new ServiceDescriptor(export, export, lifetime))); - - return builder; - } - - /// - /// Adds the specified validator type to the validator configuration. - /// - /// The type of the validator to add. - /// The to configure. - /// The instance for further configuration. - public static IRequestValidatorBuilder Add(this IRequestValidatorBuilder builder) - where TValidator : class, IValidator - { - return builder.Add(typeof(TValidator)); - } - - /// - /// Adds the specified validator type to the validator configuration with the specified lifetime. - /// - /// The type of the validator to add. - /// The to configure. - /// The lifetime scope of the validator in the service container. - /// The instance for further configuration. - public static IRequestValidatorBuilder Add(this IRequestValidatorBuilder builder, ServiceLifetime lifetime) - where TValidator : class, IValidator - { - return builder.Add(typeof(TValidator), lifetime); - } - - private static bool IsValidatorImplementationType(Type type) - { - return type.GetInterfaces().Any(IsValidatorType); - } -} diff --git a/src/request.validation/RequestValidatorOptions.cs b/src/request.validation/RequestValidatorOptions.cs deleted file mode 100644 index d7c5d75..0000000 --- a/src/request.validation/RequestValidatorOptions.cs +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) The Geekeey Authors -// SPDX-License-Identifier: EUPL-1.2 - -using System.Collections; -using System.Collections.Concurrent; - -using Microsoft.Extensions.DependencyInjection; - -namespace Geekeey.Request.Validation; - -internal sealed class RequestValidatorOptions -{ - private readonly List _search = []; - private readonly Lazy _validatorsTypeIndex; - - public RequestValidatorOptions() - { - _validatorsTypeIndex = new Lazy(() => new ValidatorTypeIndex(_search.Distinct())); - } - - public void Inspect(IEnumerable assembly) - { - if (_validatorsTypeIndex.IsValueCreated) - { - throw new InvalidOperationException("The type index has already been created. Cannot inspect new assemblies."); - } - - _search.AddRange(assembly); - } - - public IEnumerable> GetValidators(IServiceProvider services) - { - return _validatorsTypeIndex.Value.Resolve>(services); - } - - private abstract class TypeIndex - { - private readonly ConcurrentDictionary> _cache = new(); - - protected readonly Dictionary> _closedTypeInfo = []; - protected readonly List _openTypeInfo = []; - - protected TypeIndex(IEnumerable collection, Func predicate) - { - foreach (var type in collection) - { - if (type.IsGenericTypeDefinition) - { - if (type.GetInterfaces().Any(predicate)) - { - _openTypeInfo.Add(type); - } - } - else - { - foreach (var @interface in type.GetInterfaces().Where(predicate)) - { - (_closedTypeInfo.TryGetValue(@interface, out var list) ? list : _closedTypeInfo[@interface] = []).Add(type); - } - } - } - } - - public IEnumerable Resolve(IServiceProvider services) - { - return (IEnumerable)_cache.GetOrAdd(typeof(T), CreateResolverFactory)(services); - } - - protected abstract IReadOnlyList IsAssignableTo(Type type); - - private Func> CreateResolverFactory(Type @interface) - { - var list = IsAssignableTo(@interface); - return ResolverFactory; - - IEnumerable ResolverFactory(IServiceProvider services) - { - foreach (var type in list) - { - yield return (T)ActivatorUtilities.GetServiceOrCreateInstance(services, type); - } - } - } - } - - internal static bool IsValidatorType(Type type) - { - return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IValidator<>); - } - - private sealed class ValidatorTypeIndex(IEnumerable collection) - : TypeIndex(collection, IsValidatorType) - { - protected override IReadOnlyList IsAssignableTo(Type @interface) - { - var result = new List(); - - foreach (var kvp in _closedTypeInfo) - { - if (@interface.IsAssignableFrom(kvp.Key)) - { - result.AddRange(kvp.Value); - } - } - - var validatedType = @interface.GetGenericArguments()[0]; - - foreach (var type in _openTypeInfo) - { - try - { - // open type case one: Validator : IValidator - // We try to close it with the validated type. - var impl = type.MakeGenericType(validatedType); - if (impl.IsAssignableTo(@interface)) - { - result.Add(impl); - } - } - catch (ArgumentException) - { - } - - try - { - // open type case two: Validator : IValidator> - // If the validated type is generic, we try to close the validator with the validated type's generic arguments. - if (validatedType.IsGenericType) - { - var impl = type.MakeGenericType(validatedType.GetGenericArguments()); - if (impl.IsAssignableTo(@interface)) - { - result.Add(impl); - } - } - } - catch (ArgumentException) - { - } - } - - return result; - } - } -} diff --git a/src/request.validation/ServiceCollectionExtensions.cs b/src/request.validation/ServiceCollectionExtensions.cs deleted file mode 100644 index 52b47bf..0000000 --- a/src/request.validation/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) The Geekeey Authors -// SPDX-License-Identifier: EUPL-1.2 - -using Microsoft.Extensions.DependencyInjection; - -namespace Geekeey.Request.Validation; - -/// -/// Provides extension methods for configuring and registering validator services in the . -/// -public static class ServiceCollectionExtensions -{ - /// - /// Adds validator services to the specified . - /// - /// The service collection to which the validator services will be added. - /// An instance of to configure the validator registrations. - public static IRequestValidatorBuilder AddRequestValidation(this IServiceCollection services) - { - ArgumentNullException.ThrowIfNull(services); - - services.AddOptions(); - services.AddTransient(); - - return new RequestValidatorBuilder(services); - } - - /// - /// Adds validator services to the specified - /// and configures them using the provided . - /// - /// The service collection to which the validator services will be added. - /// A delegate to configure the validator builder. - /// The service collection with the validator services added. - public static IServiceCollection AddRequestValidation(this IServiceCollection services, Action configure) - { - ArgumentNullException.ThrowIfNull(services); - ArgumentNullException.ThrowIfNull(configure); - - configure(services.AddRequestValidation()); - - return services; - } - - private sealed class RequestValidatorBuilder(IServiceCollection services) : IRequestValidatorBuilder - { - public IServiceCollection Services { get; } = services; - } -} diff --git a/src/request.validation/ValidatorExtensions.cs b/src/request.validation/ValidatorExtensions.cs deleted file mode 100644 index 7f0c69c..0000000 --- a/src/request.validation/ValidatorExtensions.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) The Geekeey Authors -// SPDX-License-Identifier: EUPL-1.2 - -namespace Geekeey.Request.Validation; - -/// -/// Extension methods for . -/// -public static class ValidatorExtensions -{ - /// - /// Executes the validation logic for a specified instance of type and returns the validation result. - /// - /// The instance used to perform the validation. - /// The instance of type to validate. - /// The service provider available for nested validator resolution. - /// Per-call state shared across nested validation operations. - /// A object containing the results of the validation, including any problems encountered. - public static Validation Validate(this IValidator validator, T instance, IServiceProvider? serviceProvider = null, IReadOnlyDictionary? items = null) - { - return validator.Validate(new ValidationContext(instance, serviceProvider, items)); - } -} diff --git a/src/request.validation/ValidatorInvoker.cs b/src/request.validation/ValidatorInvoker.cs deleted file mode 100644 index ad41030..0000000 --- a/src/request.validation/ValidatorInvoker.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) The Geekeey Authors -// SPDX-License-Identifier: EUPL-1.2 - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; - -namespace Geekeey.Request.Validation; - -internal abstract class ValidatorInvoker -{ - public abstract Validation Validate(ValidationContext context, IServiceProvider serviceProvider); -} - -internal sealed class ValidatorInvoker : ValidatorInvoker -{ - public override Validation Validate(ValidationContext context, IServiceProvider serviceProvider) - { - var options = serviceProvider.GetRequiredService>().Value; - - var validators = options.GetValidators(serviceProvider); - - var problems = new List(); - - foreach (var validator in validators) - { - if (validator.Validate(context) is { IsValid: false, Problems: { } result }) - { - problems.AddRange(result); - } - } - - return new Validation(problems); - } -}