feat: add support for validator resolution from dependency injection #1
19 changed files with 806 additions and 1 deletions
feat: add support for validator resolution from dependency injection
commit
22142882b5
|
|
@ -16,6 +16,11 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|||
|
||||
### 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<T>` extension method for simplified validator invocation
|
||||
|
||||
### Changed
|
||||
|
||||
### Removed
|
||||
|
|
|
|||
177
src/request.validation.tests/DependencyInjectionTests.cs
Normal file
177
src/request.validation.tests/DependencyInjectionTests.cs
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
// 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.AddValidation(builder =>
|
||||
{
|
||||
builder.Add<OrderedFailuresValidator>(ServiceLifetime.Transient);
|
||||
});
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
var validator = provider.GetRequiredService<IValidator>();
|
||||
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.AddValidation(builder =>
|
||||
{
|
||||
builder.Add<PersonValidator>(ServiceLifetime.Transient);
|
||||
builder.Add<AnotherPersonValidator>(ServiceLifetime.Transient);
|
||||
});
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
var validator = provider.GetRequiredService<IValidator>();
|
||||
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.AddValidation(builder =>
|
||||
{
|
||||
builder.Add(typeof(GenericValidator<>), ServiceLifetime.Transient);
|
||||
});
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
var validator = provider.GetRequiredService<IValidator>();
|
||||
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.AddValidation(builder =>
|
||||
{
|
||||
builder.Add<MultiInterfaceValidator>(ServiceLifetime.Transient);
|
||||
});
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
var validator = provider.GetRequiredService<IValidator>();
|
||||
|
||||
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.AddValidation(builder =>
|
||||
{
|
||||
builder.Add(typeof(ConstrainedValidator<>), ServiceLifetime.Transient);
|
||||
});
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
var validator = provider.GetRequiredService<IValidator>();
|
||||
|
||||
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.AddValidation(builder =>
|
||||
{
|
||||
builder.Add(typeof(ConstrainedWrapperValidator<>), ServiceLifetime.Transient);
|
||||
});
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
var validator = provider.GetRequiredService<IValidator>();
|
||||
|
||||
var teamWrapperResult = validator.Validate(new Wrapper<Team>());
|
||||
await Assert.That(teamWrapperResult.Problems).Count().IsEqualTo(1);
|
||||
await Assert.That(teamWrapperResult.Problems[0].Message).IsEqualTo("ConstrainedWrapperValidator failure.");
|
||||
|
||||
var personWrapperResult = validator.Validate(new Wrapper<Person>());
|
||||
await Assert.That(personWrapperResult.Problems).Count().IsEqualTo(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_can_resolve_aggregate_validator_directly()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddValidation(builder =>
|
||||
{
|
||||
builder.Add<PersonValidator>(ServiceLifetime.Transient);
|
||||
builder.Add<AnotherPersonValidator>(ServiceLifetime.Transient);
|
||||
});
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
var validator = provider.GetRequiredService<IValidator>();
|
||||
|
||||
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.AddValidation(builder =>
|
||||
{
|
||||
builder.Add<EntityValidator>(ServiceLifetime.Transient);
|
||||
builder.Add<AnimalValidator>(ServiceLifetime.Transient);
|
||||
builder.Add<DogValidator>(ServiceLifetime.Transient);
|
||||
});
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
var validator = provider.GetRequiredService<IValidator>();
|
||||
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.AddValidation(builder =>
|
||||
{
|
||||
builder.Add<AnimalValidator>(ServiceLifetime.Transient);
|
||||
builder.Add<DogValidator>(ServiceLifetime.Transient);
|
||||
});
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
var validator = provider.GetRequiredService<IValidator>();
|
||||
var dog = new Dog { Name = "", Breed = "" };
|
||||
var result = validator.Validate(new ValidationContext<object>(dog));
|
||||
|
||||
await Assert.That(result.Problems).Count().IsEqualTo(2);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
namespace Geekeey.Request.Validation.Tests;
|
||||
|
||||
internal sealed class AnotherPersonValidator : Validator<Person>
|
||||
{
|
||||
public AnotherPersonValidator()
|
||||
{
|
||||
RuleFor(p => p.Name).Must(_ => false, "AnotherPersonValidator failure.");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
namespace Geekeey.Request.Validation.Tests;
|
||||
|
||||
internal sealed class ConstrainedValidator<T> : Validator<T> where T : ICustomConstraint
|
||||
{
|
||||
public ConstrainedValidator()
|
||||
{
|
||||
RuleFor(x => x).Must(_ => false, "ConstrainedValidator failure.");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
namespace Geekeey.Request.Validation.Tests;
|
||||
|
||||
internal sealed class ConstrainedWrapperValidator<T> : Validator<Wrapper<T>> where T : ICustomConstraint
|
||||
{
|
||||
public ConstrainedWrapperValidator()
|
||||
{
|
||||
RuleFor(x => x).Must(_ => false, "ConstrainedWrapperValidator failure.");
|
||||
}
|
||||
}
|
||||
12
src/request.validation.tests/_fixtures/GenericValidator.cs
Normal file
12
src/request.validation.tests/_fixtures/GenericValidator.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
namespace Geekeey.Request.Validation.Tests;
|
||||
|
||||
internal sealed class GenericValidator<T> : Validator<T>
|
||||
{
|
||||
public GenericValidator()
|
||||
{
|
||||
RuleFor(x => x).Must(_ => false, "GenericValidator failure.");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
namespace Geekeey.Request.Validation.Tests;
|
||||
|
||||
internal interface ICustomConstraint { }
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
namespace Geekeey.Request.Validation.Tests;
|
||||
|
||||
internal sealed class MultiInterfaceValidator : IValidator<Person>, IValidator<Address>
|
||||
{
|
||||
public Validation Validate(Person instance)
|
||||
{
|
||||
return Validate(new ValidationContext<Person>(instance));
|
||||
}
|
||||
|
||||
public Validation Validate(Address instance)
|
||||
{
|
||||
return Validate(new ValidationContext<Address>(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([]);
|
||||
}
|
||||
}
|
||||
12
src/request.validation.tests/_fixtures/PersonValidator.cs
Normal file
12
src/request.validation.tests/_fixtures/PersonValidator.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
namespace Geekeey.Request.Validation.Tests;
|
||||
|
||||
internal sealed class PersonValidator : Validator<Person>
|
||||
{
|
||||
public PersonValidator()
|
||||
{
|
||||
RuleFor(p => p.Name).Must(_ => false, "PersonValidator failure.");
|
||||
}
|
||||
}
|
||||
44
src/request.validation.tests/_fixtures/PolymorphicTypes.cs
Normal file
44
src/request.validation.tests/_fixtures/PolymorphicTypes.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// 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<IEntity>
|
||||
{
|
||||
public EntityValidator()
|
||||
{
|
||||
RuleFor(x => x.Id).GreaterThan(0);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class AnimalValidator : Validator<Animal>
|
||||
{
|
||||
public AnimalValidator()
|
||||
{
|
||||
RuleFor(x => x.Name).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class DogValidator : Validator<Dog>
|
||||
{
|
||||
public DogValidator()
|
||||
{
|
||||
RuleFor(x => x.Breed).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
namespace Geekeey.Request.Validation.Tests;
|
||||
|
||||
internal sealed class Team
|
||||
internal sealed class Team : ICustomConstraint
|
||||
{
|
||||
public IEnumerable<Member> Members { get; init; } = [];
|
||||
}
|
||||
|
|
|
|||
6
src/request.validation.tests/_fixtures/Wrapper.cs
Normal file
6
src/request.validation.tests/_fixtures/Wrapper.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
namespace Geekeey.Request.Validation.Tests;
|
||||
|
||||
internal sealed class Wrapper<T> { }
|
||||
69
src/request.validation/DispatchingValidator.cs
Normal file
69
src/request.validation/DispatchingValidator.cs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection.Metadata;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
[assembly: MetadataUpdateHandler(typeof(Geekeey.Request.Validation.DispatchingValidator))]
|
||||
|
||||
namespace Geekeey.Request.Validation;
|
||||
|
||||
internal sealed class DispatchingValidator : IValidator
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, ValidatorInvoker> ValidatorsHandlers = new();
|
||||
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public DispatchingValidator(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -28,4 +28,9 @@
|
|||
<None Include="..\..\LICENSE.md" Pack="true" PackagePath="\" Visible="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
17
src/request.validation/IValidatorBuilder.cs
Normal file
17
src/request.validation/IValidatorBuilder.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Geekeey.Request.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a builder for configuring validator registrations.
|
||||
/// </summary>
|
||||
public interface IValidatorBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the service collection where the validators are registered.
|
||||
/// </summary>
|
||||
IServiceCollection Services { get; }
|
||||
}
|
||||
49
src/request.validation/ServiceCollectionExtensions.cs
Normal file
49
src/request.validation/ServiceCollectionExtensions.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Geekeey.Request.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for configuring and registering validator services in the <see cref="IServiceCollection"/>.
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds validator services to the specified <see cref="IServiceCollection"/>.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection to which the validator services will be added.</param>
|
||||
/// <returns>An instance of <see cref="IValidatorBuilder"/> to configure the validator registrations.</returns>
|
||||
public static IValidatorBuilder AddValidation(this IServiceCollection services)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
|
||||
services.AddOptions<ValidationOptions>();
|
||||
services.AddTransient<IValidator, DispatchingValidator>();
|
||||
|
||||
return new ValidatorBuilder(services);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds validator services to the specified <see cref="IServiceCollection"/>
|
||||
/// and configures them using the provided <see cref="Action{T}"/>.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <returns>The service collection with the validator services added.</returns>
|
||||
public static IServiceCollection AddValidation(this IServiceCollection services, Action<IValidatorBuilder> configure)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(configure);
|
||||
|
||||
configure(services.AddValidation());
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private sealed class ValidatorBuilder(IServiceCollection services) : IValidatorBuilder
|
||||
{
|
||||
public IServiceCollection Services { get; } = services;
|
||||
}
|
||||
}
|
||||
145
src/request.validation/ValidationOptions.cs
Normal file
145
src/request.validation/ValidationOptions.cs
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
// 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 ValidationOptions
|
||||
{
|
||||
private readonly List<Type> _search = [];
|
||||
private readonly Lazy<TypeIndex> _validatorsTypeIndex;
|
||||
|
||||
public ValidationOptions()
|
||||
{
|
||||
_validatorsTypeIndex = new Lazy<TypeIndex>(() => new ValidatorTypeIndex(_search.Distinct()));
|
||||
}
|
||||
|
||||
public void Inspect(IEnumerable<Type> assembly)
|
||||
{
|
||||
if (_validatorsTypeIndex.IsValueCreated)
|
||||
{
|
||||
throw new InvalidOperationException("The type index has already been created. Cannot inspect new assemblies.");
|
||||
}
|
||||
|
||||
_search.AddRange(assembly);
|
||||
}
|
||||
|
||||
public IEnumerable<IValidator<T>> GetValidators<T>(IServiceProvider services)
|
||||
{
|
||||
return _validatorsTypeIndex.Value.Resolve<IValidator<T>>(services);
|
||||
}
|
||||
|
||||
private abstract class TypeIndex
|
||||
{
|
||||
private readonly ConcurrentDictionary<Type, Func<IServiceProvider, IEnumerable>> _cache = new();
|
||||
|
||||
protected readonly Dictionary<Type, List<Type>> _closedTypeInfo = [];
|
||||
protected readonly List<Type> _openTypeInfo = [];
|
||||
|
||||
protected TypeIndex(IEnumerable<Type> collection, Func<Type, bool> 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<T> Resolve<T>(IServiceProvider services)
|
||||
{
|
||||
return (IEnumerable<T>)_cache.GetOrAdd(typeof(T), CreateResolverFactory<T>)(services);
|
||||
}
|
||||
|
||||
protected abstract IReadOnlyList<Type> IsAssignableTo(Type type);
|
||||
|
||||
private Func<IServiceProvider, IEnumerable<T>> CreateResolverFactory<T>(Type @interface)
|
||||
{
|
||||
var list = IsAssignableTo(@interface);
|
||||
return ResolverFactory;
|
||||
|
||||
IEnumerable<T> 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<Type> collection)
|
||||
: TypeIndex(collection, IsValidatorType)
|
||||
{
|
||||
protected override IReadOnlyList<Type> IsAssignableTo(Type @interface)
|
||||
{
|
||||
var result = new List<Type>();
|
||||
|
||||
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<T> : IValidator<T>
|
||||
// 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<T> : IValidator<Wrapper<T>>
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
167
src/request.validation/ValidatorBuilderExtensions.cs
Normal file
167
src/request.validation/ValidatorBuilderExtensions.cs
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
// 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.ValidationOptions;
|
||||
|
||||
namespace Geekeey.Request.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for configuring <see cref="IValidatorBuilder"/>
|
||||
/// with additional capabilities such as searching and registering validators in assemblies or adding types directly.
|
||||
/// </summary>
|
||||
public static class ValidatorBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Searches for validator types within the specified assembly and adds them to the validator
|
||||
/// configuration.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IValidatorBuilder"/> to configure.</param>
|
||||
/// <param name="assembly">The assembly to search for validator types.</param>
|
||||
/// <returns>The <see cref="IValidatorBuilder"/> instance for further configuration.</returns>
|
||||
public static IValidatorBuilder SearchInAssembly(this IValidatorBuilder 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for validator types within the specified assembly and adds them to the validator
|
||||
/// configuration with the given service lifetime.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IValidatorBuilder"/> to configure.</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>
|
||||
/// <returns>The <see cref="IValidatorBuilder"/> instance for further configuration.</returns>
|
||||
public static IValidatorBuilder SearchInAssembly(this IValidatorBuilder 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified type to the validator configuration for inspection.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IValidatorBuilder"/> to configure.</param>
|
||||
/// <param name="type">The type to be added to the validator configuration.</param>
|
||||
/// <returns>The <see cref="IValidatorBuilder"/> instance for further configuration.</returns>
|
||||
public static IValidatorBuilder Add(this IValidatorBuilder builder, Type type)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
|
||||
builder.Services.AddOptions<ValidationOptions>()
|
||||
.Configure(options => options.Inspect([type]));
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <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="lifetime">The lifetime scope of the type in the service container.</param>
|
||||
/// <returns>The <see cref="IValidatorBuilder"/> instance for further configuration.</returns>
|
||||
public static IValidatorBuilder Add(this IValidatorBuilder builder, Type type, ServiceLifetime lifetime)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
|
||||
builder.Services.AddOptions<ValidationOptions>()
|
||||
.Configure(options => options.Inspect([type]));
|
||||
|
||||
builder.Services.Add(new ServiceDescriptor(type, type, lifetime));
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified collection of types to the validator configuration for inspection.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <returns>The <see cref="IValidatorBuilder"/> instance for further configuration.</returns>
|
||||
public static IValidatorBuilder Add(this IValidatorBuilder builder, IEnumerable<Type> types)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
|
||||
var typeList = types.ToList();
|
||||
|
||||
builder.Services.AddOptions<ValidationOptions>()
|
||||
.Configure(options => options.Inspect(typeList));
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <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="lifetime">The lifetime scope of the types in the service container.</param>
|
||||
/// <returns>The <see cref="IValidatorBuilder"/> instance for further configuration.</returns>
|
||||
public static IValidatorBuilder Add(this IValidatorBuilder builder, IEnumerable<Type> types, ServiceLifetime lifetime)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
|
||||
var typeList = types.ToList();
|
||||
|
||||
builder.Services.AddOptions<ValidationOptions>()
|
||||
.Configure(options => options.Inspect(typeList));
|
||||
|
||||
builder.Services.Add(typeList.Select(export => new ServiceDescriptor(export, export, lifetime)));
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified validator type to the validator configuration.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValidator">The type of the validator to add.</typeparam>
|
||||
/// <param name="builder">The <see cref="IValidatorBuilder"/> to configure.</param>
|
||||
/// <returns>The <see cref="IValidatorBuilder"/> instance for further configuration.</returns>
|
||||
public static IValidatorBuilder Add<TValidator>(this IValidatorBuilder builder)
|
||||
where TValidator : class, IValidator
|
||||
{
|
||||
return builder.Add(typeof(TValidator));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified validator type to the validator configuration with the specified lifetime.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValidator">The type of the validator to add.</typeparam>
|
||||
/// <param name="builder">The <see cref="IValidatorBuilder"/> to configure.</param>
|
||||
/// <param name="lifetime">The lifetime scope of the validator in the service container.</param>
|
||||
/// <returns>The <see cref="IValidatorBuilder"/> instance for further configuration.</returns>
|
||||
public static IValidatorBuilder Add<TValidator>(this IValidatorBuilder builder, ServiceLifetime lifetime)
|
||||
where TValidator : class, IValidator
|
||||
{
|
||||
return builder.Add(typeof(TValidator), lifetime);
|
||||
}
|
||||
|
||||
private static bool IsValidatorImplementationType(Type type)
|
||||
{
|
||||
return type.GetInterfaces().Any(IsValidatorType);
|
||||
}
|
||||
}
|
||||
23
src/request.validation/ValidatorExtensions.cs
Normal file
23
src/request.validation/ValidatorExtensions.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
namespace Geekeey.Request.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IValidator"/>.
|
||||
/// </summary>
|
||||
public static class ValidatorExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes the validation logic for a specified instance of type <typeparamref name="T"/> and returns the validation result.
|
||||
/// </summary>
|
||||
/// <param name="validator">The <see cref="IValidator"/> instance used to perform the validation.</param>
|
||||
/// <param name="instance">The instance of type <typeparamref name="T"/> to validate.</param>
|
||||
/// <param name="serviceProvider">The service provider available for nested validator resolution.</param>
|
||||
/// <param name="items">Per-call state shared across nested validation operations.</param>
|
||||
/// <returns>A <see cref="Validation"/> object containing the results of the validation, including any problems encountered.</returns>
|
||||
public static Validation Validate<T>(this IValidator validator, T instance, IServiceProvider? serviceProvider = null, IReadOnlyDictionary<object, object?>? items = null)
|
||||
{
|
||||
return validator.Validate(new ValidationContext<T>(instance, serviceProvider, items));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue