diff --git a/Directory.Packages.props b/Directory.Packages.props
index 3973b15..7243da4 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -13,5 +13,6 @@
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 3650c9f..934ac97 100644
--- a/README.md
+++ b/README.md
@@ -41,8 +41,11 @@ class Widget
var widget = new Widget { Name = "" };
// Get your serviceProvider from wherever makes sense
-var serviceProvider = ...
-var isValid = MiniValidator.TryValidate(widget, serviceProvider, out var errors);
+var services = new ServicesCollection();
+services.AddMiniValidation();
+var serviceProvider = services.CreateServiceProvider();
+var validator = serviceProvider.GetRequiredService();
+var isValid = validator.TryValidate(widget, out var errors);
class Widget : IValidatableObject
{
diff --git a/samples/Samples.Console/Samples.Console.csproj b/samples/Samples.Console/Samples.Console.csproj
index 92b9057..7bfb498 100644
--- a/samples/Samples.Console/Samples.Console.csproj
+++ b/samples/Samples.Console/Samples.Console.csproj
@@ -2,7 +2,7 @@
Exe
- net6.0
+ net8.0
enable
diff --git a/samples/Samples.Web/Program.cs b/samples/Samples.Web/Program.cs
index 68bedb0..86fd134 100644
--- a/samples/Samples.Web/Program.cs
+++ b/samples/Samples.Web/Program.cs
@@ -6,6 +6,8 @@
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
+builder.Services.AddMiniValidator();
+builder.Services.AddClassMiniValidator();
var app = builder.Build();
@@ -23,15 +25,36 @@
app.MapGet("/widgets/{name}", (string name) =>
new Widget { Name = name });
-app.MapPost("/widgets", Results> (Widget widget) =>
- !MiniValidator.TryValidate(widget, out var errors)
- ? TypedResults.ValidationProblem(errors)
- : TypedResults.Created($"/widgets/{widget.Name}", widget));
+app.MapPost("/widgets", Results> (Widget widget, IMiniValidator validator) =>
+{
+ if (!validator.TryValidate(widget, out var errors))
+ {
+ return TypedResults.ValidationProblem(errors);
+ }
+
+ return TypedResults.Created($"/widgets/{widget.Name}", widget);
+});
+
+app.MapPost("/widgets/class-validator", async Task>> (WidgetWithClassValidator widget, IMiniValidator validator) =>
+{
+ var (isValid, errors) = await validator.TryValidateAsync(widget);
+ if (!isValid)
+ {
+ return TypedResults.ValidationProblem(errors);
+ }
-app.MapPost("/widgets/custom-validation", Results> (WidgetWithCustomValidation widget) =>
- !MiniValidator.TryValidate(widget, out var errors)
- ? TypedResults.ValidationProblem(errors)
- : TypedResults.Created($"/widgets/{widget.Name}", widget));
+ return TypedResults.Created($"/widgets/{widget.Name}", widget);
+});
+
+app.MapPost("/widgets/custom-validation", Results> (WidgetWithCustomValidation widget, IMiniValidator validator) =>
+{
+ if (!validator.TryValidate(widget, out var errors))
+ {
+ return TypedResults.ValidationProblem(errors);
+ }
+
+ return TypedResults.Created($"/widgets/{widget.Name}", widget);
+});
app.Run();
@@ -43,13 +66,34 @@ class Widget
public override string? ToString() => Name;
}
+class WidgetWithClassValidator : Widget
+{
+ [Required, MinLength(3), Display(Name = "Widget name")]
+ public string? Name { get; set; }
+
+ public override string? ToString() => Name;
+}
+
class WidgetWithCustomValidation : Widget, IValidatableObject
{
public IEnumerable Validate(ValidationContext validationContext)
{
if (string.Equals(Name, "Widget", StringComparison.OrdinalIgnoreCase))
{
- yield return new($"Cannot name a widget '{Name}'.", new[] { nameof(Name) });
+ yield return new($"Cannot name a widget '{Name}'.", [nameof(Name)]);
+ }
+ }
+}
+
+class WidgetValidator : IValidate
+{
+ public IEnumerable Validate(WidgetWithClassValidator instance, ValidationContext validationContext)
+ {
+ if (string.Equals(instance.Name, "Widget", StringComparison.OrdinalIgnoreCase))
+ {
+ return [new($"Cannot name a widget '{instance.Name}'.", [nameof(instance.Name)])];
}
+
+ return [];
}
}
diff --git a/samples/Samples.Web/Samples.Web.csproj b/samples/Samples.Web/Samples.Web.csproj
index 7190ba0..f3b5e57 100644
--- a/samples/Samples.Web/Samples.Web.csproj
+++ b/samples/Samples.Web/Samples.Web.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
enable
enable
diff --git a/src/MiniValidation/IAsyncValidatableObject.cs b/src/MiniValidation/IAsyncValidatableObject.cs
index 2fde10e..0140589 100644
--- a/src/MiniValidation/IAsyncValidatableObject.cs
+++ b/src/MiniValidation/IAsyncValidatableObject.cs
@@ -14,5 +14,9 @@ public interface IAsyncValidatableObject
///
/// The validation context.
/// A collection that holds failed-validation information.
+#if NET6_0_OR_GREATER
+ ValueTask> ValidateAsync(ValidationContext validationContext);
+#else
Task> ValidateAsync(ValidationContext validationContext);
+#endif
}
diff --git a/src/MiniValidation/IAsyncValidate.cs b/src/MiniValidation/IAsyncValidate.cs
new file mode 100644
index 0000000..b84eaea
--- /dev/null
+++ b/src/MiniValidation/IAsyncValidate.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Threading.Tasks;
+
+namespace MiniValidation;
+
+///
+/// Provides a way to add a validator for a type outside the class.
+///
+/// The type to validate.
+public interface IAsyncValidate
+{
+ ///
+ /// Determines whether the specified object is valid.
+ ///
+ /// The object instance to validate.
+ /// The validation context.
+ /// A collection that holds failed-validation information.
+#if NET6_0_OR_GREATER
+ ValueTask> ValidateAsync(T instance, ValidationContext validationContext);
+#else
+ Task> ValidateAsync(T instance, ValidationContext validationContext);
+#endif
+}
\ No newline at end of file
diff --git a/src/MiniValidation/IMiniValidator.cs b/src/MiniValidation/IMiniValidator.cs
new file mode 100644
index 0000000..71b4b1c
--- /dev/null
+++ b/src/MiniValidation/IMiniValidator.cs
@@ -0,0 +1,150 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace MiniValidation;
+
+///
+/// Represents a validator that can validate an object.
+///
+public interface IMiniValidator
+{
+ ///
+ /// Determines if the specified has anything to validate.
+ ///
+ ///
+ /// Objects of types with nothing to validate will always return true when passed to .
+ ///
+ /// The .
+ /// true to recursively check descendant types; if false only simple values directly on the target type are checked.
+ /// true if has anything to validate, false if not.
+ /// Thrown when is null.
+ bool RequiresValidation(Type targetType, bool recurse = true);
+
+ ///
+ /// Determines whether the specific object is valid. This method recursively validates descendant objects.
+ ///
+ /// The object to validate.
+ /// A dictionary that contains details of each failed validation.
+ /// true if is valid; otherwise false.
+ /// Thrown when is null.
+ bool TryValidate(TTarget target, out IDictionary errors);
+
+ ///
+ /// Determines whether the specific object is valid.
+ ///
+ /// The type of the target of validation.
+ /// The object to validate.
+ /// true to recursively validate descendant objects; if false only simple values directly on are validated.
+ /// A dictionary that contains details of each failed validation.
+ /// true if is valid; otherwise false.
+ /// Thrown when is null.
+ bool TryValidate(TTarget target, bool recurse, out IDictionary errors);
+
+ ///
+ /// Determines whether the specific object is valid.
+ ///
+ ///
+ /// The object to validate.
+ /// true to recursively validate descendant objects; if false only simple values directly on are validated.
+ /// true to allow asynchronous validation if an object in the graph requires it.
+ /// A dictionary that contains details of each failed validation.
+ /// true if is valid; otherwise false.
+ /// Thrown when is null.
+ /// Throw when requires async validation and is false.
+ bool TryValidate(TTarget target, bool recurse, bool allowAsync, out IDictionary errors);
+
+ ///
+ /// Determines whether the specific object is valid.
+ ///
+ /// The object to validate.
+ /// Thrown when is null.
+#if NET6_0_OR_GREATER
+ ValueTask<(bool IsValid, IDictionary Errors)> TryValidateAsync(TTarget target);
+#else
+ Task<(bool IsValid, IDictionary Errors)> TryValidateAsync(TTarget target);
+#endif
+
+ ///
+ /// Determines whether the specific object is valid.
+ ///
+ /// The object to validate.
+ /// true to recursively validate descendant objects; if false only simple values directly on are validated.
+ /// true if is valid; otherwise false and the validation errors.
+ ///
+#if NET6_0_OR_GREATER
+ ValueTask<(bool IsValid, IDictionary Errors)> TryValidateAsync(TTarget target, bool recurse);
+#else
+ Task<(bool IsValid, IDictionary Errors)> TryValidateAsync(TTarget target, bool recurse);
+#endif
+
+ ///
+ /// Gets a validator for the specified target type.
+ ///
+ ///
+ ///
+ IMiniValidator GetValidator();
+}
+
+///
+/// Represents a validator that can validate an object of type .
+///
+///
+public interface IMiniValidator
+{
+
+ ///
+ /// Determines whether the specific object is valid. This method recursively validates descendant objects.
+ ///
+ /// The object to validate.
+ /// A dictionary that contains details of each failed validation.
+ /// true if is valid; otherwise false.
+ /// Thrown when is null.
+ bool TryValidate(T target, out IDictionary errors);
+
+ ///
+ /// Determines whether the specific object is valid.
+ ///
+ /// The object to validate.
+ /// true to recursively validate descendant objects; if false only simple values directly on are validated.
+ /// A dictionary that contains details of each failed validation.
+ /// true if is valid; otherwise false.
+ /// Thrown when is null.
+ bool TryValidate(T target, bool recurse, out IDictionary errors);
+
+ ///
+ /// Determines whether the specific object is valid.
+ ///
+ /// The object to validate.
+ /// true to recursively validate descendant objects; if false only simple values directly on are validated.
+ /// true to allow asynchronous validation if an object in the graph requires it.
+ /// A dictionary that contains details of each failed validation.
+ /// true if is valid; otherwise false.
+ /// Thrown when is null.
+ /// Throw when requires async validation and is false.
+ bool TryValidate(T target, bool recurse, bool allowAsync, out IDictionary errors);
+
+ ///
+ /// Determines whether the specific object is valid.
+ ///
+ /// The object to validate.
+ /// Thrown when is null.
+#if NET6_0_OR_GREATER
+ ValueTask<(bool IsValid, IDictionary Errors)> TryValidateAsync(T target);
+#else
+ Task<(bool IsValid, IDictionary Errors)> TryValidateAsync(T target);
+#endif
+
+ ///
+ /// Determines whether the specific object is valid.
+ ///
+ /// The object to validate.
+ /// true to recursively validate descendant objects; if false only simple values directly on are validated.
+ /// true if is valid; otherwise false and the validation errors.
+ ///
+#if NET6_0_OR_GREATER
+ ValueTask<(bool IsValid, IDictionary Errors)> TryValidateAsync(T target, bool recurse);
+#else
+ Task<(bool IsValid, IDictionary Errors)> TryValidateAsync(T target, bool recurse);
+#endif
+}
\ No newline at end of file
diff --git a/src/MiniValidation/IValidate.cs b/src/MiniValidation/IValidate.cs
new file mode 100644
index 0000000..cddfc22
--- /dev/null
+++ b/src/MiniValidation/IValidate.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace MiniValidation;
+
+///
+/// Provides a way to add a validator for a type outside the class.
+///
+/// The type to validate.
+public interface IValidate
+{
+ ///
+ /// Determines whether the specified object is valid.
+ ///
+ /// The object instance to validate.
+ /// The validation context.
+ /// A collection that holds failed-validation information.
+ IEnumerable Validate(T instance, ValidationContext validationContext);
+}
\ No newline at end of file
diff --git a/src/MiniValidation/Internal/MiniValidatorImpl.cs b/src/MiniValidation/Internal/MiniValidatorImpl.cs
new file mode 100644
index 0000000..85570f7
--- /dev/null
+++ b/src/MiniValidation/Internal/MiniValidatorImpl.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace MiniValidation.Internal;
+
+internal class MiniValidatorImpl : IMiniValidator
+{
+ private readonly IServiceProvider _serviceProvider;
+
+ public MiniValidatorImpl(IServiceProvider serviceProvider)
+ {
+ _serviceProvider = serviceProvider;
+ }
+
+ public bool RequiresValidation(Type targetType, bool recurse = true)
+ {
+ return MiniValidator.RequiresValidation(targetType, recurse, _serviceProvider);
+ }
+
+ public bool TryValidate(TTarget target, out IDictionary errors)
+ {
+ return MiniValidator.TryValidate(target, _serviceProvider, out errors);
+ }
+
+ public bool TryValidate(TTarget target, bool recurse, out IDictionary errors)
+ {
+ return MiniValidator.TryValidate(target, _serviceProvider, recurse, out errors);
+ }
+
+ public bool TryValidate(TTarget target, bool recurse, bool allowAsync, out IDictionary errors)
+ {
+ return MiniValidator.TryValidate(target, _serviceProvider, recurse, allowAsync, out errors);
+ }
+
+#if NET6_0_OR_GREATER
+ public ValueTask<(bool IsValid, IDictionary Errors)> TryValidateAsync(TTarget target)
+#else
+ public Task<(bool IsValid, IDictionary Errors)> TryValidateAsync(TTarget target)
+#endif
+ {
+ return MiniValidator.TryValidateAsync(target, _serviceProvider);
+ }
+
+#if NET6_0_OR_GREATER
+ public ValueTask<(bool IsValid, IDictionary Errors)> TryValidateAsync(TTarget target, bool recurse)
+#else
+ public Task<(bool IsValid, IDictionary Errors)> TryValidateAsync(TTarget target, bool recurse)
+#endif
+ {
+ return MiniValidator.TryValidateAsync(target, _serviceProvider, recurse);
+ }
+
+ public IMiniValidator GetValidator()
+ {
+ return new MiniValidatorImpl(_serviceProvider);
+ }
+}
+
+internal class MiniValidatorImpl : IMiniValidator
+{
+ private readonly IServiceProvider _serviceProvider;
+
+ public MiniValidatorImpl(IServiceProvider serviceProvider)
+ {
+ _serviceProvider = serviceProvider;
+ }
+
+ public bool TryValidate(T target, out IDictionary errors)
+ {
+ return MiniValidator.TryValidate(target, _serviceProvider, out errors);
+ }
+
+ public bool TryValidate(T target, bool recurse, out IDictionary errors)
+ {
+ return MiniValidator.TryValidate(target, _serviceProvider, recurse, out errors);
+ }
+
+ public bool TryValidate(T target, bool recurse, bool allowAsync, out IDictionary errors)
+ {
+ return MiniValidator.TryValidate(target, _serviceProvider, recurse, allowAsync, out errors);
+ }
+
+#if NET6_0_OR_GREATER
+ public ValueTask<(bool IsValid, IDictionary Errors)> TryValidateAsync(T target)
+#else
+ public Task<(bool IsValid, IDictionary Errors)> TryValidateAsync(T target)
+#endif
+ {
+ return MiniValidator.TryValidateAsync(target, _serviceProvider);
+ }
+
+#if NET6_0_OR_GREATER
+ public ValueTask<(bool IsValid, IDictionary Errors)> TryValidateAsync(T target, bool recurse)
+#else
+ public Task<(bool IsValid, IDictionary Errors)> TryValidateAsync(T target, bool recurse)
+#endif
+ {
+ return MiniValidator.TryValidateAsync(target, _serviceProvider, recurse);
+ }
+}
\ No newline at end of file
diff --git a/src/MiniValidation/MiniValidation.csproj b/src/MiniValidation/MiniValidation.csproj
index 7f2afc4..90b46b5 100644
--- a/src/MiniValidation/MiniValidation.csproj
+++ b/src/MiniValidation/MiniValidation.csproj
@@ -14,6 +14,7 @@
+
all
diff --git a/src/MiniValidation/MiniValidator.cs b/src/MiniValidation/MiniValidator.cs
index ca7c3b6..bece8c2 100644
--- a/src/MiniValidation/MiniValidator.cs
+++ b/src/MiniValidation/MiniValidator.cs
@@ -1,11 +1,14 @@
-using System;
+using System;
using System.Collections;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
+using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
namespace MiniValidation;
@@ -16,8 +19,19 @@ namespace MiniValidation;
public static class MiniValidator
{
private static readonly TypeDetailsCache _typeDetailsCache = new();
+ private static readonly ConcurrentDictionary _validateTypesCache = new();
+ private static readonly ConcurrentDictionary _asyncValidateTypesCache = new();
+ private static readonly ConcurrentDictionary _validateMethodsCache = new();
+ private static readonly ConcurrentDictionary _asyncValidateMethodsCache = new();
private static readonly IDictionary _emptyErrors = new ReadOnlyDictionary(new Dictionary());
+ private delegate IEnumerable Validate(object instance, ValidationContext validationContext);
+#if NET6_0_OR_GREATER
+ private delegate ValueTask> ValidateAsync(object instance, ValidationContext validationContext);
+#else
+ private delegate Task> ValidateAsync(object instance, ValidationContext validationContext);
+#endif
+
///
/// Gets or sets the maximum depth allowed when validating an object with recursion enabled.
/// Defaults to 32.
@@ -32,19 +46,47 @@ public static class MiniValidator
///
/// The .
/// true to recursively check descendant types; if false only simple values directly on the target type are checked.
+ /// The service provider to use when checking for validators.
/// true if has anything to validate, false if not.
/// Thrown when is null.
- public static bool RequiresValidation(Type targetType, bool recurse = true)
+ public static bool RequiresValidation(Type targetType, bool recurse = true, IServiceProvider? serviceProvider = null)
{
if (targetType is null)
{
throw new ArgumentNullException(nameof(targetType));
}
-
+
return typeof(IValidatableObject).IsAssignableFrom(targetType)
|| typeof(IAsyncValidatableObject).IsAssignableFrom(targetType)
|| (recurse && typeof(IEnumerable).IsAssignableFrom(targetType))
- || _typeDetailsCache.Get(targetType).Properties.Any(p => p.HasValidationAttributes || recurse);
+ || _typeDetailsCache.Get(targetType).Properties.Any(p => p.HasValidationAttributes || recurse)
+ || HasValidatorsRegistered(targetType, serviceProvider);
+ }
+
+ private static bool HasValidatorsRegistered(Type targetType, IServiceProvider? serviceProvider)
+ {
+ if (serviceProvider == null)
+ return false;
+
+ var validatorType = GetValidatorType(targetType);
+ var asyncValidatorType = GetAsyncValidatorType(targetType);
+ var serviceProviderIsService = serviceProvider.GetService();
+ if (serviceProviderIsService != null)
+ {
+ return serviceProviderIsService.IsService(validatorType) || serviceProviderIsService.IsService(asyncValidatorType);
+ }
+
+ return serviceProvider.GetService(validatorType) != null || serviceProvider.GetService(asyncValidatorType) != null;
+ }
+
+ private static Type GetValidatorType(Type targetType)
+ {
+ return _validateTypesCache.GetOrAdd(targetType, t => typeof(IEnumerable<>).MakeGenericType(typeof(IValidate<>).MakeGenericType(t)));
+ }
+
+ private static Type GetAsyncValidatorType(Type targetType)
+ {
+ return _asyncValidateTypesCache.GetOrAdd(targetType, t => typeof(IEnumerable<>).MakeGenericType(typeof(IAsyncValidate<>).MakeGenericType(t)));
}
///
@@ -163,7 +205,7 @@ private static bool TryValidateImpl(TTarget target, IServiceProvider? s
throw new ArgumentNullException(nameof(target));
}
- if (!RequiresValidation(target.GetType(), recurse))
+ if (!RequiresValidation(target.GetType(), recurse, serviceProvider))
{
errors = _emptyErrors;
@@ -306,7 +348,7 @@ private static bool TryValidateImpl(TTarget target, IServiceProvider? s
IDictionary? errors;
- if (!RequiresValidation(target.GetType(), recurse))
+ if (!RequiresValidation(target.GetType(), recurse, serviceProvider))
{
errors = _emptyErrors;
@@ -415,6 +457,7 @@ private static async Task TryValidateImpl(
(property.Recurse
|| typeof(IValidatableObject).IsAssignableFrom(propertyValueType)
|| typeof(IAsyncValidatableObject).IsAssignableFrom(propertyValueType)
+ || serviceProvider?.GetService(typeof(IAsyncValidate<>).MakeGenericType(propertyValueType!)) != null
|| properties.Any(p => p.Recurse)))
{
propertiesToRecurse!.Add(property, propertyValue);
@@ -506,11 +549,8 @@ private static async Task TryValidateImpl(
validationContext.DisplayName = validationContext.ObjectType.Name;
var validatableResults = validatable.Validate(validationContext);
- if (validatableResults is not null)
- {
- ProcessValidationResults(validatableResults, workingErrors, prefix);
- isValid = workingErrors.Count == 0 && isValid;
- }
+ ProcessValidationResults(validatableResults, workingErrors, prefix);
+ isValid = workingErrors.Count == 0 && isValid;
}
if (isValid && typeof(IAsyncValidatableObject).IsAssignableFrom(targetType))
@@ -525,8 +565,114 @@ private static async Task TryValidateImpl(
ThrowIfAsyncNotAllowed(validateTask.IsCompleted, allowAsync);
var validatableResults = await validateTask.ConfigureAwait(false);
- if (validatableResults is not null)
+ ProcessValidationResults(validatableResults, workingErrors, prefix);
+ isValid = workingErrors.Count == 0 && isValid;
+ }
+
+ if (isValid)
+ {
+ var validatorType = GetValidatorType(targetType);
+
+ var validators = new List