Skip to content

Commit

Permalink
Merge pull request #4 from ejball/validate-items
Browse files Browse the repository at this point in the history
  • Loading branch information
ejball authored Feb 23, 2024
2 parents f0e7e47 + 9ff615f commit f419e4f
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 2 deletions.
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Project>

<PropertyGroup>
<VersionPrefix>0.1.1</VersionPrefix>
<PackageValidationBaselineVersion>0.1.0</PackageValidationBaselineVersion>
<VersionPrefix>1.0.0</VersionPrefix>
<PackageValidationBaselineVersion>0.1.1</PackageValidationBaselineVersion>
<LangVersion>12.0</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
Expand Down
4 changes: 4 additions & 0 deletions ReleaseNotes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Release Notes

## 1.0.0

* Add `ValidateItemsAttribute`.

## 0.1.1

* Add .NET 6 target.
Expand Down
35 changes: 35 additions & 0 deletions src/Faithlife.DataAnnotations/ValidateItemsAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Collections;
using System.ComponentModel.DataAnnotations;

namespace Faithlife.DataAnnotations;

/// <summary>
/// Used to validate a collection property whose items have their own properties with data annotations.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public sealed class ValidateItemsAttribute : ValidationAttribute
{
/// <summary>
/// True if null items are allowed. Defaults to false.
/// </summary>
public bool AllowNullItems { get; set; }

/// <inheritdoc />
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
if (value is not IEnumerable items)
return null;

var (validationResults, index) = items.Cast<object?>()
.Where(x => !AllowNullItems || x is not null)
.Select((x, i) => (Results: x is null ? [new ValidationResult("The item is null.")] : ValidatorUtility.GetValidationResults(x), Index: i))
.FirstOrDefault(x => x.Results.Count != 0);
if (validationResults is null)
return null;

var innerErrorMessage = string.Join(" ", validationResults.Select(x => x.ErrorMessage).Where(x => x is not null));
return new ValidationResult(
errorMessage: $"{FormatErrorMessage(validationContext.DisplayName)}{(innerErrorMessage.Length == 0 ? "" : $" ([{index}]: {innerErrorMessage})")}",
memberNames: validationContext.MemberName is { } memberName ? new[] { memberName } : null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System.ComponentModel.DataAnnotations;
using NUnit.Framework;

namespace Faithlife.DataAnnotations.Tests;

public sealed class ValidateItemsAttributeTests
{
[Test]
public void ValidateItems()
{
var validatable = new ValidatableDto { Required = "x" };

var results = ValidatorUtility.GetValidationResults(validatable);
Assert.That(results, Is.Empty);

validatable.Validatables = [null!];
results = ValidatorUtility.GetValidationResults(validatable);
Assert.That(results.Single().MemberNames.Single(), Is.EqualTo(nameof(ValidatableDto.Validatables)));
Assert.That(results.Single().ErrorMessage, Is.EqualTo("The field Validatables is invalid. ([0]: The item is null.)"));

validatable.Validatables = [new ValidatableDto(), new ValidatableDto()];
results = ValidatorUtility.GetValidationResults(validatable);
Assert.That(results.Single().MemberNames.Single(), Is.EqualTo(nameof(ValidatableDto.Validatables)));
Assert.That(results.Single().ErrorMessage, Is.EqualTo("The field Validatables is invalid. ([0]: The Required field is required.)"));

validatable.Validatables[0].Required = "x";
results = ValidatorUtility.GetValidationResults(validatable);
Assert.That(results.Single().MemberNames.Single(), Is.EqualTo(nameof(ValidatableDto.Validatables)));
Assert.That(results.Single().ErrorMessage, Is.EqualTo("The field Validatables is invalid. ([1]: The Required field is required.)"));

validatable.Validatables[1].Required = "x";
results = ValidatorUtility.GetValidationResults(validatable);
Assert.That(results, Is.Empty);
}

[Test]
public void ValidateNullableItems()
{
var validatable = new ValidatableDto { Required = "x" };

var results = ValidatorUtility.GetValidationResults(validatable);
Assert.That(results, Is.Empty);

validatable.NullableValidatables = [null!];
results = ValidatorUtility.GetValidationResults(validatable);
Assert.That(results, Is.Empty);

validatable.Validatables = [new ValidatableDto()];
results = ValidatorUtility.GetValidationResults(validatable);
Assert.That(results.Single().MemberNames.Single(), Is.EqualTo(nameof(ValidatableDto.Validatables)));
Assert.That(results.Single().ErrorMessage, Is.EqualTo("The field Validatables is invalid. ([0]: The Required field is required.)"));
}

private sealed class ValidatableDto
{
[Required]
public string? Required { get; set; }

[ValidateItems]
public IReadOnlyList<ValidatableDto>? Validatables { get; set; }

[ValidateItems(AllowNullItems = true)]
public IReadOnlyList<ValidatableDto?>? NullableValidatables { get; set; }
}
}

0 comments on commit f419e4f

Please sign in to comment.