Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for external class validators #67

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="BenchmarkDotNet" Version="0.13.12" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
</ItemGroup>
</Project>
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<IMiniValidator>();
var isValid = validator.TryValidate(widget, out var errors);

class Widget : IValidatableObject
{
Expand Down
2 changes: 1 addition & 1 deletion samples/Samples.Console/Samples.Console.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

Expand Down
62 changes: 53 additions & 9 deletions samples/Samples.Web/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddMiniValidator();
builder.Services.AddClassMiniValidator<WidgetValidator>();

var app = builder.Build();

Expand All @@ -23,15 +25,36 @@
app.MapGet("/widgets/{name}", (string name) =>
new Widget { Name = name });

app.MapPost("/widgets", Results<ValidationProblem, Created<Widget>> (Widget widget) =>
!MiniValidator.TryValidate(widget, out var errors)
? TypedResults.ValidationProblem(errors)
: TypedResults.Created($"/widgets/{widget.Name}", widget));
app.MapPost("/widgets", Results<ValidationProblem, Created<Widget>> (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<Results<ValidationProblem, Created<WidgetWithClassValidator>>> (WidgetWithClassValidator widget, IMiniValidator<WidgetWithClassValidator> validator) =>
{
var (isValid, errors) = await validator.TryValidateAsync(widget);
if (!isValid)
{
return TypedResults.ValidationProblem(errors);
}

app.MapPost("/widgets/custom-validation", Results<ValidationProblem, Created<WidgetWithCustomValidation>> (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<ValidationProblem, Created<WidgetWithCustomValidation>> (WidgetWithCustomValidation widget, IMiniValidator<WidgetWithCustomValidation> validator) =>
{
if (!validator.TryValidate(widget, out var errors))
{
return TypedResults.ValidationProblem(errors);
}

return TypedResults.Created($"/widgets/{widget.Name}", widget);
});

app.Run();

Expand All @@ -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<ValidationResult> 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<WidgetWithClassValidator>
{
public IEnumerable<ValidationResult> 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 [];
}
}
2 changes: 1 addition & 1 deletion samples/Samples.Web/Samples.Web.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
Expand Down
4 changes: 4 additions & 0 deletions src/MiniValidation/IAsyncValidatableObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,9 @@ public interface IAsyncValidatableObject
/// </summary>
/// <param name="validationContext">The validation context.</param>
/// <returns>A collection that holds failed-validation information.</returns>
#if NET6_0_OR_GREATER
ValueTask<IEnumerable<ValidationResult>> ValidateAsync(ValidationContext validationContext);
#else
Task<IEnumerable<ValidationResult>> ValidateAsync(ValidationContext validationContext);
#endif
}
24 changes: 24 additions & 0 deletions src/MiniValidation/IAsyncValidate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;

namespace MiniValidation;

/// <summary>
/// Provides a way to add a validator for a type outside the class.
/// </summary>
/// <typeparam name="T">The type to validate.</typeparam>
public interface IAsyncValidate<in T>
{
/// <summary>
/// Determines whether the specified object is valid.
/// </summary>
/// <param name="instance">The object instance to validate.</param>
/// <param name="validationContext">The validation context.</param>
/// <returns>A collection that holds failed-validation information.</returns>
#if NET6_0_OR_GREATER
ValueTask<IEnumerable<ValidationResult>> ValidateAsync(T instance, ValidationContext validationContext);
#else
Task<IEnumerable<ValidationResult>> ValidateAsync(T instance, ValidationContext validationContext);
#endif
}
150 changes: 150 additions & 0 deletions src/MiniValidation/IMiniValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace MiniValidation;

/// <summary>
/// Represents a validator that can validate an object.
/// </summary>
public interface IMiniValidator
{
/// <summary>
/// Determines if the specified <see cref="Type"/> has anything to validate.
/// </summary>
/// <remarks>
/// Objects of types with nothing to validate will always return <c>true</c> when passed to <see cref="TryValidate{TTarget}(TTarget, bool, out IDictionary{string, string[]})"/>.
/// </remarks>
/// <param name="targetType">The <see cref="Type"/>.</param>
/// <param name="recurse"><c>true</c> to recursively check descendant types; if <c>false</c> only simple values directly on the target type are checked.</param>
/// <returns><c>true</c> if <paramref name="targetType"/> has anything to validate, <c>false</c> if not.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="targetType"/> is <c>null</c>.</exception>
bool RequiresValidation(Type targetType, bool recurse = true);

/// <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="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>
bool TryValidate<TTarget>(TTarget target, out IDictionary<string, string[]> 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="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>
bool TryValidate<TTarget>(TTarget target, bool recurse, out IDictionary<string, string[]> errors);

/// <summary>
/// Determines whether the specific object is valid.
/// </summary>
/// <typeparam name="TTarget"></typeparam>
/// <param name="target">The object to validate.</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>
bool TryValidate<TTarget>(TTarget target, bool recurse, bool allowAsync, out IDictionary<string, string[]> errors);

/// <summary>
/// Determines whether the specific object is valid.
/// </summary>
/// <param name="target">The object to validate.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="target"/> is <c>null</c>.</exception>
#if NET6_0_OR_GREATER
ValueTask<(bool IsValid, IDictionary<string, string[]> Errors)> TryValidateAsync<TTarget>(TTarget target);
#else
Task<(bool IsValid, IDictionary<string, string[]> Errors)> TryValidateAsync<TTarget>(TTarget target);
#endif

/// <summary>
/// Determines whether the specific object is valid.
/// </summary>
/// <param name="target">The object to validate.</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
ValueTask<(bool IsValid, IDictionary<string, string[]> Errors)> TryValidateAsync<TTarget>(TTarget target, bool recurse);
#else
Task<(bool IsValid, IDictionary<string, string[]> Errors)> TryValidateAsync<TTarget>(TTarget target, bool recurse);
#endif

/// <summary>
/// Gets a validator for the specified target type.
/// </summary>
/// <typeparam name="TTarget"></typeparam>
/// <returns></returns>
IMiniValidator<TTarget> GetValidator<TTarget>();
}

/// <summary>
/// Represents a validator that can validate an object of type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IMiniValidator<in T>
{

/// <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="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>
bool TryValidate(T target, out IDictionary<string, string[]> errors);

/// <summary>
/// Determines whether the specific object is valid.
/// </summary>
/// <param name="target">The object to validate.</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>
bool TryValidate(T target, bool recurse, out IDictionary<string, string[]> errors);

/// <summary>
/// Determines whether the specific object is valid.
/// </summary>
/// <param name="target">The object to validate.</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>
bool TryValidate(T target, bool recurse, bool allowAsync, out IDictionary<string, string[]> errors);

/// <summary>
/// Determines whether the specific object is valid.
/// </summary>
/// <param name="target">The object to validate.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="target"/> is <c>null</c>.</exception>
#if NET6_0_OR_GREATER
ValueTask<(bool IsValid, IDictionary<string, string[]> Errors)> TryValidateAsync(T target);
#else
Task<(bool IsValid, IDictionary<string, string[]> Errors)> TryValidateAsync(T target);
#endif

/// <summary>
/// Determines whether the specific object is valid.
/// </summary>
/// <param name="target">The object to validate.</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
ValueTask<(bool IsValid, IDictionary<string, string[]> Errors)> TryValidateAsync(T target, bool recurse);
#else
Task<(bool IsValid, IDictionary<string, string[]> Errors)> TryValidateAsync(T target, bool recurse);
#endif
}
19 changes: 19 additions & 0 deletions src/MiniValidation/IValidate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace MiniValidation;

/// <summary>
/// Provides a way to add a validator for a type outside the class.
/// </summary>
/// <typeparam name="T">The type to validate.</typeparam>
public interface IValidate<in T>
{
/// <summary>
/// Determines whether the specified object is valid.
/// </summary>
/// <param name="instance">The object instance to validate.</param>
/// <param name="validationContext">The validation context.</param>
/// <returns>A collection that holds failed-validation information.</returns>
IEnumerable<ValidationResult> Validate(T instance, ValidationContext validationContext);
}
Loading