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 ValidateItemsAttribute. #4

Merged
merged 2 commits into from
Feb 23, 2024
Merged
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
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; }
}
}
Loading