Validating Domain Models.

In the realm of Domain-Driven Design (DDD) and Clean Architecture, ensuring the integrity and correctness of the domain model is paramount. This...

Written by Bala · 5 min read >

In the realm of Domain-Driven Design (DDD) and Clean Architecture, ensuring the integrity and correctness of the domain model is paramount. This is achieved through meticulous validation within the domain layer. If domain model classes aren't properly validated, it can lead to severe issues such as invalid data states, business rule violation, security risks, runtime errors, and a fragile codebase that is difficult to maintain. This blog explores why domain model validation is essential, discusses the shortcomings of using NuGet libraries like FluentValidation for this purpose, and demonstrates various techniques for implementing validation within the domain model using exception handling, guard clauses, and the result pattern.

FluentValidation - NOT suitable

FluentValidation and comparable NuGet libraries excel at validating requests and commands at the application boundary, like in a controller for an ASP.NET Web API. While they can be used for domain model validation, they are not specifically tailored for that purpose. These libraries are primarily aimed at validating data transfer objects (DTOs) and lack the deep integration with the domain layer necessary to effectively enforce business rules. Domain model validation demands more inherent and strict rules that should be directly incorporated into the domain model.

Exception Handling

Exception handling is a fundamental technique for enforcing validation rules within domain models. This approach involves throwing exceptions when domain rules are violated, ensuring that entities remain in a valid state. In the below example, the Order entity validates the Product and Amount fields in the constructor. Custom exceptions InvalidOrderAmountException and InvalidProductException are thrown if the validation fails.

public class DomainException : Exception
{
    public DomainException(string message) : base(message) { }
}

public class InvalidOrderAmountException : DomainException
{
    public InvalidOrderAmountException() : base("Order amount must be greater than zero.") { }
}

public class InvalidProductException : DomainException
{
    public InvalidProductException() : base("Product must not be null or empty.") { }
}

public class Order
{
    public string Product { get; private set; }
    public decimal Amount { get; private set; }

    private Order(string product, decimal amount)
    {
        if (string.IsNullOrWhiteSpace(product))
        {
            throw new InvalidProductException();
        }

        if (amount <= 0)
        {
            throw new InvalidOrderAmountException();
        }

        Product = product;
        Amount = amount;
    }

    public static Order Create(string product, decimal amount)
    {
        return new Order(product, amount);
    }
}

Problems with Validation through Exceptions

Performance Cost - Throwing exceptions is expensive in terms of performance. Exceptions disrupt the normal flow of the application and generate a stack trace, which can slow down the system.

Code Clutter - Using exceptions for validation can make the code more cluttered and harder to read. Each validation rule requires a corresponding exception class, which can lead to a proliferation of exception types.

Maintenance Difficulty - Maintaining a large number of custom exception classes can be challenging. As the application evolves, updating the validation logic and ensuring consistency across all exception classes can become cumbersome.

Error Handling Complexity - Exceptions need to be caught and handled appropriately. This can lead to complex error handling logic, especially in large applications with many layers of abstraction.

Lack of Explicitness - Methods that throw exceptions do not explicitly indicate in their signature that they might fail. This can make it harder for developers to understand the failure scenarios and handle them correctly.

The sole advantage of validation using Exceptions is the capability to obtain a detailed stack trace of your error or bug, enabling you to pinpoint the precise origin of the problem.

Guard Clause Validation

Guard clauses provide a more concise and readable way to enforce validation rules. They provide and expressive and centralized way to enforce validation rules or even business rules. By checking conditions at the beginning of a method, guard clauses help to keep the code clean and maintainable. Several .NET libraries provide ready-to-use guard clauses, enhancing code readability and maintainability.

A notable example is Ardalis.GuardClauses by Steve Smith which offers a comprehensive set of guard clauses. Together with we also have Throw by Amichai Mantinband.

public class Order
{
    public int Quantity { get; private set; }
    public decimal Price { get; private set; }

    public Order(int quantity, decimal price)
    {
        Guard.Against.NegativeOrZero(quantity, nameof(quantity));
        Guard.Against.NegativeOrZero(price, nameof(price));

        Quantity = quantity;
        Price = price;
    }
}

You could also write your own Guard Clauses like the one shown below.

public static class CustomGuard
{
    public static void AgainstInvalidEmail(string email, string parameterName)
    {
        var emailPattern = @"^[^@\s]+@[^@\s]+\.[^@\s]+$";
        if (!Regex.IsMatch(email, emailPattern))
        {
            throw new ArgumentException($"Invalid email format: {email}", parameterName);
        }
    }
}

// Usage
public class Customer
{
    public string Email { get; private set; }

    public Customer(string email)
    {
        CustomGuard.AgainstInvalidEmail(email, nameof(email));
        Email = email;
    }
}

There several advantages of using Guard clauses over Exception handling when it comes to domain model validation. Here are some that I could think of.

Guard Clauses increase the readability of the code, obviously indicating when the method will exit before time, given particular conditions. This way, the maintenance of the method—better understandability at a glance—is made easy. Exception handling can obscure the main flow of the code since the validation logic is usually mixed with the business logic, making it harder to trace and maintain. Guard Clauses also clarify the conditions to be met for code to proceed. This makes it evident what the developer intended, thus minimizing the probability of producing a side effect. May I also add that Guard clauses provide more concrete and meaningful messages about validation checks, hence easy to understand the point of failure.

However, Guard Clauses still throw Exceptions behind the scenes, hence the performance bottle neck that I mentioned earlier still exists.

The alternative and much cleaner approach is the Result Pattern.

The Result Pattern

The Result pattern offers a more efficient and cleaner way to handle validation. Instead of throwing exceptions, the Result pattern returns a result object that indicates success or failure, along with any associated errors.

public class Order
{
    public int Quantity { get; private set; }
    public decimal Price { get; private set; }

    private Order(int quantity, decimal price)
    {
        Quantity = quantity;
        Price = price;
    }

    public static Result<Order> Create(int quantity, decimal price)
    {
        if (quantity <= 0)
        {
            return Result.Failure<Order>("Quantity must be greater than zero.");
        }

        if (price <= 0)
        {
            return Result.Failure<Order>("Price must be greater than zero.");
        }

        return Result.Success(new Order(quantity, price));
    }
}

public class Result<T>
{
    public bool IsSuccess { get; }
    public bool IsFailure => !IsSuccess;
    public T Value { get; }
    public string Error { get; }

    protected Result(bool isSuccess, T value, string error)
    {
        IsSuccess = isSuccess;
        Value = value;
        Error = error;
    }

    public static Result<T> Success(T value) => new Result<T>(true, value, null);
    public static Result<T> Failure(string error) => new Result<T>(false, default, error);
}

I have written a separate blog on the Result Pattern. Please read it if you want a more in-depth understanding of it.

Custom Guard Clauses with Result Pattern.

By combining the benefits of Guard Clauses, which enhance intentionality, maintainability, and readability, with the Result Pattern, we could devise something along these lines.

public static class CustomGuard
{
    public static Result<string> AgainstInvalidCurrency(string currency)
    {
        if (currency.Length != 3)
        {
            return Result<string>.Failure("Currency must be a valid 3-letter code.");
        }
        return Result<string>.Success(currency);
    }
}

public class Order
{
    public string Product { get; private set; }
    public decimal Amount { get; private set; }
    public string Currency { get; private set; }

    private Order(string product, decimal amount, string currency)
    {
        Product = product;
        Amount = amount;
        Currency = currency;
    }

    public static Result<Order> Create(string product, decimal amount, string currency)
    {
        if (string.IsNullOrWhiteSpace(product))
        {
            return Result<Order>.Failure("Product must not be null or empty.");
        }

        if (amount <= 0)
        {
            return Result<Order>.Failure("Order amount must be greater than zero.");
        }

        var currencyResult = CustomGuard.AgainstInvalidCurrency(currency);
        if (!currencyResult.IsSuccess)
        {
            return Result<Order>.Failure(currencyResult.Error);
        }

        return Result<Order>.Success(new Order(product, amount, currency));
    }
}

Constructors or Factory Methods

The decision to perform validations in the constructor or a factory method hinges on your application's design and requirements. Validating within the constructor guarantees that an entity is always in a valid state from the moment of creation, whereas a factory method, such as Create, permits more intricate creation logic and the consolidation of various validation steps.

// Validating in the constructor - NOT RECCOMMENDED
public class Customer
{
    public Name FirstName { get; private set; }
    public Name LastName { get; private set; }

    public Customer(Name firstName, Name lastName)
    {
        if (firstName == null || lastName == null)
        {
            throw new ArgumentNullException(firstName == null ? nameof(firstName) : nameof(lastName));
        }

        FirstName = firstName;
        LastName = lastName;
    }
}

// Validating via the Factory Method Create - RECCOMMENDED
public class Customer
{
    public Name FirstName { get; private set; }
    public Name LastName { get; private set; }

    private Customer(Name firstName, Name lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }

    public static Result<Customer> Create(Name firstName, Name lastName)
    {
        if (firstName == null || lastName == null)
        {
            return Result.Failure<Customer>("First name and last name cannot be null.");
        }

        return Result.Success(new Customer(firstName, lastName));
    }
}

Refactoring Primitive Types to Value Objects

Refactoring primitive types to value objects enhances domain modeling by encapsulating behaviors and validation logic within the value objects themselves.

public class Name
{
	public string Value { get; }

	private Name(string value)
	{
		Value = value;
	}

	public static Result<Name> Create(string value)
	{
		if (string.IsNullOrWhiteSpace(value))
		{
			return Result.Failure<Name>("Name cannot be empty.");
		}

		return Result.Success(new Name(value));
	}
}



public class Customer
{
	public Name FirstName { get; private set; }
	public Name LastName { get; private set; }

	private Customer(Name firstName, Name lastName)
	{
		FirstName = firstName;
		LastName = lastName;
	}

	public static Result<Customer> Create(Name firstName, Name lastName)
	{
		if (firstName == null || lastName == null)
		{
			return Result.Failure<Customer>("First name and last name cannot be null.");
		}

		return Result.Success(new Customer(firstName, lastName));
	}
}

What are Domain Models

Bala in Domain Driven Design
  ·   5 min read

Leave a Reply

Your email address will not be published. Required fields are marked *