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.
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\$