I have been working on a side project for input filtering/Validation.
First I would like to go over my interface.
public interface IValidatable<T> : IValidatable
{
MyAction<T> ModifyInput { get; set; }
Predicate<T> PreCheck { get; set; }
bool Equals(object obj);
int GetHashCode();
}
public interface IValidatable
{
}
The action is used to filter or modify the input data, the predicate is used to check the input meets a bool condition, and standard overrides.
Next up is the implementation of the class. This class provides the backing field and proxy to input of that field. The private data field becomes the instance of the type you pass in with builder class function AddColumnToHeader.
The new thing here is the private function SetData that is ran on sets of the Data Property, which in turn gets a private field, and sets a private field based on configuration passed in, by the builder class function AddPredicateToColumn and AddHandlerToColumn.
public class Validatable<T> : IValidatable<T>
{
public Validatable()
{
}
private T data = default;
public T Data { get => data; set => data = SetData(value); }
public Predicate<T> PreCheck { get; set; } = default;
public MyAction<T> ModifyInput { get; set; } = default;
private T SetData(T _value)
{
if (ModifyInput == default)
{
if (PreCheck != default)
{
if (PreCheck(_value) == true)
{
return _value;
}
return default;
}
return _value;
}
else
{
ModifyInput(ref _value);
if (PreCheck != default)
{
if (PreCheck(_value) == true)
{
return _value;
}
return default;
}
return _value;
}
}
public override bool Equals(object other)
{
Validatable<T> column = other as Validatable<T>;
return column != null &&
EqualityComparer<T>.Default.Equals(Data, column.Data);
}
public override int GetHashCode()
{
return HashCode.Combine(Data);
}
public static bool operator ==(Validatable<T> column1, Validatable<T> column2)
{
return EqualityComparer<Validatable<T>>.Default.Equals(column1, column2);
}
public static bool operator !=(Validatable<T> column1, Validatable<T> column2)
{
return !(column1 == column2);
}
}
The next class is my builder class. This class builds out the Validatable objects that will be used to input validate the underlying object. It does this using the builder pattern.
The pattern I used for this follows. AddColumnToHeader<T>
returns the this, so you can chain call it until the number of types fits your needs, and in the last call we have to set true
to the 2nd parameter _typeCompleted, to signal the builder can move on to adding predicates and handlers.I do have a need for it to be split up in this way. I want GetTypedRow to return a deep clone of the HeaderContainer object you have defined, enforcing a immutable type on to it, and returning it.. You could remove 2nd parameter from AddColumnToHeader<T>
and create and add dynamically, without issue I suppose.
public delegate void MyAction<T>(ref T Parameter);
public sealed class ValidatableBuilder
{
private Validatable<T> HeaderColumnFactory<T>() => new Validatable<T>();
public IQueryable<(string Name, IValidatable Value, Type ColumnType)> QueryableHeader { get => HeaderContainer.AsQueryable(); }
private List<(string Name, IValidatable Value, Type ColumnType)> HeaderContainer { get; set; } = default;
private bool TypesCompleted = false;
private bool SetupCompleted = false;
public ValidatableBuilder()
{
HeaderContainer = new List<(string Name, IValidatable Value, Type ColumnType)>();
}
public ValidatableBuilder AddColumnToHeader<T>(string _columnName, bool _typesComplete = false)
{
if (!TypesCompleted)
{
if (typeof(T).IsValueType)
{
if (!HeaderContainer.Exists(x => x.Name == _columnName))
{
HeaderContainer.Add((_columnName, HeaderColumnFactory<T>(), typeof(T)));
if (_typesComplete)
{
TypesCompleted = true;
}
}
}
}
return this;
}
public ValidatableBuilder AddPredicateToColumn<T>(string _columnName, Predicate<T> _predicate)
{
if(TypesCompleted)
{
Validatable<T> validatable = QueryableHeader.FirstOrDefault(x => x.Name == _columnName).Value as Validatable<T>;
validatable.PreCheck = _predicate;
}
return this;
}
public ValidatableBuilder AddHandlerToColumn<T>(string _columnName, MyAction<T> _action)
{
if (TypesCompleted)
{
Validatable<T> validatable = QueryableHeader.FirstOrDefault(x => x.Name == _columnName).Value as Validatable<T>;
validatable.ModifyInput = _action;
}
return this;
}
private void SetupComplete()
{
if (HeaderContainer.Count > 0)
{
if (TypesCompleted)
{
SetupCompleted = true;
}
}
}
public IQueryable<(string Name, IValidatable Value, Type DataType)> GetTypedRow()
{
if (!SetupCompleted)
{
SetupComplete();
if (SetupCompleted)
{
return QueryableHeader;
}
return default;
}
else
{
return QueryableHeader;
}
}
}
Testing class, is not much in the way of finished
, but it shows usage of the code and that is important.
First up, I create a ValidatableBuilder
then I use it to add 2 columns of type ReadonlyMemory and int. I then add a condition to the int setter to say this value must be 106 to be set, and for the ReadOnlyMemory setter I tell it length must be in range 0(exclusive) to 11(inclusive). I then iterate over the Queryable I return from GetTypedRow and try to set the Property Data
to incorrect values and assert against those values.
public class ValidationTests
{
public ValidationTests()
{
ValidatableBuilder builder = new ValidatableBuilder();
void PredicateTests()
{
builder
.AddColumnToHeader<ReadOnlyMemory<byte>>("Test")
.AddColumnToHeader<int>("Test2", _typesComplete: true)
.AddPredicateToColumn<int>("Test2", x => x == 106)
.AddPredicateToColumn<ReadOnlyMemory<byte>>("Test", x => x.Length > 0 & x.Length <= 11);
foreach( var (Name, Value, DataType) in builder.GetTypedRow())
{
if(Value is IValidatable<ReadOnlyMemory<byte>>)
{
var ourValue = Value as Validatable<ReadOnlyMemory<byte>>;
ourValue.Data = new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes("Hello World!"));
Debug.Assert(ourValue.Data.Span.SequenceEqual(Encoding.UTF8.GetBytes("Hello World!")), "Not Equal");
}
else if(Value is IValidatable<int>)
{
var ourValue = Value as Validatable<int>;
ourValue.Data = 105;
Debug.Assert(ourValue.Data == 105, "Not Equal");
}
}
}
PredicateTests();
}
}
I tried to incorporate everything, but if I left something out or something is unclear, please let me know. All input is welcomed. :)
Edit: In response to Pieter's questions, I got further insight into where I could improve and the goals and completeness of my tool. I have decided to rename it ProgrammableSetter and instead of working directly towards validation before setting a property, I will change direction and work towards making it as Configurable and reusable as possible, it will probably take me a few days to retool/rewrite. Thanks for the valued input.
1 Answer 1
IValidatable<T>
ModifyInput
andPreCheck
are problematic: their public setters allow any code to modify or disable sanitizing and validation. Surely that's something that only the initializing code should be able to do? I also don't see why these properties need to be exposed at all: they're implementation details.- Why does
IValidatable<T>
contain anEquals
andGetHashCode
method? They're not used anywhere. - Because of the above two issues,
IValidatable<T>
is rather pointless in its current shape. Why doesn't it contain aT Data { get; set; }
property? And if you're only ever going to have one implementation then I don't see much use for an interface.Validatable<T>
seems flexible enough.
Validatable<T>
- You don't need to initialize properties with
= default
- that already happens by default. - I'd expect
Validatable<T>
's constructor to acceptmodifyInput
andpreCheck
as arguments, and to store them in private fields. - Why does
ModifyInput
take aref
parameter instead of returning the result? This prevents you from using lambda's. SetData
is a confusing name: it doesn't actually setData
- it returns a sanitized and validated value.- Most people would expect a setter to store the value given to it. Getting a different value back would be rather surprising - and not in a good way. Consider using a clearly named method instead:
v.Value = 105
versusv.SanitizeAndStore(105)
. - Returning
default
when validation fails is problematic: is0
an intended, valid value, or the result of a validation failure? I'd expect an exception to be thrown instead, or some other failure-handling mechanism. I would also clearly document the chosen behavior. SetData
can be simplified to justModifyInput?.Invoke(ref _value); return (PreCheck?.Invoke(_value) == false) ? default : _value;
.- A boolean is already either true or false, so the
== true
in your code is superfluous. Note that?.
returns a nullable bool, which is why the== false
part above is necessary. - In
Equals
, instead of usingas
and a null-check, you can use pattern matching instead:return other is Validatable<T> column && ...;
.
ValidatableBuilder
- Why do
QueryableHeader
andGetTypesRow
return anIQueryable<>
? It's an in-memory list, soIEnumerable<>
would make more sense. GetTypedRow
always returns the same row, including data that was set previously. I can't imagine that to be the intended behavior.Property { get => ...; }
can be simplified toProperty => ...;
.- Why explicitly initialize
HeaderContainer
todefault
, only to override that in the constructor? Just initialize it to a new list directly. - The various
Add
methods are very brittle: they fail silently depending on whetherTypesCompleted
is true or not. If a method cannot be called when an object is in a certain state, then I'd expect an exception to be thrown, but I'd much rather look for a better design. Perhaps aCreateRowBuilder
method that returns aRowBuilder
with the current configuration, which can then be used to create rows. - Likewise, silently ignoring reference types is brittle. Use a generic type constraint instead, so you can prevent the use of reference types at compile-time.
- Why have separate methods for adding columns, handlers and predicates? Why not add optional arguments for handlers and predicates to
AddColumnToHeader
?
ValidationTests
- Inside that foreach loop, you're using both
is
andas
, but with different types. AnIValidatable<T>
is not necessarily aValidatable<T>
- it could be any other type that implementsIValidatable<T>
. Note also that pattern matching can be used here:switch (Value) { case Validatable<ReadOnlyMemory<byte>> vmem: vmem.Data = ...; case Validatable<int> vint: vint.Data = ...; }
.
Other notes
- Leading underscores are typically used for private fields. Parameter names are written in camelCase, without leading underscore. PascalCase is used for property names, not fields.
- I prefer to add a bit of whitespace between methods and properties, especially when they have different visibilities (public/private).
- There's a general lack of documentation/comments.
Alternative approach
A property that silently modifies or rejects data is quite surprising. I'd rather see code that is more obvious about what it does:
var sanitizedValue = column.Sanitize(inputValue);
if (!column.IsValid(sanitizedValue))
{
// log, throw error, ...
}
else
{
// store value
}
-
\$\begingroup\$ This is excellent stuff, thank you so much for spending the time. I will take all that you say and try and address each point as best as I can. \$\endgroup\$BanMe– BanMe2019年05月13日 15:07:08 +00:00Commented May 13, 2019 at 15:07
-
\$\begingroup\$ ModifyInput and PreCheck are problematic: Many are in agreement on this point. I have moved the function set to the contstructor for my class. I now have 4 optional parameters to address this issue. Why does IValidatable<T> contain an Equals and GetHashCode method? Not sure where I was going, removed from next Implementation. I was just updating the Interface definitions now. new one will have
T Data
andT Set(T Value)
defined, this gives me the option to setup the validation and not use the inner field to receive the value \$\endgroup\$BanMe– BanMe2019年05月13日 15:11:48 +00:00Commented May 13, 2019 at 15:11 -
\$\begingroup\$ Validatable<T>: 1st point: Interesting I did not know that about C#, this can definitely reduce my typing. 2nd point: Addressed last comment, 3rd: ModifyInput has been adjusted to not take a reference but to return the modified value to a local variable for future use.4th I definitely agree it is misleading, I have a hard time naming things, I will try to Describe functions by their Impl better.5th this makes a good point, SanitizeOrSetAndStore? 6th: this should be addressed by the new Success/Fail Callbacks in my next version. \$\endgroup\$BanMe– BanMe2019年05月13日 16:00:33 +00:00Commented May 13, 2019 at 16:00
ModifyInput
: why can't a caller do that before callingSetData
? And how are you going to detect validation failures ifPreCheck
failures are not reported? \$\endgroup\$