C# Rule Validation in Business Logic

David Rogers
The Cloud Builders Guild
5 min readJun 30, 2022

--

In a previous article, I showed a technique for validating business logic. As a quick and easy way of doing this, I used FluentValidation. But this is not really a good idea. FluentValidation is awesome for validating DTOs, especially payloads being sent to an API and commands (before they reach any business services). But when you get to your business services, validation requirements become more complex. You are often not validating objects, but running all manner of complex validation requirements. So I wanted to write a flexible validator to facilitate that.

I’ve seen validation done in a variety of ways, across many projects that I’ve worked on. Armed with this knowledge, I endeavored to cherry-pick the best ideas and incorporate them into a tiny library (mostly 1 class). So Business Validation was born.

The Plan

The main aim was to collect validation failures as messages and group them in collections, accessible via a key (or name). As I don’t consider a validation failure an error, I named these groupings fail-bundles. I wanted the API to be clean and simple and to be able to validate code in an intuitive way. So lets take a look.
A simple validation scenario:

public bool EnrolInUnit(string unitCode, string studentId)
{
var validator = new Validator();

validator.Validate("EnrolmentIssue", "Student already enrolled", !IsEnrolled(unitCode, studentId));
validator.Validate("EnrolmentIssue", "Student is not eligible", IsEligible(unitCode, studentId));
validator.Validate("StudentFeesOwing", "Student has not paid outstanding Fees", IsFinancial(studentId));
return validator; // bool operator is overloaded
}

After creating our Validator, we run some validation rules through the simplest overload of the Validate method. Note how you can add more than one failure to the same fail-bundle. In the above example, there are 2 messages added to the “EnrolmentIssue” bundle (assuming they fail validation). The final argument of the Validate method (the predicate) is an assertion. If the assertion proves to be incorrect, validation will fail and the failure message is added to the fail-bundle.
Also note the ability to test a validator’s validity by simply if(!validator) i.e. no need to write if(validator.NotValid()):

if(!validator)
return false;
return true;
// OR
// return validator; <- an even more concise way

I’ve overloaded the boolean operator which effectively delegates a call to validator.IsValid() (which checks if there are any fail-bundles). This makes for cleaner code.

Validating an Object

Lets take a look at another overload, which validates an object:

// Step 1 - Retrieve object
var lecturer = GetLecturerBySubject(unitNumber);
// Step 2 - Create validator
var validator = new Validator();
// Step 3 - pass the object to Validate (3rd argument)
validator.Validate(
l => l.CurrentlyRostered,
$"{lecturer.FirstName} {lecturer.LastName} is not currently available.",
lecturer,
l => l.CurrentlyRostered
);

Lecturer is the object being validated. You can see here the ability to pass a lambda as the first argument, reducing potential typos that plague “magic strings”. More contextual information is added to the validation message via string interpolation and the final argument also enables the passing of a lambda (specifically a Func<T, bool> type). In this example, the code is validating that the lecturer has a CurrentlyRostered value of true.

So there are a number of overloads of the Validate method which combine simplicity and convenience, depending on the nature of the validation being executed.

Consolidate Validation Failures from Different Sources

Another feature that I built in was the ability to merge validation objects. For example, a ViewModel may be sent to the server carrying a number of DTOs, each of which require validation in the business logic. The merge feature enables you to do something like this:

var licenceValidator = ValidateLicenceDto(licenceDto);
var trafficHistoryValidator =
ValidateTrafficHistoryDto(trafficHistoryDto);
var registrationValidator =
ValidateRegistrationDto(registrationDto);
var consolidatedValidator = licenceValidator
.Merge(trafficHistoryValidator)
.Merge(registrationValidator);

Now you have a consolidated validation object which will enable you to report all the various validation failures back to the user interface (“UI”). But how does it do that? That’s next.

Handling the Failures

There’s an internal custom collection inside the Validator class which exposes the validation failures as a property called ValidationFailures (of type IReadOnlyDictionary<string, IReadOnlyList<string>> ).

ValidationFailures exposed as a dictionary. Here, 2 messages for the EnrolmentIssue key.

It’s really up to the developer as to how that percolates up to the UI. You can use functional programming techniques, such as the one I set out in my last article, or you could throw an exception (I realize many people prefer this approach). There is a method called Throw which does exactly that i.e. throws an custom exception that wraps the ValidationFailures dictionary. You can catch that upstream and display appropriate messages to the users.

Externalizing Validation

Depending on how you structure your solutions, you may not want to include validation logic directly inside your business services. The Validator class does not actually contain validation logic, it just reacts to the result of validation logic execution. You can easily encapsulate a Validator in a more meaningful validation object or abstraction. Each such abstraction/class should be created for each business service method. Such an abstraction may look like this:

public interface IEnrolmentAttemptValidator : IValidator
{
Validator IsEnrolled(string unitCode, string studentId);
Validator IsEligible(string unitCode, string studentId);
Validator IsFinancial(string unitCode);
}

The Merge method comes in handy here and you can see an example of this implementation in the samples project in the git repo. But as I said, formalizing such validators as dependencies is a design decision which varies from project to project.

Flexibility

I mentioned at the outset that I wanted flexibility. To that end, I have exposed the method with collects up validation failures. So, you can even have validation performed completely independently of the Validator object and run code such as this to add failures:

// ... validation already run previous to here and failed
// ... ideal for null values which can't be passed to Validate
var validator = new Validator();// add the failures to the new validator object
validator.AddFailure("EnrolmentIssue", "Student is not eligible")
.AddFailure("EnrolmentIssue", "Student already enrolled")
.AddFailure("StudentFeesOwing", "Student has not paid fees");

This really highlights the core purpose of this Business Validation project. Sure it will run validation code that you pass to it, but it mainly enables you to scoop up failures, merge them with others and to pass them around. Note that AddFailure has a fluid interface for convenience.

Another convenience which I have built in is an indexer which enables you to access the messages list of a fail-bundle by using its name (which just passes through to the indexer of the ValidationFailures dictionary):

IReadOnlyList<string> messages = validator["EnrolmentIssue"];

How Do I Get It?

The git repo for Business Validation is available on Github (please log any bugs over there). I’ve also made it available as a nuget package.

This is not meant to be a huge library, with oodles of functionality; and it is by no means any engineering feat. It’s very simple, and for the most part, feature complete (perhaps a smart pull request or two will persuade me otherwise). It is not meant to replace, or even compete with, FluentValidation; it occupies a different space. Use that for validating payloads and use this in your business layer.

Hopefully, you will find it as useful as I do. I may write a follow-up article showing how I use it in my Server Framework, where it integrates really nicely with some of my other custom types, such as the ApiResponse (if readers are interested). Enjoy!

--

--