5
\$\begingroup\$

I have still another version of my validation extensions. I've reworked it and added some new features. It doesn't relay on expression trees any more but as a compensation the same extensions can be used for unit testing.


The base class is still the ValidationContext:

public class ValidationContext<TArg>
{
    private string _memberName;

    public TArg Argument { get; internal set; }

    public string MemberName
    {
        get
        {
            if (!string.IsNullOrEmpty(_memberName)) return _memberName;
            var memberExpression = Expression().Body as MemberExpression;
            return memberExpression.Member.Name;
        }
        set { _memberName = value; }
    }

    internal Func<Expression<Func<TArg>>> Expression { get; set; }

    public virtual ValidationContext<TArg> Validate<TException>(
        Func<TArg, bool> predicate, 
        params object[] args) 
        where TException : Exception
    {
        if (!predicate(Argument))
        {
            throw (TException)Activator.CreateInstance(typeof(TException), args);
        }
        return this;
    }
}

however in this version it is the one that throws exceptions after checking a validation rule. The Validate method can be overriden in a derived class which allows to create a new validation context for unit testing. It is now able to throw a different kind of exception that the unit testing environment will notice. It forwards the original exception as the inner one.

public class UnitTestingValidationContext<TArg> : ValidationContext<TArg>
{
    public override ValidationContext<TArg> Validate<TException>(
        Func<TArg, bool> predicate, 
        params object[] args)
    {
        try
        {
            if (!predicate(Argument))
            {
                throw (TException)Activator.CreateInstance(typeof(TException), args);
            }
            return this;
        }
        catch (Exception inner)
        {
            // cannot throw this in linqpad
            //throw new AssertFailedException
            throw new Exception("This is a test.", inner);
        }
    }
}

As a result the Validations extensions have changed a little and have gotten a new Test method and refactored validations:

public static class Validations
{
    // crates validation context for normal usage
    public static ValidationContext<TArg> Validate<TArg>(
        this TArg arg, 
        string memberName)
    {
        return new ValidationContext<TArg>()
        {
            Argument = arg,
            MemberName = memberName
        };
    }

    // create validation context for unit-testing
    public static UnitTestingValidationContext<TArg> Test<TArg>(
        this TArg arg, 
        string memberName)
    {
        return new UnitTestingValidationContext<TArg>()
        {
            Argument = arg,
            MemberName = memberName
        };
    }

    // validations don't throw exceptions anymore but tell the context how to do it
    public static ValidationContext<TArg> IsNotNull<TArg>(
        this ValidationContext<TArg> context)
    {
        return context.Validate<ArgumentNullException>(arg => 
            arg != null, context.MemberName, "Test message.");
    }

    public static ValidationContext<string> IsNotNullOrEmpty(
        this ValidationContext<string> context)
    {
        return context.Validate<ArgumentNullException>(arg => 
            !string.IsNullOrEmpty(context.Argument), context.MemberName, "Test message.");
    }
}

Examples:

var foo = (string)null;

// normal usage like for method parameters etc.
foo.Validate("foo").IsNotNullOrEmpty(); // bam!

// in unit-testing
foo.Test("foo").IsNotNullOrEmpty(); // bam!

Some more examples showing how I mean to use the extensions (this is a special case where I wrap the validation in an Action because it looks nice when alls tests look the same):

[TestMethod]
public void IsNullPasses()
{
   new Action(() => ((string)null).Validate().IsNull()).Test().DoesNotThrow();
}

[TestMethod]
public void IsNullThrows()
{
    new Action(() => "".Validate().IsNull()).Test().Throws<ArgumentException>();
}
\$\endgroup\$
13
  • \$\begingroup\$ Mind if I ask why TArg has an internal setter? You don't see that very often. \$\endgroup\$
    – RubberDuck
    Jun 14, 2016 at 11:36
  • \$\begingroup\$ @RubberDuck It's because I want to use the object initializer to set its value but I don't want the user to be able to do that and I don't like to use constructors for this ;-) and since I'm the only one doing this I consider it safe ;-] \$\endgroup\$
    – t3chb0t
    Jun 14, 2016 at 11:38
  • 1
    \$\begingroup\$ Then you trust the future version of yourself much more than I trust the future version of myself. I'd tighten that up and use a ctor. If you don't need to be able to reset the property, don't allow it. It's much easier to relax scope than it is to tighten it. Apologies for answering in the comments. \$\endgroup\$
    – RubberDuck
    Jun 14, 2016 at 11:41
  • 3
    \$\begingroup\$ @eurotrash you could argue that having the cast on the rhs makes it more explicit. Plus if you have several vars near each other it may make the code readable if the type information is consistently on the right. \$\endgroup\$
    – RJFalconer
    Jun 14, 2016 at 13:23
  • 1
    \$\begingroup\$ Does var x = (string)null; really look prettier than string x = null;? Really?? I use var all the time, but imo a cast always makes things look uglier, so if that's required I'd use the type name explicitly. \$\endgroup\$
    – 404
    Jun 14, 2016 at 14:21

1 Answer 1

1
\$\begingroup\$

I wasn't happy with the above solution so I've refactored it a little bit to get this:

The validation context had too many resposibilities so it has lost its Validate method:

public class ValidationContext<TArg>
{
    public ValidationContext(TArg argument, string memberName)
    {
        Argument = argument;
        MemberName = memberName;
    }

    public TArg Argument { get; }

    public string MemberName { get; }
}

Instead I created a Validation class that stores the result and if necessary throws an exception:

public class Validation<T>
{
    public Validation(ValidationContext<T> context, bool success)
    {
        Context = context;
        Success = success;
    }

    public ValidationContext<T> Context { get; }

    public bool Success { get; set; }

    public ValidationContext<T> Throw<TException>(Func<ValidationContext<T>, string> getMessage) where TException : Exception
    {
        if (Success)
        {
            return Context;
        }

        var message = getMessage(Context)?.Format(Context);
        throw (TException)Activator.CreateInstance(typeof(TException), message);
    }
}

and a validation builder that executes the validation and return the result as Validation object:

public static class ValidationBuilder
{
    public static Validation<TArg> If<TArg>(this ValidationContext<TArg> context, Func<TArg, bool> predicate)
    {
        return new Validation<TArg>(context, predicate(context.Argument));
    }
}

Its usage didn't change much:

public static ValidationContext<TArg> IsNotNull<TArg>
(
    this ValidationContext<TArg> context, Func<ValidationContext<TArg>,
    string> getMessage = null
)
{
    return context
        .If(arg => arg == null)
        .Throw<ValidationException>(
            getMessage ?? (ctx => $"'{ctx.MemberName}' must not be null."));
}

and a real-world usage example:

public static bool IsStatic(this Type type)
{
    type.Validate(nameof(type)).IsNotNull();
    return type.IsAbstract && type.IsSealed;
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.