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.

Prior post in this series:
Modelling Validation Metadata
Next post in this series:
Avoiding Magic Strings

Comments

blog comments powered by Disqus