Quantcast
Channel: Andrew Lock | .NET Escapades
Viewing all articles
Browse latest Browse all 743

Creating a not-empty GUID validation attribute and a not-default validation attribute

$
0
0
Creating a not-empty GUID validation attribute and a not-default validation attribute

In this post I describe a handy validation attribute used during ASP.NET Core model binding for verifying that a GUID doesn't have the default value of Guid.Empty. I always find myself re-creating it in projects, so now I can just copy it from here!

Why do you need a not-empty validation attribute?

You might be wondering why a "not-empty" attribute is necessary. Isn't that the same thing as the Required attribute? Let's say you have an action method on an API controller that updates the name of a user. For example:

[ApiController]
public class UserController: Controller
{
    [HttpPut("/user/name")]
    public IActionResult UpdateName(UpdateNameModel model)
    {
        if(!ModelState.IsValid)
        {
            return BadReques(ModelState);
        }

        _userService.UpdateName(model);

        return Ok();
    }
}

This method binds the body of the request to an UpdateNameModel object which contains the Id of the user to update, and the new name for the user:

public class UpdateNameModel
{
    public Guid Id { get; set; }

    [Required(AllowEmptyStrings = false)]
    [StringLength(100)]
    public string Name { get; set; }
}

As you would expect, The string Name is marked as required using the [Required] attribute, and has a validation attribute for the string length. Now, we know the Id property is also required, but how to achieve that with ValidationAttributes? The obvious answer would be to add the [Required] attribute, but unfortunately that won't work.

The [Required] attribute checks that the decorated property isn't null. But Guid is a struct that will never be null, much like int, DateTime, and other struct values. That means that Id will always have a value, whether the request body includes a value for it or not. If an Id isn't provided, it will have the default value, which for Guid is a value with all zeros (also exposed as Guid.Empty):

00000000-0000-0000-0000-000000000000

Because of this, adding the [Required] attribute to Id achieves nothing; the attribute will always say the Id is valid. Instead, you have a few options:

  • Use BindRequired to ensure a value for Id was provided in the body of the request. Filip has a great description of that approach here.
  • Make the property nullable. If you use a Guid? instead of a Guid then the value can be null, so adding the Required attribute confirms it's not null. This works, but feels clumsy to me.
  • Manually check the Guid is not empty after running other validation, or by using IValidatableObject. This seems messy and overkill for such a simple validation requirement.
  • Create a custom attribute.

The goal of this post is to be able to apply a validation attribute to the Id property of our model to validate it has a not-empty/not-default value. e.g.

public class UpdateNameModel
{
    [NotEmpty]
    public Guid Id { get; set; }
    public string Name { get; set; }
}

The NotEmptyAttribute for Guids

The attribute we need has the following characteristics:

  • For Guid properties: Returns valid for any value except Guid.Empty
  • For Guid? properties: Returns valid for any value except Guid.Empty. Note that it should return valid for null values - if the value should not be null or empty, use two attributes: [Required, NotEmpty]
  • For all other types: All values should be valid, including null, as this attribute doesn't make sense for non-Guid properties. The behaviour here is essentially undefined, so you could have the attribute always return invalid if you prefer.

The following attribute satisfies all those properties:

[AttributeUsage(
    AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, 
    AllowMultiple = false)]
public class NotEmptyAttribute : ValidationAttribute
{
    public const string DefaultErrorMessage = "The {0} field must not be empty";
    public NotEmptyAttribute() : base(DefaultErrorMessage) { }

    public override bool IsValid(object value)
    {
        //NotEmpty doesn't necessarily mean required
        if (value is null)
        {
            return true;
        }

        switch (value)
        {
            case Guid guid:
                return guid != Guid.Empty;
            default:
                return true;
        }
    }
}

You can add this attribute to the previously described UpdateNameModel model to guard against empty values in Id. Note that this guards both against the case where the request body contained no value for Id, and also the case where an explicit empty (i.e. all zeros) Guid was provided.

public class UpdateNameModel
{
    [NotEmpty]
    public Guid Id { get; set; }

    [Required(AllowEmptyStrings = false)]
    [StringLength(100)]
    public string Name { get; set; }
}

The NotDefaultAttribute for structs in general

The [NotEmpty] attribute is one I find I need commonly as ID values are often provided as Guids, and Guid.Empty is rarely used. However it's possible that you may want to use a similar approach for other structs. For example, you might want a DateTime to not have the default value DateTime.MinValue.

One option is to create different attributes for each struct you care about, for example a NotMinValueAttribute for the case of DateTime. Alternatively, we could create a more general attribute that can be applied to any struct.

Instead of comparing to a specific value (Guid.Empty or DateTime.MinValue), in the general case we compare a value to the default for that data type. For consistency with other validation attributes, we won't apply the validation to null values - null values will always be valid unless you've also applied the Required attribute.

Note: I haven't extensively tested this attribute, it was just something I thought about when writing up the NotEmpty attribute! It assumes that it's possible to create an instance of the default value, so non-public types or types containing generic-parameters will cause runtime exceptions. It's probably unlikely you're using those types in your public models, but it's something to keep in mind.

 public class NotDefaultAttribute : ValidationAttribute
{
    public const string DefaultErrorMessage = "The {0} field must not have the default value";
    public NotDefaultAttribute() : base(DefaultErrorMessage) { }

    public override bool IsValid(object value)
    {
        //NotDefault doesn't necessarily mean required
        if (value is null)
        {
            return true;
        }

        var type = value.GetType();
        if (type.IsValueType)
        {
            var defaultValue = Activator.CreateInstance(type);
            return !value.Equals(defaultValue);
        }

        // non-null ref type
        return true;
    }
}

The overall structure is very similar to the NotEmpty attribute, as you might expect. We start by checking for null, and returning true if that's the case.

To find the default value, we need to get the runtime Type of the provided value using GetType(). If we have a value-type (like Guid or DateTime) then we can use Activator.CreateInstance() to create the default value of the type. We can then compare the provided value to the defaultValue and return false if they match. If the type is not a value type (therefore it's a reference type), we already know the type doesn't have the default value (null), so we return true.

One interesting point is that due to the various boxing of values used in this method, you must use the Equals() method, not the == and != operators when comparing a value to its default. You can see the result of not doing this in the following test.

public class NotDefaultTests
{
    [Fact]
    public void WhenEmpty_NotValid()
    {
        var validator = new NotDefaultAttribute();

        var isValid = validator.IsValid(Guid.Empty);

        Assert.False(isValid); // Fails if you use != instead of !value.Equals(defaultValue)
    }
}

It's also worth pointing out that if you're using FluentValidation, the built-in NotEmpty() validator already handles checking for default (and null) values.

Summary

In this post I described a [NotEmpty] validation attribute that can be used to detect when a Guid property on a model is bound to the default value Guid.Empty. This can happen either because the value isn't model bound at all, or it's explicitly model bound to the default value. I also showed a more general version of the attribute that validates a struct doesn't have its default value.


Viewing all articles
Browse latest Browse all 743

Trending Articles