Compare commits
No commits in common. "HEAD" and "1.1.0" have entirely different histories.
15 changed files with 87 additions and 243 deletions
16
CHANGELOG.md
16
CHANGELOG.md
|
|
@ -26,28 +26,14 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
||||||
- **request.dispatcher:** Reset type caches when reloading assemblies
|
- **request.dispatcher:** Reset type caches when reloading assemblies
|
||||||
- **request.validation:** 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]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- **request.validation:** Support `PropertyPath` JSON converter for string values and dictionary property names
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
[1.0.0]: https://code.geekeey.de/geekeey/request/releases/tag/1.0.0
|
[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
|
[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/1.1.0...HEAD
|
||||||
[Unreleased]: https://code.geekeey.de/geekeey/request/compare/2.0.0...HEAD
|
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<VersionPrefix>2.1.0</VersionPrefix>
|
<VersionPrefix>1.1.0</VersionPrefix>
|
||||||
<VersionSuffix>preview</VersionSuffix>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|
|
||||||
|
|
@ -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<ScalarTestTracker>();
|
|
||||||
sc.AddRequestDispatcher(builder => builder
|
|
||||||
.Add(typeof(FailingScalarHandler))
|
|
||||||
.Add(typeof(ScalarOpenBehavior<,>)));
|
|
||||||
|
|
||||||
var provider = sc.BuildServiceProvider();
|
|
||||||
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
|
|
||||||
var request = new FailingScalarRequest();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
|
||||||
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<StreamTestTracker>();
|
|
||||||
sc.AddRequestDispatcher(builder => builder
|
|
||||||
.Add(typeof(FailingStreamHandler))
|
|
||||||
.Add(typeof(StreamOpenBehavior<,>)));
|
|
||||||
|
|
||||||
var provider = sc.BuildServiceProvider();
|
|
||||||
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
|
|
||||||
var request = new FailingStreamRequest();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
|
||||||
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+<>");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +1,14 @@
|
||||||
// Copyright (c) The Geekeey Authors
|
// Copyright (c) The Geekeey Authors
|
||||||
// SPDX-License-Identifier: EUPL-1.2
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
namespace Geekeey.Request.Dispatcher.Tests;
|
namespace Geekeey.Request.Dispatcher.Tests;
|
||||||
|
|
||||||
public class StreamOpenBehavior<TRequest, TResponse>(StreamTestTracker tracker) : IStreamRequestBehavior<TRequest, TResponse>
|
public class StreamOpenBehavior<TRequest, TResponse>(StreamTestTracker tracker) : IStreamRequestBehavior<TRequest, TResponse>
|
||||||
where TRequest : IStreamRequest<TResponse>
|
where TRequest : IStreamRequest<TResponse>
|
||||||
{
|
{
|
||||||
public async IAsyncEnumerable<TResponse> HandleAsync(TRequest request, StreamHandlerDelegate<TResponse> next, [EnumeratorCancellation] CancellationToken cancellationToken)
|
public IAsyncEnumerable<TResponse> HandleAsync(TRequest request, StreamHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
tracker.Executed = true;
|
tracker.Executed = true;
|
||||||
await foreach (var response in next(request, cancellationToken))
|
return next(request, cancellationToken);
|
||||||
{
|
|
||||||
yield return response;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
// Copyright (c) The Geekeey Authors
|
// Copyright (c) The Geekeey Authors
|
||||||
// SPDX-License-Identifier: EUPL-1.2
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
using System.Diagnostics;
|
|
||||||
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
|
@ -38,7 +36,7 @@ internal sealed class ScalarRequestInvoker<TRequest, TResponse> : ScalarRequestI
|
||||||
|
|
||||||
static ScalarHandlerDelegate<TResponse> Chain(ScalarHandlerDelegate<TResponse> next, IScalarRequestBehavior<TRequest, TResponse> filter)
|
static ScalarHandlerDelegate<TResponse> Chain(ScalarHandlerDelegate<TResponse> next, IScalarRequestBehavior<TRequest, TResponse> filter)
|
||||||
{
|
{
|
||||||
return [StackTraceHidden] (req, ct) => filter.HandleAsync((TRequest)req, next, ct);
|
return (req, ct) => filter.HandleAsync((TRequest)req, next, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
Task<TResponse> Head(IScalarRequest<TResponse> r, CancellationToken ct)
|
Task<TResponse> Head(IScalarRequest<TResponse> r, CancellationToken ct)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
// Copyright (c) The Geekeey Authors
|
// Copyright (c) The Geekeey Authors
|
||||||
// SPDX-License-Identifier: EUPL-1.2
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
@ -42,7 +41,7 @@ internal sealed class StreamRequestInvoker<TRequest, TResponse> : StreamRequestI
|
||||||
|
|
||||||
static StreamHandlerDelegate<TResponse> Chain(StreamHandlerDelegate<TResponse> next, IStreamRequestBehavior<TRequest, TResponse> filter)
|
static StreamHandlerDelegate<TResponse> Chain(StreamHandlerDelegate<TResponse> next, IStreamRequestBehavior<TRequest, TResponse> filter)
|
||||||
{
|
{
|
||||||
return [StackTraceHidden] (req, ct) => filter.HandleAsync((TRequest)req, next, ct);
|
return (req, ct) => filter.HandleAsync((TRequest)req, next, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
IAsyncEnumerable<TResponse> Head(IStreamRequest<TResponse> r, CancellationToken ct)
|
IAsyncEnumerable<TResponse> Head(IStreamRequest<TResponse> r, CancellationToken ct)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ internal sealed class DependencyInjectionTests
|
||||||
public async Task I_can_resolve_single_validator()
|
public async Task I_can_resolve_single_validator()
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddRequestValidation(builder =>
|
services.AddValidation(builder =>
|
||||||
{
|
{
|
||||||
builder.Add<OrderedFailuresValidator>(ServiceLifetime.Transient);
|
builder.Add<OrderedFailuresValidator>(ServiceLifetime.Transient);
|
||||||
});
|
});
|
||||||
|
|
@ -27,7 +27,7 @@ internal sealed class DependencyInjectionTests
|
||||||
public async Task I_can_resolve_multiple_validators()
|
public async Task I_can_resolve_multiple_validators()
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddRequestValidation(builder =>
|
services.AddValidation(builder =>
|
||||||
{
|
{
|
||||||
builder.Add<PersonValidator>(ServiceLifetime.Transient);
|
builder.Add<PersonValidator>(ServiceLifetime.Transient);
|
||||||
builder.Add<AnotherPersonValidator>(ServiceLifetime.Transient);
|
builder.Add<AnotherPersonValidator>(ServiceLifetime.Transient);
|
||||||
|
|
@ -46,7 +46,7 @@ internal sealed class DependencyInjectionTests
|
||||||
public async Task I_can_resolve_open_generic_validator()
|
public async Task I_can_resolve_open_generic_validator()
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddRequestValidation(builder =>
|
services.AddValidation(builder =>
|
||||||
{
|
{
|
||||||
builder.Add(typeof(GenericValidator<>), ServiceLifetime.Transient);
|
builder.Add(typeof(GenericValidator<>), ServiceLifetime.Transient);
|
||||||
});
|
});
|
||||||
|
|
@ -63,7 +63,7 @@ internal sealed class DependencyInjectionTests
|
||||||
public async Task I_can_resolve_multi_interface_validator()
|
public async Task I_can_resolve_multi_interface_validator()
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddRequestValidation(builder =>
|
services.AddValidation(builder =>
|
||||||
{
|
{
|
||||||
builder.Add<MultiInterfaceValidator>(ServiceLifetime.Transient);
|
builder.Add<MultiInterfaceValidator>(ServiceLifetime.Transient);
|
||||||
});
|
});
|
||||||
|
|
@ -84,7 +84,7 @@ internal sealed class DependencyInjectionTests
|
||||||
public async Task I_can_resolve_generic_validator_with_constraints()
|
public async Task I_can_resolve_generic_validator_with_constraints()
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddRequestValidation(builder =>
|
services.AddValidation(builder =>
|
||||||
{
|
{
|
||||||
builder.Add(typeof(ConstrainedValidator<>), ServiceLifetime.Transient);
|
builder.Add(typeof(ConstrainedValidator<>), ServiceLifetime.Transient);
|
||||||
});
|
});
|
||||||
|
|
@ -104,7 +104,7 @@ internal sealed class DependencyInjectionTests
|
||||||
public async Task I_can_resolve_generic_wrapper_validator_with_constraints()
|
public async Task I_can_resolve_generic_wrapper_validator_with_constraints()
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddRequestValidation(builder =>
|
services.AddValidation(builder =>
|
||||||
{
|
{
|
||||||
builder.Add(typeof(ConstrainedWrapperValidator<>), ServiceLifetime.Transient);
|
builder.Add(typeof(ConstrainedWrapperValidator<>), ServiceLifetime.Transient);
|
||||||
});
|
});
|
||||||
|
|
@ -124,7 +124,7 @@ internal sealed class DependencyInjectionTests
|
||||||
public async Task I_can_resolve_aggregate_validator_directly()
|
public async Task I_can_resolve_aggregate_validator_directly()
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddRequestValidation(builder =>
|
services.AddValidation(builder =>
|
||||||
{
|
{
|
||||||
builder.Add<PersonValidator>(ServiceLifetime.Transient);
|
builder.Add<PersonValidator>(ServiceLifetime.Transient);
|
||||||
builder.Add<AnotherPersonValidator>(ServiceLifetime.Transient);
|
builder.Add<AnotherPersonValidator>(ServiceLifetime.Transient);
|
||||||
|
|
@ -142,7 +142,7 @@ internal sealed class DependencyInjectionTests
|
||||||
public async Task I_can_select_validators_from_base_classes_and_interfaces()
|
public async Task I_can_select_validators_from_base_classes_and_interfaces()
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddRequestValidation(builder =>
|
services.AddValidation(builder =>
|
||||||
{
|
{
|
||||||
builder.Add<EntityValidator>(ServiceLifetime.Transient);
|
builder.Add<EntityValidator>(ServiceLifetime.Transient);
|
||||||
builder.Add<AnimalValidator>(ServiceLifetime.Transient);
|
builder.Add<AnimalValidator>(ServiceLifetime.Transient);
|
||||||
|
|
@ -161,7 +161,7 @@ internal sealed class DependencyInjectionTests
|
||||||
public async Task I_can_select_validators_polymorphically_based_on_the_instance_type()
|
public async Task I_can_select_validators_polymorphically_based_on_the_instance_type()
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddRequestValidation(builder =>
|
services.AddValidation(builder =>
|
||||||
{
|
{
|
||||||
builder.Add<AnimalValidator>(ServiceLifetime.Transient);
|
builder.Add<AnimalValidator>(ServiceLifetime.Transient);
|
||||||
builder.Add<DogValidator>(ServiceLifetime.Transient);
|
builder.Add<DogValidator>(ServiceLifetime.Transient);
|
||||||
|
|
|
||||||
|
|
@ -12,19 +12,6 @@ internal sealed class PropertyPathTests
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
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]
|
[Test]
|
||||||
public async Task I_can_serialize_property_paths_using_the_json_naming_policy()
|
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}""");
|
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<PropertyPath, string> 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<Dictionary<PropertyPath, string>>(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]
|
[Test]
|
||||||
public async Task I_can_iterate_and_index_segments()
|
public async Task I_can_iterate_and_index_segments()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,14 @@
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Reflection.Metadata;
|
using System.Reflection.Metadata;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
[assembly: MetadataUpdateHandler(typeof(Geekeey.Request.Validation.DispatchingValidator))]
|
[assembly: MetadataUpdateHandler(typeof(Geekeey.Request.Validation.DispatchingValidator))]
|
||||||
|
|
||||||
namespace Geekeey.Request.Validation;
|
namespace Geekeey.Request.Validation;
|
||||||
|
|
||||||
internal sealed partial class DispatchingValidator : IValidator
|
internal sealed class DispatchingValidator : IValidator
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<Type, ValidatorInvoker> ValidatorsHandlers = new();
|
private static readonly ConcurrentDictionary<Type, ValidatorInvoker> ValidatorsHandlers = new();
|
||||||
|
|
||||||
|
|
@ -41,4 +44,31 @@ internal sealed partial class DispatchingValidator : IValidator
|
||||||
|
|
||||||
return handler.Validate(context, _serviceProvider);
|
return handler.Validate(context, _serviceProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private abstract class ValidatorInvoker
|
||||||
|
{
|
||||||
|
public abstract Validation Validate(ValidationContext context, IServiceProvider serviceProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ValidatorInvoker<T> : ValidatorInvoker
|
||||||
|
{
|
||||||
|
public override Validation Validate(ValidationContext context, IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
var options = serviceProvider.GetRequiredService<IOptions<ValidationOptions>>().Value;
|
||||||
|
|
||||||
|
var validators = options.GetValidators<T>(serviceProvider);
|
||||||
|
|
||||||
|
var problems = new List<Problem>();
|
||||||
|
|
||||||
|
foreach (var validator in validators)
|
||||||
|
{
|
||||||
|
if (validator.Validate(context) is { IsValid: false, Problems: { } result })
|
||||||
|
{
|
||||||
|
problems.AddRange(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Validation(problems);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ namespace Geekeey.Request.Validation;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines a builder for configuring validator registrations.
|
/// Defines a builder for configuring validator registrations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IRequestValidatorBuilder
|
public interface IValidatorBuilder
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the service collection where the validators are registered.
|
/// Gets the service collection where the validators are registered.
|
||||||
|
|
@ -399,26 +399,9 @@ internal sealed class PropertyPathJsonConverter : JsonConverter<PropertyPath>
|
||||||
throw new JsonException($"Expected {nameof(JsonTokenType.String)} but got {reader.TokenType}.");
|
throw new JsonException($"Expected {nameof(JsonTokenType.String)} but got {reader.TokenType}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
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}.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Write(Utf8JsonWriter writer, PropertyPath value, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, PropertyPath value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
writer.WriteStringValue(value.ToJsonName(options.PropertyNamingPolicy));
|
writer.WriteStringValue(value.ToJsonName(options.PropertyNamingPolicy));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void WriteAsPropertyName(Utf8JsonWriter writer, PropertyPath value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
writer.WritePropertyName(value.ToJsonName(options.DictionaryKeyPolicy));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,15 @@ public static class ServiceCollectionExtensions
|
||||||
/// Adds validator services to the specified <see cref="IServiceCollection"/>.
|
/// Adds validator services to the specified <see cref="IServiceCollection"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="services">The service collection to which the validator services will be added.</param>
|
/// <param name="services">The service collection to which the validator services will be added.</param>
|
||||||
/// <returns>An instance of <see cref="IRequestValidatorBuilder"/> to configure the validator registrations.</returns>
|
/// <returns>An instance of <see cref="IValidatorBuilder"/> to configure the validator registrations.</returns>
|
||||||
public static IRequestValidatorBuilder AddRequestValidation(this IServiceCollection services)
|
public static IValidatorBuilder AddValidation(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(services);
|
ArgumentNullException.ThrowIfNull(services);
|
||||||
|
|
||||||
services.AddOptions<RequestValidatorOptions>();
|
services.AddOptions<ValidationOptions>();
|
||||||
services.AddTransient<IValidator, DispatchingValidator>();
|
services.AddTransient<IValidator, DispatchingValidator>();
|
||||||
|
|
||||||
return new RequestValidatorBuilder(services);
|
return new ValidatorBuilder(services);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -32,17 +32,17 @@ public static class ServiceCollectionExtensions
|
||||||
/// <param name="services">The service collection to which the validator services will be added.</param>
|
/// <param name="services">The service collection to which the validator services will be added.</param>
|
||||||
/// <param name="configure">A delegate to configure the validator builder.</param>
|
/// <param name="configure">A delegate to configure the validator builder.</param>
|
||||||
/// <returns>The service collection with the validator services added.</returns>
|
/// <returns>The service collection with the validator services added.</returns>
|
||||||
public static IServiceCollection AddRequestValidation(this IServiceCollection services, Action<IRequestValidatorBuilder> configure)
|
public static IServiceCollection AddValidation(this IServiceCollection services, Action<IValidatorBuilder> configure)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(services);
|
ArgumentNullException.ThrowIfNull(services);
|
||||||
ArgumentNullException.ThrowIfNull(configure);
|
ArgumentNullException.ThrowIfNull(configure);
|
||||||
|
|
||||||
configure(services.AddRequestValidation());
|
configure(services.AddValidation());
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class RequestValidatorBuilder(IServiceCollection services) : IRequestValidatorBuilder
|
private sealed class ValidatorBuilder(IServiceCollection services) : IValidatorBuilder
|
||||||
{
|
{
|
||||||
public IServiceCollection Services { get; } = services;
|
public IServiceCollection Services { get; } = services;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,12 @@ using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Geekeey.Request.Validation;
|
namespace Geekeey.Request.Validation;
|
||||||
|
|
||||||
internal sealed class RequestValidatorOptions
|
internal sealed class ValidationOptions
|
||||||
{
|
{
|
||||||
private readonly List<Type> _search = [];
|
private readonly List<Type> _search = [];
|
||||||
private readonly Lazy<TypeIndex> _validatorsTypeIndex;
|
private readonly Lazy<TypeIndex> _validatorsTypeIndex;
|
||||||
|
|
||||||
public RequestValidatorOptions()
|
public ValidationOptions()
|
||||||
{
|
{
|
||||||
_validatorsTypeIndex = new Lazy<TypeIndex>(() => new ValidatorTypeIndex(_search.Distinct()));
|
_validatorsTypeIndex = new Lazy<TypeIndex>(() => new ValidatorTypeIndex(_search.Distinct()));
|
||||||
}
|
}
|
||||||
|
|
@ -6,24 +6,24 @@ using System.Reflection;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
|
||||||
using static Geekeey.Request.Validation.RequestValidatorOptions;
|
using static Geekeey.Request.Validation.ValidationOptions;
|
||||||
|
|
||||||
namespace Geekeey.Request.Validation;
|
namespace Geekeey.Request.Validation;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides extension methods for configuring <see cref="IRequestValidatorBuilder"/>
|
/// Provides extension methods for configuring <see cref="IValidatorBuilder"/>
|
||||||
/// with additional capabilities such as searching and registering validators in assemblies or adding types directly.
|
/// with additional capabilities such as searching and registering validators in assemblies or adding types directly.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class RequestValidatorBuilderExtensions
|
public static class ValidatorBuilderExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Searches for validator types within the specified assembly and adds them to the validator
|
/// Searches for validator types within the specified assembly and adds them to the validator
|
||||||
/// configuration.
|
/// configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="builder">The <see cref="IRequestValidatorBuilder"/> to configure.</param>
|
/// <param name="builder">The <see cref="IValidatorBuilder"/> to configure.</param>
|
||||||
/// <param name="assembly">The assembly to search for validator types.</param>
|
/// <param name="assembly">The assembly to search for validator types.</param>
|
||||||
/// <returns>The <see cref="IRequestValidatorBuilder"/> instance for further configuration.</returns>
|
/// <returns>The <see cref="IValidatorBuilder"/> instance for further configuration.</returns>
|
||||||
public static IRequestValidatorBuilder SearchInAssembly(this IRequestValidatorBuilder builder, Assembly assembly)
|
public static IValidatorBuilder SearchInAssembly(this IValidatorBuilder builder, Assembly assembly)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(builder);
|
ArgumentNullException.ThrowIfNull(builder);
|
||||||
|
|
||||||
|
|
@ -40,11 +40,11 @@ public static class RequestValidatorBuilderExtensions
|
||||||
/// Searches for validator types within the specified assembly and adds them to the validator
|
/// Searches for validator types within the specified assembly and adds them to the validator
|
||||||
/// configuration with the given service lifetime.
|
/// configuration with the given service lifetime.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="builder">The <see cref="IRequestValidatorBuilder"/> to configure.</param>
|
/// <param name="builder">The <see cref="IValidatorBuilder"/> to configure.</param>
|
||||||
/// <param name="assembly">The assembly to search for validator types.</param>
|
/// <param name="assembly">The assembly to search for validator types.</param>
|
||||||
/// <param name="lifetime">The lifetime with which the validators are registered in the dependency injection container.</param>
|
/// <param name="lifetime">The lifetime with which the validators are registered in the dependency injection container.</param>
|
||||||
/// <returns>The <see cref="IRequestValidatorBuilder"/> instance for further configuration.</returns>
|
/// <returns>The <see cref="IValidatorBuilder"/> instance for further configuration.</returns>
|
||||||
public static IRequestValidatorBuilder SearchInAssembly(this IRequestValidatorBuilder builder, Assembly assembly, ServiceLifetime lifetime)
|
public static IValidatorBuilder SearchInAssembly(this IValidatorBuilder builder, Assembly assembly, ServiceLifetime lifetime)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(builder);
|
ArgumentNullException.ThrowIfNull(builder);
|
||||||
|
|
||||||
|
|
@ -60,14 +60,14 @@ public static class RequestValidatorBuilderExtensions
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds the specified type to the validator configuration for inspection.
|
/// Adds the specified type to the validator configuration for inspection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="builder">The <see cref="IRequestValidatorBuilder"/> to configure.</param>
|
/// <param name="builder">The <see cref="IValidatorBuilder"/> to configure.</param>
|
||||||
/// <param name="type">The type to be added to the validator configuration.</param>
|
/// <param name="type">The type to be added to the validator configuration.</param>
|
||||||
/// <returns>The <see cref="IRequestValidatorBuilder"/> instance for further configuration.</returns>
|
/// <returns>The <see cref="IValidatorBuilder"/> instance for further configuration.</returns>
|
||||||
public static IRequestValidatorBuilder Add(this IRequestValidatorBuilder builder, Type type)
|
public static IValidatorBuilder Add(this IValidatorBuilder builder, Type type)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(builder);
|
ArgumentNullException.ThrowIfNull(builder);
|
||||||
|
|
||||||
builder.Services.AddOptions<RequestValidatorOptions>()
|
builder.Services.AddOptions<ValidationOptions>()
|
||||||
.Configure(options => options.Inspect([type]));
|
.Configure(options => options.Inspect([type]));
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
|
|
@ -78,15 +78,15 @@ public static class RequestValidatorBuilderExtensions
|
||||||
/// This also adds the type to the service collection with the specified lifetime,
|
/// This also adds the type to the service collection with the specified lifetime,
|
||||||
/// allowing it to be resolved as a dependency.
|
/// allowing it to be resolved as a dependency.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="builder">The <see cref="IRequestValidatorBuilder"/> used to configure the validators.</param>
|
/// <param name="builder">The <see cref="IValidatorBuilder"/> used to configure the validators.</param>
|
||||||
/// <param name="type">The type to be added to the validator configuration.</param>
|
/// <param name="type">The type to be added to the validator configuration.</param>
|
||||||
/// <param name="lifetime">The lifetime scope of the type in the service container.</param>
|
/// <param name="lifetime">The lifetime scope of the type in the service container.</param>
|
||||||
/// <returns>The <see cref="IRequestValidatorBuilder"/> instance for further configuration.</returns>
|
/// <returns>The <see cref="IValidatorBuilder"/> instance for further configuration.</returns>
|
||||||
public static IRequestValidatorBuilder Add(this IRequestValidatorBuilder builder, Type type, ServiceLifetime lifetime)
|
public static IValidatorBuilder Add(this IValidatorBuilder builder, Type type, ServiceLifetime lifetime)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(builder);
|
ArgumentNullException.ThrowIfNull(builder);
|
||||||
|
|
||||||
builder.Services.AddOptions<RequestValidatorOptions>()
|
builder.Services.AddOptions<ValidationOptions>()
|
||||||
.Configure(options => options.Inspect([type]));
|
.Configure(options => options.Inspect([type]));
|
||||||
|
|
||||||
builder.Services.Add(new ServiceDescriptor(type, type, lifetime));
|
builder.Services.Add(new ServiceDescriptor(type, type, lifetime));
|
||||||
|
|
@ -97,16 +97,16 @@ public static class RequestValidatorBuilderExtensions
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds the specified collection of types to the validator configuration for inspection.
|
/// Adds the specified collection of types to the validator configuration for inspection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="builder">The <see cref="IRequestValidatorBuilder"/> to configure.</param>
|
/// <param name="builder">The <see cref="IValidatorBuilder"/> to configure.</param>
|
||||||
/// <param name="types">The collection of types to be added to the validator configuration.</param>
|
/// <param name="types">The collection of types to be added to the validator configuration.</param>
|
||||||
/// <returns>The <see cref="IRequestValidatorBuilder"/> instance for further configuration.</returns>
|
/// <returns>The <see cref="IValidatorBuilder"/> instance for further configuration.</returns>
|
||||||
public static IRequestValidatorBuilder Add(this IRequestValidatorBuilder builder, IEnumerable<Type> types)
|
public static IValidatorBuilder Add(this IValidatorBuilder builder, IEnumerable<Type> types)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(builder);
|
ArgumentNullException.ThrowIfNull(builder);
|
||||||
|
|
||||||
var typeList = types.ToList();
|
var typeList = types.ToList();
|
||||||
|
|
||||||
builder.Services.AddOptions<RequestValidatorOptions>()
|
builder.Services.AddOptions<ValidationOptions>()
|
||||||
.Configure(options => options.Inspect(typeList));
|
.Configure(options => options.Inspect(typeList));
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
|
|
@ -117,17 +117,17 @@ public static class RequestValidatorBuilderExtensions
|
||||||
/// This also adds the specified collection of types to the service collection with the specified lifetime,
|
/// This also adds the specified collection of types to the service collection with the specified lifetime,
|
||||||
/// allowing it to be resolved as a dependency.
|
/// allowing it to be resolved as a dependency.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="builder">The <see cref="IRequestValidatorBuilder"/> to configure.</param>
|
/// <param name="builder">The <see cref="IValidatorBuilder"/> to configure.</param>
|
||||||
/// <param name="types">The collection of types to be added to the validator configuration.</param>
|
/// <param name="types">The collection of types to be added to the validator configuration.</param>
|
||||||
/// <param name="lifetime">The lifetime scope of the types in the service container.</param>
|
/// <param name="lifetime">The lifetime scope of the types in the service container.</param>
|
||||||
/// <returns>The <see cref="IRequestValidatorBuilder"/> instance for further configuration.</returns>
|
/// <returns>The <see cref="IValidatorBuilder"/> instance for further configuration.</returns>
|
||||||
public static IRequestValidatorBuilder Add(this IRequestValidatorBuilder builder, IEnumerable<Type> types, ServiceLifetime lifetime)
|
public static IValidatorBuilder Add(this IValidatorBuilder builder, IEnumerable<Type> types, ServiceLifetime lifetime)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(builder);
|
ArgumentNullException.ThrowIfNull(builder);
|
||||||
|
|
||||||
var typeList = types.ToList();
|
var typeList = types.ToList();
|
||||||
|
|
||||||
builder.Services.AddOptions<RequestValidatorOptions>()
|
builder.Services.AddOptions<ValidationOptions>()
|
||||||
.Configure(options => options.Inspect(typeList));
|
.Configure(options => options.Inspect(typeList));
|
||||||
|
|
||||||
builder.Services.Add(typeList.Select(export => new ServiceDescriptor(export, export, lifetime)));
|
builder.Services.Add(typeList.Select(export => new ServiceDescriptor(export, export, lifetime)));
|
||||||
|
|
@ -139,9 +139,9 @@ public static class RequestValidatorBuilderExtensions
|
||||||
/// Adds the specified validator type to the validator configuration.
|
/// Adds the specified validator type to the validator configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TValidator">The type of the validator to add.</typeparam>
|
/// <typeparam name="TValidator">The type of the validator to add.</typeparam>
|
||||||
/// <param name="builder">The <see cref="IRequestValidatorBuilder"/> to configure.</param>
|
/// <param name="builder">The <see cref="IValidatorBuilder"/> to configure.</param>
|
||||||
/// <returns>The <see cref="IRequestValidatorBuilder"/> instance for further configuration.</returns>
|
/// <returns>The <see cref="IValidatorBuilder"/> instance for further configuration.</returns>
|
||||||
public static IRequestValidatorBuilder Add<TValidator>(this IRequestValidatorBuilder builder)
|
public static IValidatorBuilder Add<TValidator>(this IValidatorBuilder builder)
|
||||||
where TValidator : class, IValidator
|
where TValidator : class, IValidator
|
||||||
{
|
{
|
||||||
return builder.Add(typeof(TValidator));
|
return builder.Add(typeof(TValidator));
|
||||||
|
|
@ -151,10 +151,10 @@ public static class RequestValidatorBuilderExtensions
|
||||||
/// Adds the specified validator type to the validator configuration with the specified lifetime.
|
/// Adds the specified validator type to the validator configuration with the specified lifetime.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TValidator">The type of the validator to add.</typeparam>
|
/// <typeparam name="TValidator">The type of the validator to add.</typeparam>
|
||||||
/// <param name="builder">The <see cref="IRequestValidatorBuilder"/> to configure.</param>
|
/// <param name="builder">The <see cref="IValidatorBuilder"/> to configure.</param>
|
||||||
/// <param name="lifetime">The lifetime scope of the validator in the service container.</param>
|
/// <param name="lifetime">The lifetime scope of the validator in the service container.</param>
|
||||||
/// <returns>The <see cref="IRequestValidatorBuilder"/> instance for further configuration.</returns>
|
/// <returns>The <see cref="IValidatorBuilder"/> instance for further configuration.</returns>
|
||||||
public static IRequestValidatorBuilder Add<TValidator>(this IRequestValidatorBuilder builder, ServiceLifetime lifetime)
|
public static IValidatorBuilder Add<TValidator>(this IValidatorBuilder builder, ServiceLifetime lifetime)
|
||||||
where TValidator : class, IValidator
|
where TValidator : class, IValidator
|
||||||
{
|
{
|
||||||
return builder.Add(typeof(TValidator), lifetime);
|
return builder.Add(typeof(TValidator), lifetime);
|
||||||
|
|
@ -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<T> : ValidatorInvoker
|
|
||||||
{
|
|
||||||
public override Validation Validate(ValidationContext context, IServiceProvider serviceProvider)
|
|
||||||
{
|
|
||||||
var options = serviceProvider.GetRequiredService<IOptions<RequestValidatorOptions>>().Value;
|
|
||||||
|
|
||||||
var validators = options.GetValidators<T>(serviceProvider);
|
|
||||||
|
|
||||||
var problems = new List<Problem>();
|
|
||||||
|
|
||||||
foreach (var validator in validators)
|
|
||||||
{
|
|
||||||
if (validator.Validate(context) is { IsValid: false, Problems: { } result })
|
|
||||||
{
|
|
||||||
problems.AddRange(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Validation(problems);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue