Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
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
}
5 changes: 2 additions & 3 deletions src/MiniValidation/IValidate.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
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"></typeparam>
/// <typeparam name="T">The type to validate.</typeparam>
public interface IValidate<in T>
{
/// <summary>
Expand All @@ -16,5 +15,5 @@ public interface IValidate<in T>
/// <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>
Task<IEnumerable<ValidationResult>> ValidateAsync(T instance, ValidationContext validationContext);
IEnumerable<ValidationResult> Validate(T instance, ValidationContext validationContext);
}
Loading