As often I am writing about topics I encounter in my daily life. So just imagine you have a blog... like this one you are reading right now. This has some properties. Simplified your model could look like this:
public class BlogPostModel
{
[Required]
public string Title { get; set; }
[Required]
public string ShortDescription { get; set; }
[Required]
public string Content { get; set; }
[Required]
public string PreviewImageUrl { get; set; }
[Required]
public bool IsPublished { get; set; } = true;
}
That looks very nice. Basically the user (in this case: me) has to fill up all the properties otherwise the form will not get submitted. But what if the blog post gets not published IsPublished == false
. Do we still have to provide all the information? Unfortunately yes. Blazor does not offer us any possibility to give the Required
attribute any context. If Blazor doesn't, then we do!
RequiredIf
The goal is that we want to express:
Content
is only mandatory, if we publish the blog post!
So let us start with the notation. I want something like this:
public class BlogPostModel
{
[Required]
public string Title { get; set; }
[RequiredIf(nameof(IsPublished), true)]
public string Content { get; set; }
[Required]
public bool IsPublished { get; set; }
}
Now here a few things:
Title
is still mandatory. I don't want to change thatContent
should be only required ifIsPublished
istrue
Why the nameof
notation. Well attributes are nice and cool but they lag some key features. For example generics. An attribute is not allowed to have generics. So we have to workaround that. To be able to be used for validation we have to at least extend ValidationAttribute
. Have a look at the documentation here. You can see that there are a lot of derived attributes.
So let's go, the constructor is pretty much done:
public class RequiredIfAttribute : ValidationAttribute
{
private readonly string _propertyName;
private readonly object? _isValue;
public RequiredIfAttribute(string propertyName, object? isValue)
{
_propertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName));
_isValue = isValue;
}
Well that was easy.. the heavy lifting will follow. But first let us override the error message. We want to indicate in the error message as well, that there is some dependencies. We can do so by overriding FormatErrorMessage
public override string FormatErrorMessage(string name)
{
var errorMessage = $"Property {name} is required when {_propertyName} is {_isValue}";
return ErrorMessage ?? errorMessage;
}
In our case with the blog post, we want to say: Property content is required when IsPublished is not True.
Now the central function: ValidationResult? IsValid(object? value, ValidationContext validationContext)
. (Here for more information)
This one gets called for every property with a RequiredAttribute
. So this is our point to jump in and do the heavy work.
The first thing we want to do is: Check if we have every information. That also includes, does our propertyName
which we got via nameof(IsPublished)
really exist?
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
ArgumentNullException.ThrowIfNull(validationContext);
var property = validationContext.ObjectType.GetProperty(_propertyName);
if (property == null)
{
throw new NotSupportedException($"Can't find {_propertyName} on searched type: {validationContext.ObjectType.Name}");
}
I guess the only line which needs a bit more explanation is the following: var property = validationContext.ObjectType.GetProperty(_propertyName);
The ValidationContext.ObjectType
holds our model (BlogPostModel
). That is important because we need to resolve the dependency to our property. Therefore we need the type and also the concrete instance to get the actual value of IsPublished
. So this snippet gives us the type information.
var requiredIfTypeActualValue = property.GetValue(validationContext.ObjectInstance);
if (requiredIfTypeActualValue == null && _isValue != null)
{
return ValidationResult.Success;
}
var requiredIfNotTypeActualValue = property.GetValue(validationContext.ObjectInstance);
does exactly what I wrote before. We want to get the real value of our RequiredIf
"pointer". Again in our case we want to have IsPublished
.
Afterwards we make a quick check the actual value is null and the expected is not null. The both would be null, we would consider the property as required. If not we can go out here and can say: "Everything fine!". And now exactly that is left: What if the property is required? Well then we just have to check if it is set or not:
if (requiredIfTypeActualValue == null || requiredIfTypeActualValue.Equals(_isValue))
{
return value == null
? new ValidationResult(FormatErrorMessage(validationContext.DisplayName))
: ValidationResult.Success;
}
return ValidationResult.Success;
The whole if does nothing more than checking, if our IsPublished
is either null
or the required value. If so, we check if value
is null. If so that would be a violation and leads to an error result.
Why did I use requiredIfTypeActualValue.Equals(_isValue)
instead of requiredIfTypeActualValue == _isValue
. If you check the types, they are all object
s. That means ==
would check for ReferenceEquals
. Not so great if we use bool
.
Links and resources
Now if you just want to use this and more: I am currently building a library which houses such helper functionality. I am also planning to add dependencies outside of the model.
If you want to see the code plus tests: Here you go on github. And here directly the nuget package.
The whole Thing
using System.ComponentModel.DataAnnotations;
namespace LinkDotNet.ValidationExtensions;
public class RequiredIfAttribute : ValidationAttribute
{
private readonly string _propertyName;
private readonly object? _isValue;
public RequiredIfAttribute(string propertyName, object? isValue)
{
_propertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName));
_isValue = isValue;
}
public override string FormatErrorMessage(string name)
{
var errorMessage = $"Property {name} is required when {_propertyName} is {_isValue}";
return ErrorMessage ?? errorMessage;
}
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
ArgumentNullException.ThrowIfNull(validationContext);
var property = validationContext.ObjectType.GetProperty(_propertyName);
if (property == null)
{
throw new NotSupportedException($"Can't find {_propertyName} on searched type: {validationContext.ObjectType.Name}");
}
var requiredIfTypeActualValue = property.GetValue(validationContext.ObjectInstance);
if (requiredIfTypeActualValue == null && _isValue != null)
{
return ValidationResult.Success;
}
if (requiredIfTypeActualValue == null || requiredIfTypeActualValue.Equals(_isValue))
{
return value == null
? new ValidationResult(FormatErrorMessage(validationContext.DisplayName))
: ValidationResult.Success;
}
return ValidationResult.Success;
}
}