Skip to content

Commit

Permalink
Only non empty strings and guids attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
nicholas-renaud committed Feb 29, 2024
1 parent c541410 commit 94f78cb
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 19 deletions.
33 changes: 18 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,24 @@ The most useful validation attribute here is probably `ValidatePropertiesAttribu

## List of data annotation attributes

| Attribute | Description |
|-------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `GuidAttribute` | Validates that a string property is a well-formatted GUID with an optional format |
| `ContainsNonEmptyGuidAttribute` | Validates that a Guid enumerable property contains at least one non-empty Guid |
| `NotEmptyAttribute` | Validates that an enumerable property is not empty |
| `ValidatePropertiesAttribute` | Validates **all properties of a complex type property** (nested object validation) |
| `TimeSpanAttribute` | Validates that a string property is a well-formatted TimeSpan with an optional format |
| `UrlOfKindAttribute` | Validates that a string or `Uri` property is a well-formatted url of the specified `UriKind` |
| `ContainsAttribute` | Validates that a string contains the specified substring (casing can be specified) |
| `StartsWithAttribute` | Validates that a string starts with the specified substring (casing can be specified) |
| `EndsWithAttribute` | Validates that a string ends with the specified substring (casing can be specified) |
| `ProvidedByAzureKeyVaultAttribute` | Indicates that a property value might be loaded from Azure Key Vault (_has no effect_) |
| `ProvidedByAzureAppConfigAttribute` | Indicates that a property value might be loaded from Azure App Configuration (_has no effect_) |
| `SensitiveInformationAttribute` | Indicates that a property contains sensitive information, such as personally identifiable information (PII), or any other information that might result in loss of an advantage or level of security if disclosed to others (_has no effect_) |
| `NonSensitiveInformationAttribute` | Indicates that a property does not contain sensitive information (_has no effect_) |
| Attribute | Description |
|---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `GuidAttribute` | Validates that a string property is a well-formatted GUID with an optional format |
| `ContainsNonEmptyGuidAttribute` | Validates that a Guid enumerable property contains at least one non-empty Guid |
| `ContainsNonEmptyStringAttribute` | Validates that a String enumerable property contains at least one non-empty String |
| `ContainsOnlyNonEmptyGuidsAttribute` | Validates that a Guid enumerable property contains only non-empty Guids |
| `ContainsOnlyNonEmptyStringsAttribute`| Validates that a String enumerable property contains only non-empty Strings |
| `NotEmptyAttribute` | Validates that an enumerable property is not empty |
| `ValidatePropertiesAttribute` | Validates **all properties of a complex type property** (nested object validation) |
| `TimeSpanAttribute` | Validates that a string property is a well-formatted TimeSpan with an optional format |
| `UrlOfKindAttribute` | Validates that a string or `Uri` property is a well-formatted url of the specified `UriKind` |
| `ContainsAttribute` | Validates that a string contains the specified substring (casing can be specified) |
| `StartsWithAttribute` | Validates that a string starts with the specified substring (casing can be specified) |
| `EndsWithAttribute` | Validates that a string ends with the specified substring (casing can be specified) |
| `ProvidedByAzureKeyVaultAttribute` | Indicates that a property value might be loaded from Azure Key Vault (_has no effect_) |
| `ProvidedByAzureAppConfigAttribute` | Indicates that a property value might be loaded from Azure App Configuration (_has no effect_) |
| `SensitiveInformationAttribute` | Indicates that a property contains sensitive information, such as personally identifiable information (PII), or any other information that might result in loss of an advantage or level of security if disclosed to others (_has no effect_) |
| `NonSensitiveInformationAttribute` | Indicates that a property does not contain sensitive information (_has no effect_) |


## License
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;

namespace Workleap.ComponentModel.DataAnnotations.Tests;

public sealed class ContainsOnlyNonEmptyGuidsAttributeTests
{
[Theory]
[ClassData(typeof(ValidData))]
public void Given_ValidGuids_When_Validate_Then_Valid(object? inputs)
{
var attr = new ContainsOnlyNonEmptyGuidsAttribute();
Assert.True(attr.IsValid(inputs));
}

[Theory]
[ClassData(typeof(InvalidData))]
public void Given_InvalidGuids_When_Validate_Then_Invalid(object? inputs)
{
var attr = new ContainsOnlyNonEmptyGuidsAttribute();
Assert.False(attr.IsValid(inputs));
}

[Fact]
public void Validator_TryValidateObject_Returns_The_Expected_Error_Message_When_Validation_Fails()
{
var something = new SomeClass();
var expectedErrorMessage = string.Format(CultureInfo.InvariantCulture, ContainsOnlyNonEmptyGuidsAttribute.ErrorMessageFormat, nameof(SomeClass.Values));

var results = new List<ValidationResult>();
var context = new ValidationContext(something, serviceProvider: null, items: null);
var isValid = Validator.TryValidateObject(something, context, results, validateAllProperties: true);

Assert.False(isValid);
var result = Assert.Single(results);
Assert.NotNull(result.ErrorMessage);
Assert.Equal(expectedErrorMessage, result.ErrorMessage);
}

private class ValidData : IEnumerable<object?[]>
{
public IEnumerator<object?[]> GetEnumerator()
{
yield return new object?[] { null };
yield return new object[] { new Guid[] { } };
yield return new object[] { new Guid?[] { } };
yield return new object[] { new string[] { } };
yield return new object[] { new string?[] { } };
yield return new object[] { new Guid[] { new("f8daff85-4393-42ae-9ab5-8620ab20c8da") } };
yield return new object[] { new string[] { "d78b48f9-37b8-47dd-8e47-0325dd3e7899" } };
}

IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}

private class InvalidData : IEnumerable<object?[]>
{
public IEnumerator<object?[]> GetEnumerator()
{
yield return new object[] { new Guid[] { default } };
yield return new object[] { new Guid?[] { default } };
yield return new object[] { new string[] { string.Empty } };
yield return new object[] { new string?[] { default } };
yield return new object[] { new Guid[] { Guid.Empty } };
yield return new object[] { new string[] { "00000000-0000-0000-0000-000000000000" } };
yield return new object[] { new int[] { 0 } };
yield return new object[] { new Guid[] { default, new("846398d2-416d-4f05-86b0-431e11117abc") } };
yield return new object[] { new Guid[] { Guid.Empty, new("08bfec4a-da43-40de-a15a-0d10f227e6b7") } };
yield return new object[] { new Guid?[] { default, new Guid("2dd81281-7498-411c-aea1-5cec96f2dd8d") } };
yield return new object[] { new string[] { "00000000-0000-0000-0000-000000000000", "7d8c61f7-a334-4599-b801-45f0934099a4" } };
yield return new object[] { new string[] { string.Empty, "7c3cdfba-4466-44ab-bb95-da02eac67380" } };
yield return new object[] { new string?[] { default, "f8daff85-4393-42ae-9ab5-8620ab20c8da" } };
}

IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}

private class SomeClass
{
[ContainsOnlyNonEmptyGuids]
public Guid[] Values { get; set; } = new[] { Guid.Empty };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;

namespace Workleap.ComponentModel.DataAnnotations.Tests;

public sealed class ContainsOnlyNonEmptyStringsAttributeTests
{
[Theory]
[ClassData(typeof(ValidData))]
public void Given_ValidStrings_When_Validate_Then_Valid(object? inputs)
{
var attr = new ContainsOnlyNonEmptyStringsAttribute();
Assert.True(attr.IsValid(inputs));
}

[Theory]
[ClassData(typeof(InvalidData))]
public void Given_InvalidGuids_When_Validate_Then_Invalid(object? inputs)
{
var attr = new ContainsOnlyNonEmptyStringsAttribute();
var result = attr.IsValid(inputs);
Assert.False(result);
}

[Fact]
public void Validator_TryValidateObject_Returns_The_Expected_Error_Message_When_Validation_Fails()
{
var something = new SomeClass();
var expectedErrorMessage = string.Format(CultureInfo.InvariantCulture, ContainsOnlyNonEmptyStringsAttribute.ErrorMessageFormat, nameof(SomeClass.Values));

var results = new List<ValidationResult>();
var context = new ValidationContext(something, serviceProvider: null, items: null);
var isValid = Validator.TryValidateObject(something, context, results, validateAllProperties: true);

Assert.False(isValid);
var result = Assert.Single(results);
Assert.NotNull(result.ErrorMessage);
Assert.Equal(expectedErrorMessage, result.ErrorMessage);
}

private class ValidData : IEnumerable<object?[]>
{
public IEnumerator<object?[]> GetEnumerator()
{
yield return new object?[] { null };
yield return new object[] { new string?[] { } };
yield return new object[] { new string[] { } };
yield return new object[] { new string[] { "Lorem ipsum dolor sit amet" } };
yield return new object[] { new string[] { "Lorem ipsum dolor sit amet", "Etiam porta velit non nisi feugiat pulvinar" } };
}

IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}

private class InvalidData : IEnumerable<object?[]>
{
public IEnumerator<object?[]> GetEnumerator()
{
yield return new object[] { new string?[] { string.Empty, default } };
yield return new object[] { new string?[] { string.Empty } };
yield return new object[] { new string?[] { default } };
yield return new object[] { new string[] { " " } };
yield return new object[] { new string[] { string.Empty, "Lorem ipsum dolor sit amet" } };
yield return new object[] { new string?[] { default, "Lorem ipsum dolor sit amet" } };
yield return new object[] { new string[] { "Lorem ipsum dolor sit amet", " " } };
}

IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}

private class SomeClass
{
[ContainsOnlyNonEmptyStrings]
public string?[] Values { get; set; } = new[] { string.Empty };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;

namespace Workleap.ComponentModel.DataAnnotations;

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
public sealed class ContainsOnlyNonEmptyGuidsAttribute : ValidationAttribute
{
internal const string ErrorMessageFormat = "The field {0} must be an collection that contains only non-empty GUIDs";

public ContainsOnlyNonEmptyGuidsAttribute() : base(ErrorMessageFormat)
{
}

public override bool IsValid(object? value) => value switch
{
null => true,
IEnumerable<Guid> enumerable => enumerable.All(this.IsValidGuid),
IEnumerable<Guid?> enumerable => enumerable.All(x => x != null && this.IsValidGuid(x.Value)),
IEnumerable<string> enumerable => enumerable.All(this.IsValidGuid),
_ => false,
};

private bool IsValidGuid(string valueAsString)
{
return Guid.TryParse(valueAsString, out var guid) && this.IsValidGuid(guid);
}

private bool IsValidGuid(Guid guid)
{
return guid != Guid.Empty;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;

namespace Workleap.ComponentModel.DataAnnotations;

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
public sealed class ContainsOnlyNonEmptyStringsAttribute : ValidationAttribute
{
internal const string ErrorMessageFormat = "The field {0} must be a collection that contains only non-empty Strings";

public ContainsOnlyNonEmptyStringsAttribute() : base(ErrorMessageFormat)
{
}

public override bool IsValid(object? value) => value switch
{
null => true,
IEnumerable<string> enumerable => enumerable.All(x => !string.IsNullOrWhiteSpace(x)),
_ => false,
};
}
5 changes: 2 additions & 3 deletions src/Workleap.ComponentModel.DataAnnotations/GuidAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;

Expand Down Expand Up @@ -37,9 +37,8 @@ public GuidAttribute(string format)

private bool IsValidGuid(string valueAsString)
{
Guid guid;
return this.Format is null
? Guid.TryParse(valueAsString, out guid) && this.IsValidGuid(guid)
? Guid.TryParse(valueAsString, out Guid guid) && this.IsValidGuid(guid)
: Guid.TryParseExact(valueAsString, this.Format, out guid) && this.IsValidGuid(guid);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
#nullable enable
#nullable enable
Workleap.ComponentModel.DataAnnotations.ContainsOnlyNonEmptyGuidsAttribute
Workleap.ComponentModel.DataAnnotations.ContainsOnlyNonEmptyGuidsAttribute.ContainsOnlyNonEmptyGuidsAttribute() -> void
override Workleap.ComponentModel.DataAnnotations.ContainsOnlyNonEmptyGuidsAttribute.IsValid(object? value) -> bool
Workleap.ComponentModel.DataAnnotations.ContainsOnlyNonEmptyStringsAttribute
Workleap.ComponentModel.DataAnnotations.ContainsOnlyNonEmptyStringsAttribute.ContainsOnlyNonEmptyStringsAttribute() -> void
override Workleap.ComponentModel.DataAnnotations.ContainsOnlyNonEmptyStringsAttribute.IsValid(object? value) -> bool

0 comments on commit 94f78cb

Please sign in to comment.