FluentValidation 8.1 released

FluentValidation 8.1 is now available. This release contains several new features and bug fixes:

String Formatting in Message Placeholders

FluentValidation has always supported placeholders within error messages, such as {PropertyValue}:

RuleFor(x => x.Age).GreaterThan(18).WithMessage("Must be older than 18 years. You entered {PropertyValue} years.");

Now you can also specify standard .NET formatting strings as part of the placeholder, such as {PropertyValue:d} or {PropertyValue:p1} etc.

Overriding Indexers for Collection Rules

Collection rules built using RuleForEach now allow you to override the indexer. For example, imagine you defined a collection rule against an collection of Address lines:

RuleForEach(x => x.AddressLines).NotNull();

…Then the validator would generate validation failures for properties person.AddressesLines[0] and person.AddressLines[1] etc. The indexer would always be the numeric index within the collection surrounded by square brackets. This can now be overridden to use something different, or remove the indexer completely. Eg:

v.RuleForEach(x => x.AddressLines)
  .OverrideIndexer((x, collection, element, index) => "<" + index + ">")

This would use angle brackets rather than square brackets in the generated indexer.

Error code can now be used to select a different default error message.

You can now use error codes to point to a particular error message stored in the default language manager. For example, the default ‘NotNull’ validation message has a code of NotNullValidator and a message of '{PropertyName}' must not be empty. If you want to use this message from inside a different validator (eg for a custom validator), you would have to hard-code the message again:

RuleFor(x => x.Name).NotNull(); //defaults to '{PropertyName}' must not be empty.
// `Must` has its own default error message, so if you want to use the NotNull message you'd have to specify it explicitly
RuleFor(x => x.Name).Must(name => name != null).WithMessage("'{PropertyName}' must not be empty.");

However now you can select a message based on error code:

RuleFor(x +> x.Name).Must(name => name != null).WithErrorCode("NotNullValidator"); 

Test Helper improvements

Additional details are now shown in the output for ShouldNotHaveValidationErrorFor.

Conditional validation improvements

If you use the top-level When or Unless methods to wrap multiple rules, the condition check is now cached and only executed once.

// The IsStudnet check will now only be executed once.
When(x => x.IsStudent, () => {
   RuleFor(x => x.StudentNumber).NotNull();
   RuleFor(x => x.Courses).NotNull();

There’s also a new Otherwise method that can now be chanined onto a When or Unless to do its opposite, without having to set up a second When call with the opposite condition:

When(x => x.IsStudent, () => {
   RuleFor(x => x.StudentNumber).NotNull();
   RuleFor(x => x.Courses).NotNull();
}).Otherwise(() => {
   RuleFor(x => x.StudentNumber).Null();

Other minor changes

  • MVC 5’s CustomizeValidatorAttribute now has the Skip property, like WebApi and AspNetCore, allowing auto-validation to be skipped for specific action parameters.
  • Japanese language translation
  • The overload of OverridePropertyName that takes an expression can now use an expression with any return value, not just strings.
  • Minor wording changes to the default English error messages.
Written on December 6, 2018

Updating .NET Core inside AppVeyor

I currently use appveyor to run Continuous Integration builds for FluentValidation. It’s really easy to get started with appveyor as the build images come pre-installed with most tools you’d want to use as part of a build. The downside is it’s not easy to specify which version of a particular tool you want to use, and appveyor often lags behind with new releases.

For example, for FluentValidation I want the master branch to be built against the current LTS version of the .NET Core SDK (2.1.400), and I want my vNext branch to build against 2.2-preview1. But appveyor only has version 2.1.300 installed, so both branches need a new version of the .NET SDK.

Based on this post by Andrew Lock, I updated the FluentValidation build script to download an install a newer .NET Core SDK if needed. The build script uses powershell with the posh-build helpers for defining targets:

target install-dotnet-core {
  # Ensures that .net core is up to date.
  # first get the required version from global.json
  $json = ConvertFrom-Json (Get-Content "$path/global.json" -Raw)
  $required_version = $json.sdk.version

  # Running dotnet --version stupidly fails if the required SDK version is higher 
  # than the currently installed version. So move global.json out the way 
  # and then put it back again 
  Rename-Item "$path/global.json" "$path/global.json.bak"
  $current_version = (dotnet --version)
  Rename-Item "$path/global.json.bak" "$path/global.json"
  Write-Host "Required .NET version: $required_version Installed: $current_version"

  if ($current_version -lt $required_version) {
    # Current installed version is too low.
    # Install new version as a local only dependency. 
    $urlCurrent = "https://dotnetcli.blob.core.windows.net/dotnet/Sdk/$required_version/dotnet-sdk-$required_version-win-x64.zip"
    Write-Host "Installing .NET Core $required_version from $urlCurrent"
    $env:DOTNET_INSTALL_DIR = "$path/.dotnetsdk"
    New-Item -Type Directory $env:DOTNET_INSTALL_DIR -Force | Out-Null
    (New-Object System.Net.WebClient).DownloadFile($urlCurrent, "dotnet.zip")
    Write-Host "Unzipping to $env:DOTNET_INSTALL_DIR"
    Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory("dotnet.zip", $env:DOTNET_INSTALL_DIR)

Here I do the following:

  • Check which version of .net core is needed based on what’s defined in my global.json
  • Check the current .net core installed version (working around that dotnet --version fails if there’s a mismatch between required and expected version)
  • Download the new SDK and unzip it to a local directory
  • Update the environment PATH variable so that the new version is used whenever dotnet is called.

This script can then be called as part of the install section of your appveyor.yml.

The full example is available in the FluentValidation repository.

Written on August 28, 2018

FluentValidation 8.0 released

FluentValidation is out and available to download from Nuget. This is a major release with several breaking change, so please make sure you read the upgrade notes before upgrading.

Validating properties by path

You can now validate specific properties using a full path, eg:

validator.Validate(customer, "Address.Line1", "Address.Line2");

Validating a specific ruleset with SetValidator

Previously, if you defined a child validator with SetValidator, then whichever ruleset you invoked on the parent validator will cascade to the child validator. Now you can explicitly define which ruleset will run on the child:

RuleFor(x => x.Address).SetValidator(new AddressValidator(), "myRuleset");

AttrbiutedValidatorFactory has been moved to a separate package

The ValidatorAttribute and the AttributedValidatorFactory were typically used in MVC/WebApi projects to wire models to their validators. This is no longer recommended when usign AspNetCore as the built-in Service Provider is a better alternative. These classes can still be used by explicitly installing the FluentValidation.ValidatorAttribute package. Note this package is installed by default if you are using the legacy MVC5/WebApi integration rather than AspNetCore.

SetCollectionValidator is deprecated

RuleForEach provides a more flexible syntax for the same result.

Async changes

Internally, the asynchronous validation API has been cleaned up thanks to await\async. From a consumer’s point of view, the asynchronous methods should all continue to work as before with the exception of some methods that previously didn’t take a CancellationToken that now do.

The full changelog is available here

Written on August 16, 2018

Powershell SSH Connection manager

I’ve recently a connection manager for SSH connections to the posh-sshell project.

What is Posh-Sshell?

Posh-Sshell is a set of powershell scripts that making working with SSH agents and clients easier. These utilities were originally part of the posh-git project, but have been separated into a separate module in preparation for the Posh-Git 1.0 release.

Posh-Sshell can be downloaded from the powershell gallery by running the following in a powershell prompt:

Install-Module Posh-Sshell -Scope CurrentUser

Once installed, import the module with Import-Module Posh-Shell (you can add this to your powershell profile so you don’t need to run it every time you launch a new terminal).

Connection Manager

Since splitting the SSH functionality out of posh-git, I’ve been working on several new features the first of which is the Connection Manager.

The Connection Manager can be used to display a list of SSH connections as well as add/remove connections from the ~/.ssh/config file. By running Connect-Ssh, you’ll be presented with a list of connections stored in your .ssh/config file:


You can enter the number of the server into the prompt, and an SSH connection will be made to that server. If no username is specified in the configuration file, then you’ll also be prompted for a username.

Adding a New Connection

A new connection can be added by running Add-SshConnection. In its simplest form, it takes an alias and a URI, eg:

Add-SshConnection MyServer3 myserver.mydomain.com

You can also specify additional common properties such as the username, either by using the -User parameter or using the user@host syntax:

# These are both the same
Add-SshConnection MyServer3 myserver.mydomain.com -User jeremy
Add-SshConnection MyServer3 jeremy@myserver.mydomain.com

You can specify a custom key file by using -IdentityFile <path> as well as parameters for configuring an SSH tunnel with -LocalTunnelPort <port number> and -RemoteTunnelPort <port number>.

Additional parameters can also be specified by supplying a hashtable to the -AdditionalOptions parameter.

After running Add-SshConnection MyServer3 myserver.mydomain.com -User jeremy, your .ssh/config file will contain the new entry:

Host MyServer3
  HostName myserver.mydomain.com
  User jeremy

Removing an entry

Entries can be removed from the config file by running Remove-SshConnection <name>

Written on July 30, 2018

Thoughts on using Drupal

After 10 years working with ASP.NET and C#, I’ve spent the last few months working with Drupal 8 and PHP. The last time I worked with PHP was in 2005, and although the language is still full of inconsistencies, it’s nice to see that it’s a fully object-oriented language and supports more modern functionality like anonymous functions and traits.

Working with Drupal 8 has been quite a learning experience. Drupal is an extremely powerful platform, but has a huge learning curve. The documentation is pretty poor compared to the very well-written in-depth tutorials and API docs that ASP.NET provides. However It’s good to see that Drupal 8 has embraced a more MVC-based approach (by using Symfony controllers), dependency injection and plugins. Sadly the horrible hook-based extensibility system hasn’t completely gone away yet.

The hardest thing I’ve found with Drupal is following an application’s flow. Diving into an existing codebase and trying to find where a piece of functionality is implemented is something I found extremely challenging. For example a button on a form could be defined directly in a Form class, through a YAML file, or any number of hooks in any module within the application. I suppose this is the downside of an extremely flexible extensibility model.

I’ve also been enjoying getting much more into Linux and server management using the Bash CLI, something I’ve had very little experience with (my CLI of choice is usually Powershell)

Overall C#/.NET would still be my platform of choice, but learning something new and working on some very interesting projects with a great team is much more important to me than choice of language.

Written on July 28, 2018