16
\$\begingroup\$

One of my biggest complaints about .NET is that there's no way to guarantee a string match a certain type in the type-system.

Note

This started as more of a proof-of-concept than a real usable system, but I'm curious about feasibility in real-world work now, because it does seem moderately usable.

That is, say I want an alpha-numeric string, or I want it to be no longer than a certain length, I have no guarantee that the string passed to a function will meet those requirements. I have to run my validation each and every time I call a function that needs that validity.

This problem is a tough problem to correct, especially as string is sealed. Because we cannot inherit from a string, we have to build our own implementation.

As a result, I built a simple implementation that seems to work properly, but I'm curious on any intricacies I might have missed.

I tried to make sensible decisions for the case when certain things are null, but I'm curious on any other suggestions anyone might have for other situations that have been missed.

It starts with the ValidatedString abstract class:

[JsonConverter(typeof(ValidatedStringJsonNetConverter))]
public abstract class ValidatedString
    : IComparable, IEnumerable, IEnumerable<char>, IComparable<string>, IComparable<ValidatedString>, IEquatable<string>, IEquatable<ValidatedString>, IXmlSerializable
{
    protected abstract string ErrorRequirement { get; }
    protected Exception Exception => new ArgumentException($"The value must {ErrorRequirement}");

    public string String { get; private set; }
    public int Length => String.Length;
    public char this[int index] => String[index];

    protected ValidatedString() { }
    public ValidatedString(string str)
    {
        String = Validate(str);
    }

    private string Validate(string str) => IsValid(str) ? str : throw Exception;

    protected abstract bool IsValid(string str);

    public static implicit operator string(ValidatedString str) => str?.String;
    public override bool Equals(object obj) => (String == null && obj == null) || (String?.Equals(obj) ?? false);
    public override int GetHashCode() => String?.GetHashCode() ?? 0;
    public override string ToString() => String?.ToString();

    int IComparable.CompareTo(object obj) => (String == null && obj == null) ? 0 : ((IComparable)String)?.CompareTo(obj) ?? 0;
    IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)String)?.GetEnumerator();
    public IEnumerator<char> GetEnumerator() => ((IEnumerable<char>)String?.ToCharArray()).GetEnumerator();
    public int CompareTo(string other) => (String == null && other == null) ? 0 : String?.CompareTo(other) ?? other.CompareTo(String);
    public int CompareTo(ValidatedString other) => (String == null && other.String == null) ? 0 : String?.CompareTo(other.String) ?? other.String.CompareTo(String);
    public bool Equals(string other) => (String == null && other == null) || (String?.Equals(other) ?? false);
    public bool Equals(ValidatedString other) => (String == null && other.String == null) || (String?.Equals(other.String) ?? false);

    public static bool operator ==(ValidatedString a, ValidatedString b) => a.String == b.String;
    public static bool operator !=(ValidatedString a, ValidatedString b) => a.String != b.String;

    public static int Compare(ValidatedString strA, ValidatedString strB) => string.Compare(strA.String, strB.String);
    [SecuritySafeCritical]
    public static int Compare(ValidatedString strA, ValidatedString strB, StringComparison comparisonType) => string.Compare(strA.String, strB.String, comparisonType);
    public static int Compare(ValidatedString strA, int indexA, ValidatedString strB, int indexB, int length) => string.Compare(strA.String, indexA, strB.String, indexB, length);
    [SecuritySafeCritical]
    public static int Compare(ValidatedString strA, int indexA, ValidatedString strB, int indexB, int length, StringComparison comparisonType) => string.Compare(strA.String, indexA, strB.String, indexB, length, comparisonType);

    public static int CompareOrdinal(ValidatedString strA, ValidatedString strB) => string.CompareOrdinal(strA.String, strB.String);
    [SecuritySafeCritical]
    public static int CompareOrdinal(ValidatedString strA, int indexA, ValidatedString strB, int indexB, int length) => string.CompareOrdinal(strA.String, indexA, strB.String, indexB, length);

    public static bool Equals(ValidatedString a, ValidatedString b) => string.Equals(a.String, b.String);
    [SecuritySafeCritical]
    public static bool Equals(ValidatedString a, ValidatedString b, StringComparison comparisonType) => string.Equals(a.String, b.String, comparisonType);

    XmlSchema IXmlSerializable.GetSchema() => null;
    void IXmlSerializable.ReadXml(XmlReader reader)
    {
        var isEmpty = reader.IsEmptyElement;
        reader.Read();
        if (isEmpty) return;
        String = Validate(reader.Value);
    }

    void IXmlSerializable.WriteXml(XmlWriter writer)
    {
        writer.WriteValue(String);
    }
}

Here, we do a lot of the major work required. This is the foundation of our string validation: we build the infrastructure for it to make sure we work consistently.

From there, it's just a matter of building an implementation. I built a second major abstract class: RegexString, which can be supplied with a regular expression to perform the validation:

public abstract class RegexString
    : ValidatedString
{
    protected abstract string RegexValidation { get; }
    protected abstract bool AllowNull { get; }
    protected override string ErrorRequirement => $"match the Regular Expression: {RegexValidation}";

    private Regex _regex;

    protected RegexString() { }
    public RegexString(string str) : base(str) { }

    protected override bool IsValid(string str)
    {
        if (_regex == null) { _regex = new Regex(RegexValidation); };
        if (str == null) { return AllowNull; }
        return _regex.IsMatch(str);
    }
}

That said, no one has to use the RegexString: it's trivial to build other implementations, like a NonEmptyString:

public class NonEmptyString
    : ValidatedString
{
    protected override string ErrorRequirement => "not be null, empty, or whitespace";

    protected NonEmptyString() { }
    public NonEmptyString(string str) : base(str) { }

    protected override bool IsValid(string str) => !string.IsNullOrWhiteSpace(str);
    public static explicit operator NonEmptyString(string str) => new NonEmptyString(str);
}

Now obviously there's a point to all of this, and I'm getting to that now.

In my situations, I often want to guarantee that certain strings, like a username or email, are of a certain format. Previously, to do that, I would need to add many guard-clauses at the beginning of my function to validate them all. Now, instead, I just change their type:

public class StringEmail : RegexString
{
    protected override string ErrorRequirement => "be a valid email of the format <example>@<example>.<com>";
    protected override string RegexValidation => @"^.+@.+\..+$";
    protected override bool AllowNull => false;

    protected StringEmail() { }
    public StringEmail(string str) : base(str) { }

    public static explicit operator StringEmail(string str) => new StringEmail(str);
}

Then I require that string type in the class:

public class Test
{
    public StringEmail Email { get; set; }
}

This allows me to guarantee that the string is validated before it is given to me. Because there are no conversions, one cannot skip the validation process. Even serialization to/from XML/JSON revalidates the string. (This is why we implement IXmlSerializable, and why we have a ValidatedStringJsonNetConverter below.)

public class ValidatedStringJsonNetConverter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) =>
        writer.WriteValue((value as ValidatedString).String);

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) =>
        Activator.CreateInstance(objectType, reader.Value);

    public override bool CanConvert(Type objectType)
    {
#if NETSTANDARD_1_0
        try
        {
            return Activator.CreateInstance(objectType) is ValidatedString;
        }
        catch
        {
            // If we can't make an instance it's definitely not our type
            return false;
        }
#else
        return objectType.IsSubclassOf(typeof(ValidatedString)) || objectType == typeof(ValidatedString);
#endif
    }
}

A few other basic implementations:

public class StringAlpha
    : RegexString
{
    protected override string RegexValidation => "^[a-zA-Z]*$";
    protected override string ErrorRequirement => "contain only alphabetical (a-z) characters";
    protected override bool AllowNull => true;

    protected StringAlpha() { }
    public StringAlpha(string str) : base(str) { }

    public static explicit operator StringAlpha(string str) => new StringAlpha(str);
}
public class StringAlphaNum
    : RegexString
{
    protected override string RegexValidation => "^[a-zA-Z0-9]*$";
    protected override string ErrorRequirement => "contain only alphabetical (a-z) or numeric (0-9) characters";
    protected override bool AllowNull => true;

    protected StringAlphaNum() { }
    public StringAlphaNum(string str) : base(str) { }

    public static explicit operator StringAlphaNum(string str) => new StringAlphaNum(str);
}
public class StringHex
    : RegexString
{
    protected override string RegexValidation => "^[0-9a-fA-F]*$";
    protected override string ErrorRequirement => "be a hexadecimal number";
    protected override bool AllowNull => true;

    protected StringHex() { }
    public StringHex(string str) : base(str) { }

    public static explicit operator StringHex(string str) => new StringHex(str);
}
public class StringHexPrefix
    : RegexString
{
    protected override string RegexValidation => "^(0x|&H)?[0-9a-fA-F]*$";
    protected override string ErrorRequirement => "be a hexadecimal number (optional 0x or &H prefix)";
    protected override bool AllowNull => true;

    protected StringHexPrefix() { }
    public StringHexPrefix(string str) : base(str) { }

    public static explicit operator StringHexPrefix(string str) => new StringHexPrefix(str);
}
public class StringNum
    : RegexString
{
    protected override string RegexValidation => "^[0-9]*$";
    protected override string ErrorRequirement => "contain only numeric (0-9) characters";
    protected override bool AllowNull => true;

    protected StringNum() { }
    public StringNum(string str) : base(str) { }

    public static explicit operator StringNum(string str) => new StringNum(str);
}

And finally, some of the remaining base classes one could build from:

public abstract class String_N
    : RegexString
{
    protected abstract int MaxLength { get; }
    protected override string RegexValidation => $"^.{{0,{MaxLength}}}$";
    protected override string ErrorRequirement => $"be no more than {MaxLength} characters";
    protected override bool AllowNull => true;

    protected String_N() { }
    public String_N(string str) : base(str) { }
}
public abstract class StringN_
    : RegexString
{
    protected abstract int MinLength { get; }
    protected override string RegexValidation => $"^.{{{MinLength},}}$";
    protected override string ErrorRequirement => $"be no less than {MinLength} characters";
    protected override bool AllowNull => true;

    protected StringN_() { }
    public StringN_(string str) : base(str) { }
}
public abstract class StringNN
    : RegexString
{
    protected abstract int MinLength { get; }
    protected abstract int MaxLength { get; }
    protected override string RegexValidation => $"^.{{{MinLength},{MaxLength}}}$";
    protected override string ErrorRequirement => $"be between {MinLength} and {MaxLength} characters";
    protected override bool AllowNull => true;

    protected StringNN() { }
    public StringNN(string str) : base(str) { }
}
public abstract class StringWhitelist
    : RegexString
{
    private const string _special = @"[\^$.|?*+()";

    protected abstract char[] Whitelist { get; }
    protected override string RegexValidation => $"^[{CreateWhitelist(Whitelist)}]*$";
    protected override string ErrorRequirement => $"contain only the whitelisted characters: {CreateWhitelist(Whitelist)}";
    protected override bool AllowNull => true;

    protected StringWhitelist() { }
    public StringWhitelist(string str) : base(str) { }

    public static string CreateWhitelist(char[] whitelist)
    {
        var result = new StringBuilder(whitelist.Length);

        foreach (var c in whitelist)
        {
            if (_special.IndexOf(c) >= 0)
            {
                result.Append($@"\{c}");
            }
            else
            {
                result.Append(c);
            }
        }

        return result.ToString();
    }
}
public abstract class StringWhitelist_N
    : StringWhitelist
{
    protected abstract int MaxLength { get; }
    protected override string RegexValidation => $"^[{CreateWhitelist(Whitelist)}]{{0,{MaxLength}}}$";
    protected override string ErrorRequirement => $"be no more than {MaxLength} characters and {base.ErrorRequirement}";

    protected StringWhitelist_N() { }
    public StringWhitelist_N(string str) : base(str) { }
}
public abstract class StringWhitelistN_
    : StringWhitelist
{
    protected abstract int MinLength { get; }
    protected override string RegexValidation => $"^[{CreateWhitelist(Whitelist)}]{{{MinLength},}}$";
    protected override string ErrorRequirement => $"be no less than {MinLength} characters and {base.ErrorRequirement}";

    protected StringWhitelistN_() { }
    public StringWhitelistN_(string str) : base(str) { }
}
public abstract class StringWhitelistNN
    : StringWhitelist
{
    protected abstract int MinLength { get; }
    protected abstract int MaxLength { get; }
    protected override string RegexValidation => $"^[{StringWhitelist.CreateWhitelist(Whitelist)}]{{{MinLength},{MaxLength}}}$";
    protected override string ErrorRequirement => $"be between {MinLength} and {MaxLength} characters and {base.ErrorRequirement}";

    protected StringWhitelistNN() { }
    public StringWhitelistNN(string str) : base(str) { }
}

Another note: when using Newtonsoft.Json.JsonConvert or System.Xml.Serialization.XmlSerializer, this serializes directly to/from the raw node, this doesn't serialize the class, but strictly the string:

var xmlSer = new XmlSerializer(test.GetType());
byte[] buffer;
using (var ms = new System.IO.MemoryStream())
{
    xmlSer.Serialize(ms, test);
    buffer = ms.GetBuffer();
}
Console.WriteLine(new UTF8Encoding(false).GetString(buffer));
using (var ms = new System.IO.MemoryStream(buffer))
{
    var result = (Test)xmlSer.Deserialize(ms);
    Console.WriteLine(result.Email);
}
var jsonResult = JsonConvert.SerializeObject(test);
Console.WriteLine(jsonResult);
Console.WriteLine(JsonConvert.DeserializeObject<Test>(jsonResult).Email);

Result:

<?xml version="1.0"?>
<Test xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Email>ebrown@example.com</Email>
</Test>
ebrown@example.com
{"Email":"ebrown@example.com"}
ebrown@example.com

Any commentary is welcome, but especially any commentary with regard to whether this might be safe or not to use.

And finally, if you want to see it on GitHub: EBrown8534/Evbpc.Strings

\$\endgroup\$
  • \$\begingroup\$ I like it but... there is one super important feature missing that when forgotten it makes all this effort in vain and makes most string comparisons fail... I mean trim - I don't know how many times something appeared to be broken only becuase some value had a leading/trailing whitespace ;-) \$\endgroup\$ – t3chb0t Nov 23 '18 at 16:22
  • 2
    \$\begingroup\$ @t3chb0t I actually specifically decided not to implement trim because that can affect the validation. Instead, you would want to whatever.String.Trim() or what-have-you, because it's possible that people would validate against whitespace, and I don't want to negatively impact that idea. \$\endgroup\$ – Der Kommissar Nov 23 '18 at 16:26
  • 2
    \$\begingroup\$ ok, so it's by design - that's an explanation too even though I've never ever seen a case where a not trimmed string was desired. It was always a bug. \$\endgroup\$ – t3chb0t Nov 23 '18 at 16:30
  • 1
    \$\begingroup\$ @t3chb0t Yeah, I could see it being an intentional case, at which point I would have made the unilateral decision to say "you can't do that", I'd rather have the bug where whitespace is left as-is, than omit a potential feature. (As weird as that might sound, given the nature of what we're talking about.) \$\endgroup\$ – Der Kommissar Nov 23 '18 at 16:31
  • \$\begingroup\$ fine, I think one could simply derive another class from it and make it both ignore-case and trimmed... \$\endgroup\$ – t3chb0t Nov 23 '18 at 17:57
10
\$\begingroup\$

Review

I find this is a very nice idea that I have borrow from you and while doing this I'd change a couple things to make it more mature and even more flexible.


: IComparable, IEnumerable, IEnumerable<char>, IComparable<string>, IComparable<ValidatedString>, IEquatable<string>, IEquatable<ValidatedString>, IXmlSerializable

The base class implements a lot of interfaces which is great because it can be used in many scenarios. There are however some more of them that currently cannot be implemented. By that I mean ones that require the usage of the IEqualityComparer<T> or IComparer<T>. This means I would extract the implementations from this class and put them in two corresponding and separate comparers. Then I would reuse them with the base class to imlement the class' interfaces.


I would also unify the naming convention to SomethingString. Currently it's a battle between prefix vs suffix style. I don't know whether the NN style is a convention but I've never seen it before so I'd probably rename it to the full name.


The StringAlphaNum type should something like AlphanumericAsciiString becuase it won't work correctly with other cultures. For them using char.IsLetter and char.IsDigit could be more appropriate.


 public static implicit operator string(ValidatedString str) => str?.String;

This might be a source of weird bugs so I would definitely make it explicit because otherwise it's very easy to loose the validation when it invisibly gets converted into a string. To me it's like converting double to int. The ValidatedString is stripped off of its additional functionality so it should be an intentional act. Not something that happens somewhere automatically.


Alternative design

I'd like to suggest a different approach that makes it possible to combine powers of various validations. The following code is ony a model and a rough proof-of-concept so please don't be too harsh with it.

In this design there is only one base class with different generic overloads. I think we actually don't need more then two or three of them. I created only to for this example.

The T of each class is a simple interface that should be implemented by validations:

public interface IStringValidation
{
    bool IsValid(string value);
}

They can be used to pass them as arguments for method parameters:

void Main()
{
    //Do1(string.Empty); // boom! = NotNullOrWhitespaceException
    Do1("abc");
    //Do2("abc"); // boom! = MinLength10Exception
    Do2("1234567890");

    //Do3("1234567890X"); // boom! = HexException
    Do3("1234567890");
}

public static void Do1(SafeString<NotNullOrWhitespace> value)
{

}

public static void Do2(SafeString<NotNullOrWhitespace, MinLength10> value)
{

}

public static void Do3(SafeString<NotNullOrWhitespace, MinLength10, Hex> value)
{

}

And here's the actual very general and basic implementation of the first class:

public class SafeString<T>
    where T : IStringValidation, new()
{
    private readonly string _value;

    protected readonly IEnumerable<IStringValidation> _validations;

    private SafeString(string value)
    {
        _validations = new IStringValidation[] { new T() };
        _value = Validate(value);
    }

    protected SafeString(string value, params IStringValidation[] validations)
    {
        _validations = new IStringValidation[] { new T() }.Concat(validations);
        _value = Validate(value);
    }

    protected string Validate(string value)
    {
        return
            _validations.FirstOrDefault(v => !v.IsValid(value)) is var failed && failed is null
                ? value
                : throw DynamicException.Create(failed.GetType().Name, "Ooops!");
    }

    public static implicit operator SafeString<T>(string value) => new SafeString<T>(value);
}

and two more of these that extend it with further Ts and reuse the previous one:

public class SafeString<T1, T2> : SafeString<T1>
    where T1 : IStringValidation, new()
    where T2 : IStringValidation, new()
{
    private SafeString(string value) : base(value, new T2()) { }

    protected SafeString(string value, IStringValidation validation) : base(value, new T2(), validation) { }

    public static implicit operator SafeString<T1, T2>(string value) => new SafeString<T1, T2>(value);
}

public class SafeString<T1, T2, T3> : SafeString<T1, T2>
    where T1 : IStringValidation, new()
    where T2 : IStringValidation, new()
    where T3 : IStringValidation, new()
{
    private SafeString(string value) : base(value, new T3()) { }

    public static implicit operator SafeString<T1, T2, T3>(string value) => new SafeString<T1, T2, T3>(value);
}

I've created three example implementations that look like this:

public class NotNullOrWhitespace : IStringValidation
{
    public bool IsValid(string value) => !string.IsNullOrWhiteSpace(value);
}

public abstract class MinLengthValidation : IStringValidation
{
    private readonly int _minLength;

    protected MinLengthValidation(int minLength)
    {
        _minLength = minLength;
    }

    public bool IsValid(string value) => value.Length >= _minLength;
}

public class MinLength10 : MinLengthValidation
{
    public MinLength10() : base(10) { }
}

public abstract class RegexValidation : IStringValidation
{
    protected abstract string Pattern { get; }

    private readonly Lazy<Regex> _regex;

    protected RegexValidation()
    {
        _regex = Lazy.Create(() => new Regex(Pattern));
    }

    public bool IsValid(string value) => _regex.Value.IsMatch(value);
}

public class Hex : RegexValidation
{
    protected override string Pattern => "^[0-9a-fA-F]*$";
}

I find it's more flexible this way and the user can better see which validations are going to be made like here:

SafeString<NotNullOrWhitespace, MinLength10, Hex>

The string will be validated from left to right - in the same order as the generic parameters.

\$\endgroup\$
  • \$\begingroup\$ Not sure what you're getting at with the last point, can you expand on that? \$\endgroup\$ – Der Kommissar Nov 23 '18 at 20:13
  • \$\begingroup\$ @202_accepted me either ;-] I removed this point and added an alternative design instead. Sorry for the confusion. I'm not sure what I was thinking about. \$\endgroup\$ – t3chb0t Nov 23 '18 at 20:49
  • \$\begingroup\$ @HenrikHansen what do you think about my alternative design? ;-) \$\endgroup\$ – t3chb0t Nov 23 '18 at 20:50
  • 1
    \$\begingroup\$ @HenrikHansen nope, there is no particular reason for that. It's just a quick'n'dirty example for what is possible. There is a great deal of other features missing too that have to be implemented before it's production ready. I just wanted to to show a different point of view. It's intentionally not complete. \$\endgroup\$ – t3chb0t Nov 24 '18 at 8:05
  • 2
    \$\begingroup\$ Regarding implicit conversion to string: any ValidatedString is a valid string value, just like how any int value is a valid double value. I'd say that makes it similar to converting an int to a double, not the other way around. \$\endgroup\$ – Pieter Witvoet Nov 24 '18 at 11:47
10
\$\begingroup\$

I would call the actual String property Value instead of String, it will improve readability.


Maybe you want to mark it as serializable?


The String property should be immutable: public string Value { get; }


public IEnumerator<char> GetEnumerator() => ((IEnumerable<char>)String?.ToCharArray()).GetEnumerator();  // HH: Why ?.ToCharArray()

Why do you call ToCharArray()?

Why not just:

public IEnumerator<char> GetEnumerator() => String?.GetEnumerator(); 

protected override string ErrorRequirement => "contain only alphabetical (a-z) characters";

I'm not a fan of this ErrorRequirement. It is IMO only useful when debugging, and it's hard (read: impossible) to localize. A specialized Exception would be better (ex: InvalidEmailFormatException)


Here I'm just thinking loud:

Maybe I would not make the base class abstract and inject a validator interface and/or delegate into the constructor in a way like this:

  public interface IStringValidator
  {
    string Validate(string value);
  }

  public class ValidatedString
    : IEnumerable<char> /* etc. */
  {
    public ValidatedString(string value, IStringValidator validator)
    {
      Value = validator.Validate(value);
    }

    public ValidatedString(string value, Func<string, string> validator)
    {
      Value = validator(value);
    }

    public string Value { get; }
    public int Length => Value.Length;
    public char this[int index] => Value[index];

    public IEnumerator<char> GetEnumerator()
    {
      return Value?.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
      return GetEnumerator();
    }
  }

Notice that here it's the responsibility of the derived class to react on invalidity in the validator. It makes it possible for the derived class to make the string value valid before sending it back to base class or throw an exception (dedicated).

In the above, you're still free to derive from it, but also use it more freely in more rare places where a specialized subclass is overkill.

The danger with all these sub classes is that over time you forget about them and invent them once and again.


Example of subclass:

  public class EmailValidator : IStringValidator
  {
    public string Validate(string value)
    {

      if (!Regex.IsMatch(value, @"^.+@.+\..+$"))
        throw new ArgumentException("invalid email format");

      return value;
    }
  }


  public class EmailString : ValidatedString
  {
    public EmailString(string value) : base(value, new EmailValidator())
    {

    }

    public static implicit operator EmailString(string email)
    {
      return new EmailString(email);
    }
  }


  SendEmail("email@example.com");

  void SendEmail(EmailString email)
  {
    Console.WriteLine(email);
  }

Just another idea:

You could easily make a generic super class to ValidatedString:

  public abstract class ValidatedValue<TValue>
  {
    public ValidatedValue()
    {

    }

    public ValidatedValue(TValue value)
    {

    }

    protected abstract string ErrorRequirement { get; }
    protected Exception Exception => new ArgumentException($"The value must {ErrorRequirement}");

    private TValue Validate(TValue value) => IsValid(value) ? value : throw Exception;

    protected abstract bool IsValid(TValue value);

    public TValue Value { get; }
  }

And let ValidatedString inherit from that.

That would make it possible to create validated objects from every possible type like DateTime:

  public class HistoryTime : ValidatedValue<DateTime>
  {
    public HistoryTime(DateTime value) : base(value)
    {

    }

    protected override string ErrorRequirement => "be in the past";

    protected override bool IsValid(DateTime value)
    {
       return value < DateTime.Now;
    }
  }
\$\endgroup\$
  • 1
    \$\begingroup\$ I actually wanted to mark it Serializable, but I'm struggling to do that with .NET Standard 1.3. Looks like that's something Microsoft wants to move away from. \$\endgroup\$ – Der Kommissar Nov 23 '18 at 18:27
  • \$\begingroup\$ @202_accepted: It could well be. \$\endgroup\$ – Henrik Hansen Nov 23 '18 at 18:36
  • \$\begingroup\$ Making the base class non-abstract and letting it accept an interface would make it impossible to use it for implicit casting into this type when using as a parameter. \$\endgroup\$ – t3chb0t Nov 23 '18 at 19:10
  • \$\begingroup\$ @t3chb0t: I don't see your point. Implicit conversion to string works for method parameters, but that's maybe not, what you mean? \$\endgroup\$ – Henrik Hansen Nov 23 '18 at 19:17
  • 1
    \$\begingroup\$ I mean turning a string into a ValidatedString won't work, e.g: void SendEmail(EmailString email) and called like that x.SendEmail("abc@example.com") - you would not be able to implicitly cast the string so that it's automatically validated. This is where I see the main use case for it. \$\endgroup\$ – t3chb0t Nov 23 '18 at 19:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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