0 Comments
  •   Posted in: 
  • C#

The need to validate our POCO classes in our domain layer is common and pervasive. We all know we need to apply defensive programming principles, and that input from the client can’t be trusted. This isn’t just to thwart intentionally bad actors, because regular users can become quite creative and bring us to places we never considered. When people state that user input can’t be trusted they are talking about two broad categories; mitigating security threats, and validating that the input meets the required invariants. A security threat would be something like preventing cross-site scripting. An example of this would be allowing JavaScript to run in user comments on your blog. Validating invariants is what I’ll be discussing in this blog post, and it is concerned with ensuring that required fields are present and within acceptable ranges. An example of this would be ensuring that both latitude and longitude are present for a GPS coordinate and the values are within acceptable ranges.

Unfortunately in my experience one of two things typically happen:

  1. People sprinkle validation inline throughout their code, invariably missing something, or forgetting to update a method when the logic changes.
  2. A null check is deemed good enough

Framework Solutions

One highly promoted way to accomplish this goal is to use attributes in or derived from classes in the DataAnnotations namespace. This can be a decent approach for many use cases. I personally tend to avoid it for the following reasons:

  • I don’t use the ModelState.IsValid logic in my controllers because I prefer to perform validation in the domain layer. This ensures validation will always be performed, and make my controller logic exceedingly simple.
  • I don’t like the signature of the Validator class methods because they feel unnecessarily complicated. I also dislike that the ValidationResult collection is modified by reference.
  • There is limited opportunity to tweak the performance if this is identified as a bottleneck without introducing a new validation pattern.

Another option that seems relatively less well known is implementing the IValidatableObject interface. In my opinion this is a clear step forward over the attribute method. It is marginally more work but much more flexible, self descriptive, and easier to test. Even though it is a step in the right direction I believe it has some flaws:

  • Assuming the interface isn’t named inappropriately, the ValidationContext parameter feels unnecessary. Why should I specify the context when the object is expected to know how to validate itself?
  • It is helpful that it returns an itemized list of the problems, but ideally that should be the least common use case. Why optimize for the least common use case and force a LINQ call to verify the most common use case?

Preferred Solution

The option that I prefer is to use to implement a custom interface IValidatable, that returns a very simple POCO.

public interface IValidatable
{
    DomainResult Validate();
}

public class DomainResult // In some projects I've named this class 'Result'
{
    public DomainResult(bool success, string message)
    {
        Success = success;
        Message = message;
    }

    public bool Success { get; }

    public string Message { get; }
}

With those two tools I can easily implement validation in a standard way that is simultaneously dead simple and very flexible. Here is a quick example with a POCO for creating a circular geo fence.

public class GeoFenceCreation : IValidatable
{
    public decimal Latitude { get; set; }

    public decimal Longitude { get; set; }

    public int RadiusInMeters { get; set; }

    public string Description { get; set; }

    public virtual DomainResult Validate()
    {
        var errors = new List();
        // IsInSymmetricRange is an extension method 
        if (!Latitude.IsInSymmetricRange(Limits.MaxLatitude))
        {
            // Inline strings can be centralized and/or replaced with resource files for localization
            errors.Add("Invalid latitude.");
        }

        // Limits is a static class for constants
        if (!Longitude.IsInSymmetricRange(Limits.MaxLongitude))
        {
            errors.Add("Invalid longitude.");
        }

        if (RadiusInMeters < Limits.MinGeoFenceRadius || RadiusInMeters > Limits.MaxGeoFenceRadius)
        {
            errors.Add($"Invalid radius. The radius must be between {Limits.MinGeoFenceRadius} and {Limits.MaxGeoFenceRadius} meters.");
        }

        if (string.IsNullOrWhiteSpace(Description))
        {
            errors.Add("The description must not be null or empty.");
        }

        // ToValidationResult is an extension method described below.
        var result = errors.ToValidationResult();
        return result; // Depending on your needs and the model you can cache this locally.
    }
}

Before I show an example of how to validate the GeoFenceCreation class in your domain layer I’ll introduce a few extension methods that will make your life easier.

public static class ValidatableExtensions
{
    public static bool IsValid(this IValidatable input)
    {
        // Sometimes all you care about is a boolean
        return input.ValidationResult().Success;
    }

    public static string ValidationMessage(this IValidatable input)
    {
        // Other times you just want the message. This is much more rare.
        return input.ValidationResult().Message;
    }

    public static DomainResult ValidationResult(this IValidatable input)
    {
        // This avoids needing a null check in our code when we validate nullable objects
        return input == null ? new DomainResult(false, "Null input is invalid.") : input.Validate();
    }

    public static DomainResult ToValidationResult(this IEnumerable input)
    {
        // Centralizes boilerplate needed to convert the list of errors into a single string.
        if (input == null)
        {
            return new DomainResult(false, "Null input is invalid.");
        }

        // Enumerate the errors
        var errors = input.ToList();
        var success = !errors.Any();
        var message = success ? "Validation successful." : string.Join(" ", errors);

        return new DomainResult(success, message);
    }
}

As you can see these extension methods are mostly about convenience, and reducing duplicated code. Speaking of convenience and duplication, I really do like the power of the data annotations, but I like to consume them differently.  One example of this is the extension method I wrote for validating email addresses below. As you can see it leverages the power of the attribute without needing reflection, or needing to instantiate multiple instances of the attribute. I’m also able to extend the validation that the attribute performs easily in keeping with the open/closed principle.

private static readonly EmailAddressAttribute EmailValidator = new EmailAddressAttribute();

public static bool IsValidEmail(this string email)
{
    return !string.IsNullOrWhiteSpace(email) && 
      EmailValidator.IsValid(email) && email.Length <= Constants.MaxLength.Email;
}

Without further ado here are a couple of examples of validation with the tools I’ve outlined above. As you will see the validation is performed via the extension methods instead of calling IValidatable.Validate(). This is to avoid null checks, and to provide consistency when validating a struct.

public async Task CreateGeoFenceAsync(GeoFenceCreation fenceCreation)
{
    if(!fenceCreation.IsValid())
    {
        return fenceCreation.ValidationResult();
    }

    var result = await _repository.PersistNewGeoFence(fenceCreation);
    return result;
}

Conclusion

I discussed three different approaches to validating invariants in your domain layer. These included two options that are offered by Microsoft via the DataAnnotations library. The third option is my preferred approach due to the simplicity, and flexibility it offers. I personally use the IValidatable interface described above in both personal projects and projects at work. It has been a great tool in my toolbox. While I haven’t needed to move beyond this simple interface you should feel free to adapt it to your particular needs.