简体   繁体   中英

How should I design string validation classes in C#?

I have several applications within my domain that accept similar inputs in text fields. Each application implements its own validation. I want to bring that functionality into a class library so that rather than re-inventing the wheel on each project, our developers can quickly implement the validation library, and move on.

I'm not the best when it comes to OO design. What I need is the ability for a user to enter an arbitrary string, and then for the validation library to check it against the known types to make sure that it matches one of them. Should I build an interface and make each type of string a class that implements that interface? (seems wrong since I won't know the type when I read in the string). I could use some help identifying a pattern for this.

Thanks.

I've always been a fan of Fluent Validation for .Net. If it's more robust then you need, it's functionality is easy enough to mimic on your own.

If you're interested, here's a link to my very simple validation class . It's similar in usage to Fluent Validation, but uses lambdas to create the validation assertions. Here's a quick example of how to use it:

public class Person
{
    public Person(int age){ Age = age; }
    public int Age{ get; set;}
}

public class PersonValidator : AbstractValidator
{
    public PersonValidator()
    {
        RuleFor(p => p.Age >= 0,
            () => new ArgumentOutOfRangeException(
                "Age must be greater than or equal to zero."
        ));
    }
}

public class Example
{
    void exampleUsage()
    {
        var john = new Person(28);
        var jane = new Person(-29);

        var personValidator = new PersonValidator();

        var johnsResult = personValidator.Validate(john);
        var janesResult = personValidator.Validate(jane);

        displayResult(johnsResult);
        displayResult(janesResult);
    }

    void displayResult(ValidationResult result)
    {
        if(!result.IsValid)
            Console.WriteLine("Is valid");
        else
            Console.WriteLine(result.Exception.GetType());
    }
}

(see source code for a more thorough example).

Output:

Is valid
System.ArgumentOutOfRangeException

Each application implements its own validation. I want to bring that functionality into a class library so that rather than re-inventing the wheel on each project, our developers can quickly implement the validation library, and move on.

Your problem seems similar to custom NUnit constraints.

NUnit allows something they call a constraint-based assertion model , and allow the user to create custom constraints , saying whether or not a given object satisfies the criteria of that constraint.

Using an object-based constraint model is superior to a purely function-based constraint model:

  • It lets you aggregate sub-constraints to evaluate a higher level constraint.
  • It lets you provide diagnostic information as to why a specific constraint doesn't match your input data.

This sounds fancy, but constraints are just functions that take a parameter of your desired type, returns true if it matches, and false if it doesn't.

Adapting it to your problem

What I need is the ability for a user to enter an arbitrary string, and then for the validation library to check it against the known types to make sure that it matches one of them.

You don't actually have to build assertions out of your constraints. You could evaluate constraints without throwing exceptions, and do your classifications first.

But I don't recommend you do any automatic classification. I recommend you attach a specific constraint to a specific input, rather than trying to match all available constraints. Pass in the string to that constraint, and call it done.

If you need to do this for higher level objects, build a constraint for the higher level object that uses specific (existing) constraints for each of its sub fields, as well as doing cross-field constraint validation.

When you're done, you can aggregate all constraint violations to the top level, and have your validation logic throw an exception containing all the violations.

BTW, I wouldn't use the exact same interface NUnit does:

  • It is a confusing design
  • I'd prefer an approach that used generics all the way through
  • I'd prefer an approach that allowed you to return an IEnumerable<ConstraintViolation> or IEnumerable<string> , rather than taking some sort of output writer class as a dependency

But I'd definitely steal the base concept :)

Implementation

Here's an example implementation of what I'm talking about:

public class ConstraintViolation
{
    public ConstraintViolation(IConstraintBase source, string description)
    {
        Source = source;
        Description = description;
    }

    public IConstraintBase Source { get; }
    public string Description { get; set; }
}

public interface IConstraintBase
{
    public string Name { get; }
    public string Description { get; }
}

public interface IConstraint<T> : IConstraintBase
{
    public IEnumerable<ConstraintViolation> GetViolations(T value);
}

And here's an example constraint to validate the length of a string (a weak example, but see my comments about this below):

public class StringLengthConstraint : IConstraint<string>
{
    public StringLengthConstraint(int maximumLength)
        : this(minimumLength: 0, maximumLength: maximumLength)
    {
    }

    public StringLengthConstraint(int minimumLength, int maximumLength,
        bool isNullAllowed = false)
    {
        MinimumLength = minimumLength;
        MaximumLength = maximumLength;
        IsNullAllowed = isNullAllowed;
    }

    public int MinimumLength { get; private set; }
    public int MaximumLength { get; private set; }
    public bool IsNullAllowed { get; private set; }

    public IEnumerable<ConstraintViolation> GetViolations(string value)
    {
        if (value == null)
        {
            if (!IsNullAllowed)
            {
                yield return CreateViolation("Value cannot be null");
            }
        }
        else
        {
            int length = value.Length;

            if (length < MinimumLength)
            {
                yield return CreateViolation(
                    "Value is shorter than minimum length {0}",
                    MinimumLength);
            }

            if (length > MaximumLength)
            {
                yield return CreateViolation("Value is longer than maximum length {0}",
                    MaximumLength);
            }
        }
    }

    public string Name
    {
        get { return "String Length"; }
    }

    public string Description
    {
        get
        {
            return string.Format("Ensure a string is an acceptable length"
                + " - Minimum: {0}"
                + ", Maximum: {1}"
                + "{2}"
                , MinimumLength
                , MaximumLength
                , IsNullAllowed ? "" : ", and is not null"
                );
        }
    }

    private ConstraintViolation CreateViolation(string description,
        params object[] args)
    {
        return new ConstraintViolation(this, string.Format(description, args));
    }
}

Here's how to use it when doing validation of a single field:

var violations = new StringLengthConstraint(10).GetViolations(value);

if(violations.Any())
{
    throw new InvalidArgumentException("value", string.Join(", ", violations));
}

Justification

The string length constraint is a lot of code to do something stupidly simple, especially if you're doing this just once. But there are advantages to this approach:

It is reusable

Write this or use it once, and I'd agree this is a pain.

But most of the code here is to allow this to be reusable. For example you can select this out of a list of constraints for a string type. Or you can display a list of constraints or constraint violations on a UI, with tooltips, etc. Or you can use it in a unit testing framework; With an adapter class it could plug directly into NUnit.

This model supports aggregating constraints and violations

  • Through Linq
  • Through object composition

Linq:

var violations = new SomeConstraint(someData).GetViolations(value)
    .Concat(new SomeOtherConstraint(someData).GetViolations(value))
    ;

Object composition:

// ...
public IEnumerable<ConstraintViolation> GetViolations(SomeType value)
{
    if(value == 42)
    {
        yield return new ConstraintViolation(this, "Value cannot be 42");
    }

    foreach(var subViolation in subConstraint.GetViolations(value))
    {
        yield return subViolation;
    }
}

private SomeSubConstraint subConstraint;

You need to do the following:

  1. Parse your string and figure out (somehow) what type of string it is. I'd prefer to know it before the validation (by assigning types to fields), because if some string is incorrect, you can assign incorrect type for it.
  2. Validate your string based on the validation rules applicable to the given field type. These validators should implement some interface, so you can validate any type of string. Usually you have not only field-type-specific validation, but field-specific-validation, so this kind of validators should also implement the same interface.

Everything else is depending on your app-specific logic.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM