I'm trying to validate my Unity MonoBehavior
in order to make it more obvious when scripts aren't set up properly. I was aware of FluentValidation from other C# work I've done, so I set up Nuget for Unity and installed it.
The problem is that when a GameObject on a MonoBehavior is not initialized, the expression for FluentValidation throws an error instead of returning null due to Unity's behavior overrides.
I've attempted to fix this by creating an extension method which will catch any exceptions thrown in the expression and return null instead. I'm unfamiliar with building expressions using the Expression class, but I'm unsure what to do. Running this causes the following error to appear in the Editor Console:
InvalidOperationException: No coercion operator is defined between types 'System.Func`2[DialogController,UnityEngine.GameObject]' and 'UnityEngine.GameObject'.
System.Linq.Expressions.Expression.GetUserDefinedCoercionOrThrow (System.Linq.Expressions.ExpressionType coercionType, System.Linq.Expressions.Expression expression, System.Type convertToType) (at <351e49e2a5bf4fd6beabb458ce2255f3>:0)
System.Linq.Expressions.Expression.Convert (System.Linq.Expressions.Expression expression, System.Type type, System.Reflection.MethodInfo method) (at <351e49e2a5bf4fd6beabb458ce2255f3>:0)
System.Linq.Expressions.Expression.Convert (System.Linq.Expressions.Expression expression, System.Type type) (at <351e49e2a5bf4fd6beabb458ce2255f3>:0)
ValidationExtensions.SafeRuleFor[T,TProperty] (FluentValidation.AbstractValidator`1[T] validator, System.Linq.Expressions.Expression`1[TDelegate] expression) (at Assets/Scripts/ValidationExtensions.cs:12)
DialogControllerValidation.WhenAsync (System.Func`3[T1,T2,TResult] predicate, System.Action action) (at Assets/Scripts/DialogControllerValidation.cs:10)
DialogController.OnValidate () (at Assets/Scripts/DialogController.cs:170)
Here's my MonoBehavior:
public class DialogController: MonoBehavior {
public GameObject content;
private void OnValidate()
{
new DialogControllerValidation().ValidateAndThrow(this);
}
}
Here's my Validator class:
public class DialogControllerValidation : AbstractValidator<DialogController>
{
public DialogControllerValidation()
{
this.SafeRuleFor(x => x.content).NotNull();
}
}
Here's my extension method:
public static class ValidationExtensions
{
public static IRuleBuilderInitial<T, TProperty> SafeRuleFor<T, TProperty>(this AbstractValidator<T> validator,
Expression<Func<T, TProperty>> expression)
{
var tryExpression = Expression.TryCatch(
Expression.Block(typeof(TProperty), Expression.Convert(expression, typeof(TProperty))),
Expression.Catch(typeof(TProperty), Expression.Constant(null))
);
return validator.RuleFor(Expression.Lambda<Func<T, TProperty>>(
tryExpression,
Expression.Parameter(typeof(T), "t")
)
);
}
}
Edit: If I replace Expression.Convert(expression, typeof(TProperty))
with expression
, i.e.:
public static class ValidationExtensions
{
public static IRuleBuilderInitial<T, TProperty> SafeRuleFor<T, TProperty>(this AbstractValidator<T> validator,
Expression<Func<T, TProperty>> expression)
{
var tryExpression = Expression.TryCatch(
Expression.Block(typeof(TProperty), expression),
Expression.Catch(typeof(TProperty), Expression.Constant(null))
);
return validator.RuleFor(Expression.Lambda<Func<T, TProperty>>(
tryExpression,
Expression.Parameter(typeof(T), "t")
)
);
}
}
Then the error I get is
ArgumentException: Argument types do not match
System.Linq.Expressions.Expression.BlockCore (System.Type type, System.Collections.ObjectModel.ReadOnlyCollection`1[T] variables, System.Collections.ObjectModel.ReadOnlyCollection`1[T] expressions) (at <351e49e2a5bf4fd6beabb458ce2255f3>:0)
System.Linq.Expressions.Expression.Block (System.Type type, System.Collections.Generic.IEnumerable`1[T] variables, System.Collections.Generic.IEnumerable`1[T] expressions) (at <351e49e2a5bf4fd6beabb458ce2255f3>:0)
System.Linq.Expressions.Expression.Block (System.Type type, System.Collections.Generic.IEnumerable`1[T] expressions) (at <351e49e2a5bf4fd6beabb458ce2255f3>:0)
System.Linq.Expressions.Expression.Block (System.Type type, System.Linq.Expressions.Expression[] expressions) (at <351e49e2a5bf4fd6beabb458ce2255f3>:0)
ValidationExtensions.SafeRuleFor[T,TProperty] (FluentValidation.AbstractValidator`1[T] validator, System.Linq.Expressions.Expression`1[TDelegate] expression) (at Assets/Scripts/ValidationExtensions.cs:12)
DialogControllerValidation.WhenAsync (System.Func`3[T1,T2,TResult] predicate, System.Action action) (at Assets/Scripts/DialogControllerValidation.cs:10)
DialogController.OnValidate () (at Assets/Scripts/DialogController.cs:170)
UnityEngine.GUIUtility:ProcessEvent(Int32, IntPtr, Boolean&)
Edit:
I don't use FluentValidations in Unity anymore. I have my own classes that just throw on conditions for when I need them, but note that Unity has a built in RequireComponent Attribute which handle a good amount of cases.
Original:
I didn't figure out how to accomplish this in a generic way using Expression
s but I did figure out how to solve this specifically for GameObject
s, which were the cause of the problem I was trying to solve.
The general idea is that you can run a method in an Expression
as long as there is only one statement in the expression.
Here's the extension method I came up with:
public static class ValidationExtensions
{
public static IRuleBuilderInitial<T, GameObject> SafeRuleFor<T>(this AbstractValidator<T> validator,
Func<T, GameObject> func)
{
return validator.RuleFor(x => SafeWrap(func(x)));
}
private static GameObject SafeWrap(GameObject gameObject)
{
return gameObject ? gameObject : null;
}
}
If you wish to keep the input parameter as an expression, you can replace func(x)
with expression.Compile().Invoke(x)
.