0

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&)
star4z
  • 377
  • 3
  • 10
  • "No coercion operator is defined between types 'System.Func``2[DialogController,UnityEngine.GameObject]' and 'UnityEngine.GameObject'", because that's what you're asking for here; `Expression.Convert(expression, typeof(TProperty))`. Did you mean to invoke the method? – Jeremy Lakeman Aug 27 '21 at 00:53
  • @JeremyLakeman I was looking at [this](https://stackoverflow.com/questions/17647627/dynamic-linq-order-by-on-nested-property-with-null-properties/20393314#20393314) SO answer. If I don't include `Expression.Convert()` and just use `expression`, then I get an "Argument Types do not match" error. (See my edit.) My intention was to use `Expression.Convert()` to change the two Expressions to have the same type. – star4z Aug 27 '21 at 03:13
  • What you probably want is to reuse `expression.Body` & `expression.Parameters`. But control flow and return values are a bit weird (eg https://stackoverflow.com/questions/13687914/dynamic-linq-expression-with-return-value). – Jeremy Lakeman Aug 27 '21 at 04:30

1 Answers1

0

I didn't figure out how to accomplish this in a generic way using Expressions but I did figure out how to solve this specifically for GameObjects, 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).

star4z
  • 377
  • 3
  • 10