Even more .NET validation attributes with GSoft.ComponentModel.DataAnnotations
Tags: programming, dotnet, csharp, aspdotnet
Published: 2023-06-17 20:57:46

[Validation attributes](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute) 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](https://github.com/gsoft-inc/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](https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation#validation-attributes).

These attributes can also be employed on [options classes](https://learn.microsoft.com/en-us/dotnet/core/extensions/options#options-validation) 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](https://github.com/dotnet/runtime/pull/47821).
Another approach to data validation using validation attributes is to manually invoke the [Validator.ValidateObject(...)](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.validator.validateobject) method on an object.

In .NET 8, [we will have access to new validation attributes](https://devblogs.microsoft.com/dotnet/announcing-dotnet-8-preview-2/#system-componentmodel-dataannotations-extensions), which relatively highlights Microsoft's investment in this pattern.

While some may require a [more sophisticated third-party validation library such as FluentValidation](https://github.com/FluentValidation/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](https://github.com/gsoft-inc/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](https://learn.microsoft.com/en-us/dotnet/api/system.guid.tostring) 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`).

```csharp
[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.

```csharp
[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](https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-timespan-format-strings). There's also a `UseInvariantCulture` property to parse the string using an invariant culture.

```csharp
[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](https://learn.microsoft.com/en-us/dotnet/api/system.urikind) `Kind`  property. This attribute is particularly useful for enforcing absolute URLs in a .NET configuration section, for instance.

```csharp
[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`.

```csharp
[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:

```csharp
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](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration) and enable data annotations validation, the `SubSection.MyValue` property will not be validated:

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

Contrary to ASP.NET Core model validation, [which uses the visitor pattern](https://github.com/dotnet/aspnetcore/blob/v6.0.0/src/Mvc/Mvc.Core/src/ModelBinding/Validation/ValidationVisitor.cs) to validate complex types at almost any depth level, the [.NET options validator for data annotations](https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/Microsoft.Extensions.Options.DataAnnotations/src/DataAnnotationValidateOptions.cs) 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:

```csharp
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](https://github.com/gsoft-inc/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.