Skip to content

Commit

Permalink
Make ValidationContext support IServiceProvider. (#48)
Browse files Browse the repository at this point in the history
* Make `ValidationContext` support `IServiceProvider`.
* Bump to version 0.8.0
  • Loading branch information
maliming authored Jun 22, 2023
1 parent f9e3d41 commit 81b5c92
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 18 deletions.
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" />
<PackageVersion Include="coverlet.collector" Version="3.2.0" />
<PackageVersion Include="BenchmarkDotNet" Version="0.13.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
</ItemGroup>
</Project>
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>

<PropertyGroup>
<VersionPrefix>0.7.4</VersionPrefix>
<VersionPrefix>0.8.0</VersionPrefix>
<!-- VersionSuffix used for local builds -->
<VersionSuffix>dev</VersionSuffix>
<!-- VersionSuffix to be used for CI builds -->
Expand Down
162 changes: 146 additions & 16 deletions src/MiniValidation/MiniValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,25 @@ public static bool RequiresValidation(Type targetType, bool recurse = true)
/// <exception cref="ArgumentNullException">Thrown when <paramref name="target"/> is <c>null</c>.</exception>
public static bool TryValidate<TTarget>(TTarget target, out IDictionary<string, string[]> errors)
{
if (target is null)
return TryValidateImpl(target, null, recurse: true, allowAsync: false, out errors);
}

/// <summary>
/// Determines whether the specific object is valid. This method recursively validates descendant objects.
/// </summary>
/// <param name="target">The object to validate.</param>
/// <param name="serviceProvider">The service provider to use when creating ValidationContext.</param>
/// <param name="errors">A dictionary that contains details of each failed validation.</param>
/// <returns><c>true</c> if <paramref name="target"/> is valid; otherwise <c>false</c>.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="target"/> is <c>null</c>.</exception>
public static bool TryValidate<TTarget>(TTarget target, IServiceProvider serviceProvider, out IDictionary<string, string[]> errors)
{
if (serviceProvider is null)
{
throw new ArgumentNullException(nameof(target));
throw new ArgumentNullException(nameof(serviceProvider));
}

return TryValidate(target, recurse: true, allowAsync: false, out errors);
return TryValidateImpl(target, serviceProvider, recurse: true, allowAsync: false, out errors);
}

/// <summary>
Expand All @@ -75,12 +88,27 @@ public static bool TryValidate<TTarget>(TTarget target, out IDictionary<string,
/// <exception cref="ArgumentNullException">Thrown when <paramref name="target"/> is <c>null</c>.</exception>
public static bool TryValidate<TTarget>(TTarget target, bool recurse, out IDictionary<string, string[]> errors)
{
if (target is null)
return TryValidateImpl(target, null, recurse, allowAsync: false, out errors);
}

/// <summary>
/// Determines whether the specific object is valid.
/// </summary>
/// <typeparam name="TTarget">The type of the target of validation.</typeparam>
/// <param name="target">The object to validate.</param>
/// <param name="serviceProvider">The service provider to use when creating ValidationContext.</param>
/// <param name="recurse"><c>true</c> to recursively validate descendant objects; if <c>false</c> only simple values directly on <paramref name="target"/> are validated.</param>
/// <param name="errors">A dictionary that contains details of each failed validation.</param>
/// <returns><c>true</c> if <paramref name="target"/> is valid; otherwise <c>false</c>.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="target"/> is <c>null</c>.</exception>
public static bool TryValidate<TTarget>(TTarget target, IServiceProvider serviceProvider, bool recurse, out IDictionary<string, string[]> errors)
{
if (serviceProvider is null)
{
throw new ArgumentNullException(nameof(target));
throw new ArgumentNullException(nameof(serviceProvider));
}

return TryValidate(target, recurse, allowAsync: false, out errors);
return TryValidateImpl(target, serviceProvider, recurse, allowAsync: false, out errors);
}

/// <summary>
Expand All @@ -95,6 +123,40 @@ public static bool TryValidate<TTarget>(TTarget target, bool recurse, out IDicti
/// <exception cref="ArgumentNullException">Thrown when <paramref name="target"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">Throw when <paramref name="target"/> requires async validation and <paramref name="allowAsync"/> is <c>false</c>.</exception>
public static bool TryValidate<TTarget>(TTarget target, bool recurse, bool allowAsync, out IDictionary<string, string[]> errors)
{
return TryValidateImpl(target, null, recurse, allowAsync, out errors);
}

/// <summary>
/// Determines whether the specific object is valid.
/// </summary>
/// <typeparam name="TTarget"></typeparam>
/// <param name="target">The object to validate.</param>
/// <param name="serviceProvider">The service provider to use when creating ValidationContext.</param>
/// <param name="recurse"><c>true</c> to recursively validate descendant objects; if <c>false</c> only simple values directly on <paramref name="target"/> are validated.</param>
/// <param name="allowAsync"><c>true</c> to allow asynchronous validation if an object in the graph requires it.</param>
/// <param name="errors">A dictionary that contains details of each failed validation.</param>
/// <returns><c>true</c> if <paramref name="target"/> is valid; otherwise <c>false</c>.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="target"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">Throw when <paramref name="target"/> requires async validation and <paramref name="allowAsync"/> is <c>false</c>.</exception>
public static bool TryValidate<TTarget>(TTarget target, IServiceProvider? serviceProvider, bool recurse, bool allowAsync, out IDictionary<string, string[]> errors)
{
return TryValidateImpl(target, serviceProvider, recurse, allowAsync, out errors);
}

/// <summary>
/// Determines whether the specific object is valid.
/// </summary>
/// <typeparam name="TTarget"></typeparam>
/// <param name="target">The object to validate.</param>
/// <param name="serviceProvider">The service provider to use when creating ValidationContext.</param>
/// <param name="recurse"><c>true</c> to recursively validate descendant objects; if <c>false</c> only simple values directly on <paramref name="target"/> are validated.</param>
/// <param name="allowAsync"><c>true</c> to allow asynchronous validation if an object in the graph requires it.</param>
/// <param name="errors">A dictionary that contains details of each failed validation.</param>
/// <returns><c>true</c> if <paramref name="target"/> is valid; otherwise <c>false</c>.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="target"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">Throw when <paramref name="target"/> requires async validation and <paramref name="allowAsync"/> is <c>false</c>.</exception>
private static bool TryValidateImpl<TTarget>(TTarget target, IServiceProvider? serviceProvider, bool recurse, bool allowAsync, out IDictionary<string, string[]> errors)
{
if (target is null)
{
Expand All @@ -117,7 +179,7 @@ public static bool TryValidate<TTarget>(TTarget target, bool recurse, bool allow
var validatedObjects = new Dictionary<object, bool?>();
var workingErrors = new Dictionary<string, List<string>>();

var validateTask = TryValidateImpl(target, recurse, allowAsync, workingErrors, validatedObjects);
var validateTask = TryValidateImpl(target, serviceProvider, recurse, allowAsync, workingErrors, validatedObjects);

bool isValid;

Expand Down Expand Up @@ -154,12 +216,27 @@ public static bool TryValidate<TTarget>(TTarget target, bool recurse, bool allow
public static Task<(bool IsValid, IDictionary<string, string[]> Errors)> TryValidateAsync<TTarget>(TTarget target)
#endif
{
if (target is null)
return TryValidateImplAsync(target, null, recurse: true);
}

/// <summary>
/// Determines whether the specific object is valid.
/// </summary>
/// <param name="target">The object to validate.</param>
/// <param name="serviceProvider">The service provider to use when creating ValidationContext.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="target"/> is <c>null</c>.</exception>
#if NET6_0_OR_GREATER
public static ValueTask<(bool IsValid, IDictionary<string, string[]> Errors)> TryValidateAsync<TTarget>(TTarget target, IServiceProvider serviceProvider)
#else
public static Task<(bool IsValid, IDictionary<string, string[]> Errors)> TryValidateAsync<TTarget>(TTarget target, IServiceProvider serviceProvider)
#endif
{
if (serviceProvider is null)
{
throw new ArgumentNullException(nameof(target));
throw new ArgumentNullException(nameof(serviceProvider));
}

return TryValidateAsync(target, recurse: true);
return TryValidateImplAsync(target, serviceProvider, recurse: true);
}

/// <summary>
Expand All @@ -173,6 +250,40 @@ public static bool TryValidate<TTarget>(TTarget target, bool recurse, bool allow
public static ValueTask<(bool IsValid, IDictionary<string, string[]> Errors)> TryValidateAsync<TTarget>(TTarget target, bool recurse)
#else
public static Task<(bool IsValid, IDictionary<string, string[]> Errors)> TryValidateAsync<TTarget>(TTarget target, bool recurse)
#endif
{
return TryValidateImplAsync(target, null, recurse);
}

/// <summary>
/// Determines whether the specific object is valid.
/// </summary>
/// <param name="target">The object to validate.</param>
/// <param name="serviceProvider">The service provider to use when creating ValidationContext.</param>
/// <param name="recurse"><c>true</c> to recursively validate descendant objects; if <c>false</c> only simple values directly on <paramref name="target"/> are validated.</param>
/// <returns><c>true</c> if <paramref name="target"/> is valid; otherwise <c>false</c> and the validation errors.</returns>
/// <exception cref="ArgumentNullException"></exception>
#if NET6_0_OR_GREATER
public static ValueTask<(bool IsValid, IDictionary<string, string[]> Errors)> TryValidateAsync<TTarget>(TTarget target, IServiceProvider? serviceProvider, bool recurse)
#else
public static Task<(bool IsValid, IDictionary<string, string[]> Errors)> TryValidateAsync<TTarget>(TTarget target, IServiceProvider? serviceProvider, bool recurse)
#endif
{
return TryValidateImplAsync(target, serviceProvider, recurse);
}

/// <summary>
/// Determines whether the specific object is valid.
/// </summary>
/// <param name="target">The object to validate.</param>
/// <param name="serviceProvider">The service provider to use when creating ValidationContext.</param>
/// <param name="recurse"><c>true</c> to recursively validate descendant objects; if <c>false</c> only simple values directly on <paramref name="target"/> are validated.</param>
/// <returns><c>true</c> if <paramref name="target"/> is valid; otherwise <c>false</c> and the validation errors.</returns>
/// <exception cref="ArgumentNullException"></exception>
#if NET6_0_OR_GREATER
private static ValueTask<(bool IsValid, IDictionary<string, string[]> Errors)> TryValidateImplAsync<TTarget>(TTarget target, IServiceProvider? serviceProvider, bool recurse)
#else
private static Task<(bool IsValid, IDictionary<string, string[]> Errors)> TryValidateImplAsync<TTarget>(TTarget target, IServiceProvider? serviceProvider, bool recurse)
#endif
{
if (target is null)
Expand All @@ -196,7 +307,7 @@ public static bool TryValidate<TTarget>(TTarget target, bool recurse, bool allow

var validatedObjects = new Dictionary<object, bool?>();
var workingErrors = new Dictionary<string, List<string>>();
var validationTask = TryValidateImpl(target, recurse, allowAsync: true, workingErrors, validatedObjects);
var validationTask = TryValidateImpl(target, serviceProvider, recurse, allowAsync: true, workingErrors, validatedObjects);

if (validationTask.IsCompleted)
{
Expand Down Expand Up @@ -233,6 +344,7 @@ private static async ValueTask<bool> TryValidateImpl(
private static async Task<bool> TryValidateImpl(
#endif
object target,
IServiceProvider? serviceProvider,
bool recurse,
bool allowAsync,
Dictionary<string, List<string>> workingErrors,
Expand Down Expand Up @@ -264,7 +376,7 @@ private static async Task<bool> TryValidateImpl(

var isValid = true;
var propertiesToRecurse = recurse ? new Dictionary<PropertyDetails, object>() : null;
ValidationContext validationContext = new(target);
var validationContext = new ValidationContext(target, serviceProvider: serviceProvider, items: null);

foreach (var property in typeProperties)
{
Expand Down Expand Up @@ -329,7 +441,7 @@ private static async Task<bool> TryValidateImpl(
else
{
var thePrefix = $"{prefix}{propertyDetails.Name}."; // <-- Note trailing '.' here
var task = TryValidateImpl(propertyValue, recurse, allowAsync, workingErrors, validatedObjects, validationResults, thePrefix, currentDepth + 1);
var task = TryValidateImpl(propertyValue, serviceProvider, recurse, allowAsync, workingErrors, validatedObjects, validationResults, thePrefix, currentDepth + 1);
ThrowIfAsyncNotAllowed(task, allowAsync);
isValid = await task.ConfigureAwait(false) && isValid;
}
Expand All @@ -341,11 +453,11 @@ private static async Task<bool> TryValidateImpl(
if (isValid && typeof(IValidatableObject).IsAssignableFrom(targetType))
{
var validatable = (IValidatableObject)target;

// Reset validation context
validationContext.MemberName = null;
validationContext.DisplayName = validationContext.ObjectType.Name;

var validatableResults = validatable.Validate(validationContext);
if (validatableResults is not null)
{
Expand Down Expand Up @@ -422,6 +534,24 @@ private static async Task<bool> TryValidateEnumerable(
List<ValidationResult>? validationResults,
string? prefix = null,
int currentDepth = 0)
{
return await TryValidateEnumerable(target, null, recurse, allowAsync, workingErrors, validatedObjects, validationResults, prefix, currentDepth);
}

#if NET6_0_OR_GREATER
private static async ValueTask<bool> TryValidateEnumerable(
#else
private static async Task<bool> TryValidateEnumerable(
#endif
object target,
IServiceProvider? serviceProvider,
bool recurse,
bool allowAsync,
Dictionary<string, List<string>> workingErrors,
Dictionary<object, bool?> validatedObjects,
List<ValidationResult>? validationResults,
string? prefix = null,
int currentDepth = 0)
{
var isValid = true;
if (target is IEnumerable items)
Expand All @@ -437,7 +567,7 @@ private static async Task<bool> TryValidateEnumerable(

var itemPrefix = $"{prefix}[{index}].";

var task = TryValidateImpl(item, recurse, allowAsync, workingErrors, validatedObjects, validationResults, itemPrefix, currentDepth + 1);
var task = TryValidateImpl(item, serviceProvider, recurse, allowAsync, workingErrors, validatedObjects, validationResults, itemPrefix, currentDepth + 1);
ThrowIfAsyncNotAllowed(task, allowAsync);
isValid = await task.ConfigureAwait(false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
</ItemGroup>

</Project>
34 changes: 34 additions & 0 deletions tests/MiniValidation.UnitTests/TestTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,17 @@ public IEnumerable<ValidationResult> Validate(ValidationContext validationContex
}
}

class TestClassLevelValidatableOnlyTypeWithServiceProvider : IValidatableObject
{
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (validationContext.GetService(typeof(TestService)) == null)
{
yield return new ValidationResult($"This validationContext did not support ServiceProvider.", new[] { nameof(IServiceProvider) });
}
}
}

class TestClassLevelAsyncValidatableOnlyType : IAsyncValidatableObject
{
public int TwentyOrMore { get; set; } = 20;
Expand All @@ -89,6 +100,29 @@ public async Task<IEnumerable<ValidationResult>> ValidateAsync(ValidationContext
}
}

class TestClassLevelAsyncValidatableOnlyTypeWithServiceProvider : IAsyncValidatableObject
{
public async Task<IEnumerable<ValidationResult>> ValidateAsync(ValidationContext validationContext)
{
await Task.Yield();

List<ValidationResult>? errors = null;

if (validationContext.GetService(typeof(TestService)) == null)
{
errors ??= new List<ValidationResult>();
errors.Add(new ValidationResult($"This validationContext did not support ServiceProvider.", new[] { nameof(IServiceProvider) }));
}

return errors ?? Enumerable.Empty<ValidationResult>();
}
}

class TestService
{

}

class TestChildType
{
[Required]
Expand Down
43 changes: 42 additions & 1 deletion tests/MiniValidation.UnitTests/TryValidate.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
using Microsoft.Extensions.DependencyInjection;

namespace MiniValidation.UnitTests;

Expand Down Expand Up @@ -355,4 +356,44 @@ public void Invalid_When_Target_Has_Required_Uri_Property_With_Null_Value()
Assert.Equal(1, errors.Count);
Assert.Equal(nameof(ClassWithUri.BaseAddress), errors.Keys.First());
}
}

[Fact]
public void TryValidate_With_ServiceProvider()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<TestService>();
var serviceProvider = serviceCollection.BuildServiceProvider();

var thingToValidate = new TestClassLevelValidatableOnlyTypeWithServiceProvider();

var result = MiniValidator.TryValidate(thingToValidate, serviceProvider, out var errors);
Assert.True(result);

errors.Clear();
result = MiniValidator.TryValidate(thingToValidate, out errors);
Assert.False(result);
Assert.Equal(1, errors.Count);
Assert.Equal(nameof(IServiceProvider), errors.Keys.First());
}

[Fact]
public async Task TryValidateAsync_With_ServiceProvider()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<TestService>();
var serviceProvider = serviceCollection.BuildServiceProvider();

var thingToValidate = new TestClassLevelValidatableOnlyTypeWithServiceProvider();

var (isValid, errors) = await MiniValidator.TryValidateAsync(thingToValidate, serviceProvider);

Assert.True(isValid);
Assert.Equal(0, errors.Count);

errors.Clear();
(isValid, errors) = await MiniValidator.TryValidateAsync(thingToValidate);
Assert.False(isValid);
Assert.Equal(1, errors.Count);
Assert.Equal(nameof(IServiceProvider), errors.Keys.First());
}
}

0 comments on commit 81b5c92

Please sign in to comment.