In the last post we showed an api for how we can inject additional metadata into our validation results. The implementation is relatively straightforward - but there are a few moving parts that need to mesh together appropriately.
To preserve the transient nature of success and aggregate results, we only want to support metadata for warning and error; we therefore introduce an intermediate class to avoid repetition:
public abstract class ValidationResultWithMetadata : ValidationResult
{
public IReadOnlyDictionary<string, object> Metadata { get; }
// Prevent any implementations outside this assembly
private protected ValidationResultWithMetadata(
IEnumerable<ValidationMetadata> metadata)
{
if (metadata is null)
{
throw new ArgumentNullException(nameof(metadata));
}
Metadata = metadata.ToImmutableDictionary(
m => m.Name,
m => m.Value,
StringComparer.OrdinalIgnoreCase);
}
}
All this does is to declare the property Metadata
and ensure it’s properly populated by the constructor.
We then modify ErrorResult
to descend from ValidationResultWithMetadata
:
public sealed class ErrorResult
: ValidationResultWithMetadata, IEquatable<ErrorResult>
{
public string Message { get; }
public ErrorResult(
string message, IEnumerable<ValidationMetadata> metadata)
: base(metadata)
{
Message = message
?? throw new ArgumentNullException(nameof(message));
}
// ...
}
The constructor accepts the metadata as you’d expect. The changes to WarningResult
are similar.
Next, we modify our factory methods to make it easy to pass metadata when the consumer wants to do so:
public static ValidationResult Error(
string message, params ValidationMetadata[] metadata)
=> new ErrorResult(message, metadata);
public static ValidationResult ErrorWhen(
bool isError, string message, params ValidationMetadata[] metadata)
=> isError ? new ErrorResult(message, metadata) : _success;
Using the params keyword for these methods gives our consumers a choice about passing metadata, or not. It’s also a signature change we can make to the methods that doesn’t break any existing code. Once you’ve got a thousand references to .ErrorWhen()
across your codebase, you really don’t want to have to manualy modify all of them for a change.
Again, I’ll elide the changes for warnings as they’re not substantively different.
All we need now for this to work is the ValidationMetadata
class itself - and it’s just not that interesting, as it’s just a simple immutable container type:
public class ValidationMetadata
: IEquatable<ValidationMetadata>
{
private static readonly StringComparer _comparer =
StringComparer.OrdinalIgnoreCase;
public string Name { get; }
public object Value { get; }
public ValidationMetadata(string name, object value)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentNullException(nameof(name));
}
Name = name;
Value = value;
}
public bool Equals(ValidationMetadata other)
=> _comparer.Equals(Name, other?.Name)
&& Equals(Value, other?.Value);
public override bool Equals(object obj)
=> obj is ValidationMetadata vm
&& Equals(vm);
public override int GetHashCode()
=> Name.GetHashCode() ^ Value.GetHashCode();
public override string ToString() => $"{Name}: {Value}";
}
There’s nothing in this tiny type that we haven’t already covered multiple times in this blog.
Next time we’ll work on eliminating our magic strings and making the API even easier to use.
Comments
blog comments powered by Disqus