When there are multiple invalid properties that are decorated with data annotations and validated with custom validation, I want to obtain validation results for all the invalid properties rather than just the first invalid one (as what is produced by the following code).
using MiniValidation;
using System.ComponentModel.DataAnnotations;
var widget = new Widget { Name = "A", Age = 0 };
if (!MiniValidator.TryValidate(widget, out var errors))
{
foreach (var entry in errors)
{
Console.WriteLine($" {entry.Key}:");
foreach (var error in entry.Value)
{
Console.WriteLine($" - {error}");
}
}
}
class Widget : IValidatableObject
{
[Required, MinLength(3)]
public string Name { get; set; } = null!;
public int Age { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext ctx)
{
if (Age < 10)
yield return new ValidationResult("Age cannot be less than 10.", [nameof(Age)]);
}
}
I don't want to replace the attribute-based validations with custom validation.
class Widget : IValidatableObject
{
public string Name { get; set; } = null!;
public int Age { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext ctx)
{
if (Name is null)
yield return new ValidationResult("Name must be specified.", [nameof(Name)]);
if (Name?.Length < 3)
yield return new ValidationResult("Name must contain at least 3 characters.", [nameof(Name)]);
if (Age < 10)
yield return new ValidationResult("Age cannot be less than 10.", [nameof(Age)]);
}
}
I was able to reproduce the observed behavior and I was actually mistaken about the behavior when inheriting from an annotated class.
(talking about the comment "If it is this: github.com/DamianEdwards/MiniValidation then the readme suggests you can do this by inheritance. Have a base with annotations and derived with custom validation." )
Outside providing a feature to that project, I see these remedies:
using System;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
public class Program
{
public static void Main()
{
var widget = new Widget
{
Name = "A",
Age = 0
};
if (!MyValidator.TryValidate(widget, out var myErrors))
{
foreach( var (name, error) in myErrors )
{
Console.WriteLine($"{name} :");
foreach( var msg in error )
Console.WriteLine($" - {msg}");
}
}
}
}
static class MyValidator
{
public record MyResult(string Name, IReadOnlyCollection<string> Errors);
public static bool TryValidate<T>(T obj, out IEnumerable<MyResult> errors)
{
var ctx = new ValidationContext(obj);
var results = new List<ValidationResult>();
if (obj is IValidatableObject ivo)
{
results.AddRange(ivo.Validate(ctx));
}
var validatorResult = Validator.TryValidateObject(obj, ctx, results, true);
if (!validatorResult || results.Any())
{
errors = results.SelectMany(r => r.MemberNames.Select(n => new {Name = n, Error = r.ErrorMessage}))
.GroupBy(o => o.Name)
.Select(grp => new MyResult(grp.Key, grp.Select(o => o.Error).ToList()))
.ToList();
return false;
}
else
{
errors = [];
return true;
}
}
}
class Widget : IValidatableObject
{
[Required, MinLength(3)]
public string Name { get; set; } = null !;
[Range(10, 1000,
ErrorMessage = "Value for {0} must be between {1} and {2}.")]
public int Age { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext ctx)
{
if (Age < 10)
yield return new ValidationResult("Age cannot be less than 10.", [nameof(Age)]);
}
}
Fiddle: https://dotnetfiddle.net/5FUcEo
Output:
Age : - Age cannot be less than 10. - Value for Age must be between 10 and 1000. Name : - The field Name must be a string or array type with a minimum length of '3'.
Disclaimer: I do not consider above code "production ready". It is supposed to briefly showcase a possible first step towards a solid solution.