Enforcing string validity with the C# type system
up vote
15
down vote
favorite
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
c# strings .net validation type-safety
|
show 1 more comment
up vote
15
down vote
favorite
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
c# strings .net validation type-safety
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 ;-)
– t3chb0t
Nov 23 at 16:22
2
@t3chb0t I actually specifically decided not to implementtrim
because that can affect the validation. Instead, you would want towhatever.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.
– 202_accepted
Nov 23 at 16:26
2
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.
– t3chb0t
Nov 23 at 16:30
1
@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.)
– 202_accepted
Nov 23 at 16:31
fine, I think one could simply derive another class from it and make it both ignore-case and trimmed...
– t3chb0t
Nov 23 at 17:57
|
show 1 more comment
up vote
15
down vote
favorite
up vote
15
down vote
favorite
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
c# strings .net validation type-safety
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
c# strings .net validation type-safety
c# strings .net validation type-safety
edited Nov 23 at 16:47
asked Nov 23 at 16:12
202_accepted
15.4k250132
15.4k250132
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 ;-)
– t3chb0t
Nov 23 at 16:22
2
@t3chb0t I actually specifically decided not to implementtrim
because that can affect the validation. Instead, you would want towhatever.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.
– 202_accepted
Nov 23 at 16:26
2
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.
– t3chb0t
Nov 23 at 16:30
1
@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.)
– 202_accepted
Nov 23 at 16:31
fine, I think one could simply derive another class from it and make it both ignore-case and trimmed...
– t3chb0t
Nov 23 at 17:57
|
show 1 more comment
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 ;-)
– t3chb0t
Nov 23 at 16:22
2
@t3chb0t I actually specifically decided not to implementtrim
because that can affect the validation. Instead, you would want towhatever.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.
– 202_accepted
Nov 23 at 16:26
2
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.
– t3chb0t
Nov 23 at 16:30
1
@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.)
– 202_accepted
Nov 23 at 16:31
fine, I think one could simply derive another class from it and make it both ignore-case and trimmed...
– t3chb0t
Nov 23 at 17:57
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 ;-)
– t3chb0t
Nov 23 at 16:22
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 ;-)
– t3chb0t
Nov 23 at 16:22
2
2
@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.– 202_accepted
Nov 23 at 16:26
@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.– 202_accepted
Nov 23 at 16:26
2
2
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.
– t3chb0t
Nov 23 at 16:30
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.
– t3chb0t
Nov 23 at 16:30
1
1
@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.)
– 202_accepted
Nov 23 at 16:31
@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.)
– 202_accepted
Nov 23 at 16:31
fine, I think one could simply derive another class from it and make it both ignore-case and trimmed...
– t3chb0t
Nov 23 at 17:57
fine, I think one could simply derive another class from it and make it both ignore-case and trimmed...
– t3chb0t
Nov 23 at 17:57
|
show 1 more comment
2 Answers
2
active
oldest
votes
up vote
9
down vote
accepted
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 T
s 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.
Not sure what you're getting at with the last point, can you expand on that?
– 202_accepted
Nov 23 at 20:13
@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.
– t3chb0t
Nov 23 at 20:49
@HenrikHansen what do you think about my alternative design? ;-)
– t3chb0t
Nov 23 at 20:50
1
@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.
– t3chb0t
Nov 24 at 8:05
2
Regarding implicit conversion tostring
: anyValidatedString
is a validstring
value, just like how anyint
value is a validdouble
value. I'd say that makes it similar to converting anint
to adouble
, not the other way around.
– Pieter Witvoet
Nov 24 at 11:47
|
show 3 more comments
up vote
10
down vote
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;
}
}
1
I actually wanted to mark itSerializable
, but I'm struggling to do that with .NET Standard 1.3. Looks like that's something Microsoft wants to move away from.
– 202_accepted
Nov 23 at 18:27
@202_accepted: It could well be.
– Henrik Hansen
Nov 23 at 18:36
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.
– t3chb0t
Nov 23 at 19:10
@t3chb0t: I don't see your point. Implicit conversion to string works for method parameters, but that's maybe not, what you mean?
– Henrik Hansen
Nov 23 at 19:17
1
I mean turning astring
into aValidatedString
won't work, e.g:void SendEmail(EmailString email)
and called like thatx.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.
– t3chb0t
Nov 23 at 19:20
|
show 5 more comments
2 Answers
2
active
oldest
votes
2 Answers
2
active
oldest
votes
active
oldest
votes
active
oldest
votes
up vote
9
down vote
accepted
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 T
s 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.
Not sure what you're getting at with the last point, can you expand on that?
– 202_accepted
Nov 23 at 20:13
@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.
– t3chb0t
Nov 23 at 20:49
@HenrikHansen what do you think about my alternative design? ;-)
– t3chb0t
Nov 23 at 20:50
1
@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.
– t3chb0t
Nov 24 at 8:05
2
Regarding implicit conversion tostring
: anyValidatedString
is a validstring
value, just like how anyint
value is a validdouble
value. I'd say that makes it similar to converting anint
to adouble
, not the other way around.
– Pieter Witvoet
Nov 24 at 11:47
|
show 3 more comments
up vote
9
down vote
accepted
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 T
s 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.
Not sure what you're getting at with the last point, can you expand on that?
– 202_accepted
Nov 23 at 20:13
@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.
– t3chb0t
Nov 23 at 20:49
@HenrikHansen what do you think about my alternative design? ;-)
– t3chb0t
Nov 23 at 20:50
1
@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.
– t3chb0t
Nov 24 at 8:05
2
Regarding implicit conversion tostring
: anyValidatedString
is a validstring
value, just like how anyint
value is a validdouble
value. I'd say that makes it similar to converting anint
to adouble
, not the other way around.
– Pieter Witvoet
Nov 24 at 11:47
|
show 3 more comments
up vote
9
down vote
accepted
up vote
9
down vote
accepted
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 T
s 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.
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 T
s 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.
edited Nov 24 at 6:48
Henrik Hansen
6,6931824
6,6931824
answered Nov 23 at 18:19
t3chb0t
33.7k746110
33.7k746110
Not sure what you're getting at with the last point, can you expand on that?
– 202_accepted
Nov 23 at 20:13
@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.
– t3chb0t
Nov 23 at 20:49
@HenrikHansen what do you think about my alternative design? ;-)
– t3chb0t
Nov 23 at 20:50
1
@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.
– t3chb0t
Nov 24 at 8:05
2
Regarding implicit conversion tostring
: anyValidatedString
is a validstring
value, just like how anyint
value is a validdouble
value. I'd say that makes it similar to converting anint
to adouble
, not the other way around.
– Pieter Witvoet
Nov 24 at 11:47
|
show 3 more comments
Not sure what you're getting at with the last point, can you expand on that?
– 202_accepted
Nov 23 at 20:13
@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.
– t3chb0t
Nov 23 at 20:49
@HenrikHansen what do you think about my alternative design? ;-)
– t3chb0t
Nov 23 at 20:50
1
@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.
– t3chb0t
Nov 24 at 8:05
2
Regarding implicit conversion tostring
: anyValidatedString
is a validstring
value, just like how anyint
value is a validdouble
value. I'd say that makes it similar to converting anint
to adouble
, not the other way around.
– Pieter Witvoet
Nov 24 at 11:47
Not sure what you're getting at with the last point, can you expand on that?
– 202_accepted
Nov 23 at 20:13
Not sure what you're getting at with the last point, can you expand on that?
– 202_accepted
Nov 23 at 20:13
@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.
– t3chb0t
Nov 23 at 20:49
@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.
– t3chb0t
Nov 23 at 20:49
@HenrikHansen what do you think about my alternative design? ;-)
– t3chb0t
Nov 23 at 20:50
@HenrikHansen what do you think about my alternative design? ;-)
– t3chb0t
Nov 23 at 20:50
1
1
@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.
– t3chb0t
Nov 24 at 8:05
@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.
– t3chb0t
Nov 24 at 8:05
2
2
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.– Pieter Witvoet
Nov 24 at 11:47
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.– Pieter Witvoet
Nov 24 at 11:47
|
show 3 more comments
up vote
10
down vote
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;
}
}
1
I actually wanted to mark itSerializable
, but I'm struggling to do that with .NET Standard 1.3. Looks like that's something Microsoft wants to move away from.
– 202_accepted
Nov 23 at 18:27
@202_accepted: It could well be.
– Henrik Hansen
Nov 23 at 18:36
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.
– t3chb0t
Nov 23 at 19:10
@t3chb0t: I don't see your point. Implicit conversion to string works for method parameters, but that's maybe not, what you mean?
– Henrik Hansen
Nov 23 at 19:17
1
I mean turning astring
into aValidatedString
won't work, e.g:void SendEmail(EmailString email)
and called like thatx.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.
– t3chb0t
Nov 23 at 19:20
|
show 5 more comments
up vote
10
down vote
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;
}
}
1
I actually wanted to mark itSerializable
, but I'm struggling to do that with .NET Standard 1.3. Looks like that's something Microsoft wants to move away from.
– 202_accepted
Nov 23 at 18:27
@202_accepted: It could well be.
– Henrik Hansen
Nov 23 at 18:36
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.
– t3chb0t
Nov 23 at 19:10
@t3chb0t: I don't see your point. Implicit conversion to string works for method parameters, but that's maybe not, what you mean?
– Henrik Hansen
Nov 23 at 19:17
1
I mean turning astring
into aValidatedString
won't work, e.g:void SendEmail(EmailString email)
and called like thatx.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.
– t3chb0t
Nov 23 at 19:20
|
show 5 more comments
up vote
10
down vote
up vote
10
down vote
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;
}
}
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;
}
}
edited Nov 24 at 9:39
answered Nov 23 at 18:26
Henrik Hansen
6,6931824
6,6931824
1
I actually wanted to mark itSerializable
, but I'm struggling to do that with .NET Standard 1.3. Looks like that's something Microsoft wants to move away from.
– 202_accepted
Nov 23 at 18:27
@202_accepted: It could well be.
– Henrik Hansen
Nov 23 at 18:36
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.
– t3chb0t
Nov 23 at 19:10
@t3chb0t: I don't see your point. Implicit conversion to string works for method parameters, but that's maybe not, what you mean?
– Henrik Hansen
Nov 23 at 19:17
1
I mean turning astring
into aValidatedString
won't work, e.g:void SendEmail(EmailString email)
and called like thatx.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.
– t3chb0t
Nov 23 at 19:20
|
show 5 more comments
1
I actually wanted to mark itSerializable
, but I'm struggling to do that with .NET Standard 1.3. Looks like that's something Microsoft wants to move away from.
– 202_accepted
Nov 23 at 18:27
@202_accepted: It could well be.
– Henrik Hansen
Nov 23 at 18:36
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.
– t3chb0t
Nov 23 at 19:10
@t3chb0t: I don't see your point. Implicit conversion to string works for method parameters, but that's maybe not, what you mean?
– Henrik Hansen
Nov 23 at 19:17
1
I mean turning astring
into aValidatedString
won't work, e.g:void SendEmail(EmailString email)
and called like thatx.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.
– t3chb0t
Nov 23 at 19:20
1
1
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.– 202_accepted
Nov 23 at 18:27
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.– 202_accepted
Nov 23 at 18:27
@202_accepted: It could well be.
– Henrik Hansen
Nov 23 at 18:36
@202_accepted: It could well be.
– Henrik Hansen
Nov 23 at 18:36
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.
– t3chb0t
Nov 23 at 19:10
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.
– t3chb0t
Nov 23 at 19:10
@t3chb0t: I don't see your point. Implicit conversion to string works for method parameters, but that's maybe not, what you mean?
– Henrik Hansen
Nov 23 at 19:17
@t3chb0t: I don't see your point. Implicit conversion to string works for method parameters, but that's maybe not, what you mean?
– Henrik Hansen
Nov 23 at 19:17
1
1
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.– t3chb0t
Nov 23 at 19:20
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.– t3chb0t
Nov 23 at 19:20
|
show 5 more comments
Thanks for contributing an answer to Code Review Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
Use MathJax to format equations. MathJax reference.
To learn more, see our tips on writing great answers.
Some of your past answers have not been well-received, and you're in danger of being blocked from answering.
Please pay close attention to the following guidance:
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f208291%2fenforcing-string-validity-with-the-c-type-system%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
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 ;-)
– t3chb0t
Nov 23 at 16:22
2
@t3chb0t I actually specifically decided not to implement
trim
because that can affect the validation. Instead, you would want towhatever.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.– 202_accepted
Nov 23 at 16:26
2
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.
– t3chb0t
Nov 23 at 16:30
1
@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.)
– 202_accepted
Nov 23 at 16:31
fine, I think one could simply derive another class from it and make it both ignore-case and trimmed...
– t3chb0t
Nov 23 at 17:57