Even more .NET validation attributes with GSoft.ComponentModel.DataAnnotations

Even more .NET validation attributes with GSoft.ComponentModel.DataAnnotations

Validation attributes are an extremely convenient and non-intrusive method of validating data integrity in .NET. All you need to do is decorate your properties, fields, or method parameters to gain access to this functionality.

At GSoft, we developed GSoft.ComponentModel.DataAnnotations, which offers even more validation attributes. In this blog post, I will introduce these new attributes to you.

Why use validation in the first place?

Validation attributes in .NET are mostly used for performing model validation in ASP.NET Core.

These attributes can also be employed on options classes to ensure the validity of resolved options values. Since .NET 6, there is even a mechanism to guarantee that options are valid at application startup. Another approach to data validation using validation attributes is to manually invoke the Validator.ValidateObject(...) method on an object.

In .NET 8, we will have access to new validation attributes, which relatively highlights Microsoft's investment in this pattern.

While some may require a more sophisticated third-party validation library such as FluentValidation, for most scenarios, validation attributes are good enough. Your code remains clean, it's built into .NET, and as previously mentioned, it's already supported by ASP.NET Core, the options pattern, and Entity Framework.

The .NET SDK comes with built-in attributes including [Required], [RegularExpression], [Range], [MaxLength], [Url], [EmailAddress], and many more. It's also quite straightforward to implement your own custom validation attributes, which is precisely what we did at GSoft.

Introducing GSoft.ComponentModel.DataAnnotations

Our GSoft.ComponentModel.DataAnnotations offers 8 new validation attributes:

No more invalid GUID strings with GuidAttribute

The [Guid] validation attribute ensures that an object is either a Guid or a well-formed GUID string representation. It also includes two optional properties:

  • Format: the GUID format that must be followed during validation. By default, all formats are allowed.
  • AllowEmpty: prevents the use of an empty GUID (00000000-0000-0000-0000-000000000000).
[Guid]
public string MyGuidProperty { get; set; }

[Guid("D")]
public string MyGuidProperty { get; set; }

[Guid(AllowEmpty = false)]
public Guid MyGuidProperty { get; set; }

Prevent a collection from being empty with NotEmptyAttribute

The [NotEmpty] validation attribute ensures that an enumerable object is not empty. It can be a list, an array, an abstract collection, or any class that implements IEnumerable<T> or IEnumerable. There's an optimized check for ICollection.Count that prevents an unnecessary enumeration.

[NotEmpty]
public string[] Values { get; set; }

[NotEmpty]
public List<string> Values { get; set; }

[NotEmpty]
public IEnumerable<string> Values { get; set; }

TimeSpanAttribute is the best way to ensure a string represents a valid duration

As the name suggests, the TimeSpan attribute ensures that an object is a TimeSpan struct or a well-formed TimeSpan string representation. Similar to [Guid], there's a Format property to enforce a specific TimeSpan string format. There's also a UseInvariantCulture property to parse the string using an invariant culture.

[TimeSpan]
public string MyTimeSpanValue { get; set; }

[TimeSpan("G")]
public string MyTimeSpanValue { get; set; }

Enforce URL kind with UrlOfKindAttribute

Our [UrlOfKind] attribute goes a step further than the built-in [Url] attribute when it comes to validating URLs, strings or Uri classes. You can specify an optional UriKind Kind property. This attribute is particularly useful for enforcing absolute URLs in a .NET configuration section, for instance.

[UrlOfKind(UriKind.RelativeOrAbsolute)]
public string MyUrlValue { get; set; }

[UrlOfKind(UriKind.Relative)]
public string MyUrlValue { get; set; }

[UrlOfKind(UriKind.Absolute)]
public string MyUrlValue { get; set; }

Substring validation with ContainsAttribute, StartsWithAttribute, and EndsWithAttribute

These three attributes perform validation on string properties only and serve as an excellent alternative to [RegularExpression] for simpler use cases.

  • [Contains] validates that a string includes a certain substring.
  • [StartsWith] confirms that a substring is found at the beginning of a string value.
  • [EndsWith] verifies that a substring is found at the end of a string value.

Both attributes provide an optional IgnoreCase property. Comparison is performed using StringComparison.Ordinal.

[Contains("foo")]
public string MyStringValue { get; set; }

[StartsWith("https://", IgnoreCase = true)]
public string MyStringValue { get; set; }

[EndsWith("007")]
public string MyStringValue { get; set; }

ValidatePropertiesAttribute for complex type in options

This last validation attribute helps validating complex types within the .NET options pattern. Consider the following option class:

public class MyOptions
{
    public MySubOptions SubSection { get; set; }
}

public class MySubOptions
{
    [Required]
    public string MyValue { get; set; }
}

If we bind the MyOptions class to a configuration section and enable data annotations validation, the SubSection.MyValue property will not be validated:

builder.Services.AddOptions<MyOptions>().BindConfiguration("MySection").ValidateDataAnnotations();

Contrary to ASP.NET Core model validation, which uses the visitor pattern to validate complex types at almost any depth level, the .NET options validator for data annotations doesn't visit complex properties. That's where the [ValidateProperties] attribute comes in. It ensures that validation is performed on complex types located at any depth level. Our previous example would then become:

public class MyOptions
{
    [ValidateProperties]
    public MySubOptions SubSection { get; set; }
}

public class MySubOptions
{
    [Required]
    public string MyValue { get; set; }
}

This modification will now enable the Required attribute on the SubSection property and it will throw an exception if the value is null or empty.

Conclusion

In conclusion, .NET validation attributes are a powerful tool for ensuring data integrity across your applications. The built-in attributes provide a solid foundation, and the upcoming .NET 8 promises to extend this further. However, the GSoft.ComponentModel.DataAnnotations library takes it a step beyond, offering new attributes like Guid, NotEmpty, TimeSpan, UrlOfKind, Contains, StartsWith, EndsWith, and ValidateProperties, which further extend the capabilities of the validation framework. Whether you're using ASP.NET Core, the options pattern, or Entity Framework, these tools can help ensure your data is valid and your code remains clean and maintainable.