0

In a Blazor component, I have the following parameter. For is used to get some custom data annotations via reflection, e.g. to display an Asterisk * if a property is required and extract the label names based on data annotations

[Parameter]
public Expression<Func<T?>>? For { get; set; }

public Expression<Func<DateTime?>>? ForDateTime
{
    get
    {
        if (typeof(T) == typeof(DateTime))
        {
            //return <Func<DateTime?>>? here
        }
        else if (typeof(T) == typeof(DateTimeOffset))
        {
            //return <Func<DateTime?>>? here
        }
        else
        {
            return null;
        }
    }
    set
    {
        //For = set value of Expression<Func<T?>>? here
    }
}

How can I cast this to Expression<Func<DateTime?>>? ForDateTime { get; set; } and back if T is of Type DateTime or DateTimeOffset.

The reason for that is I am using MudBlazor and want to call

<MudDatePicker For="@(ForDateTime)" Variant="Variant.Outlined"/>

But there an Expression<Func<DateTime?>>? is needed.

For now I created a ExpressionVisitor like that:

public class TypeConverterVisitor<T, TU> : ExpressionVisitor
{
    protected override Expression VisitParameter(ParameterExpression node)
    {
        if (node.Type == typeof(T?) || node.Type == typeof(T))
        {
            return Expression.Parameter(typeof(TU?));
        }

        return base.VisitParameter(node);
    }
}

which I use like that:

var visitor = new TypeConverterVisitor<T, DateTime?>();
var expression = (Expression<Func<DateTime?>>)visitor.Visit(For);
return expression;

But this only works if my For is actually a (nullable) DateTime? already. I want it to be able to cast (non nullable) DateTime and DateTimeOffset as well. But I am not sure where I can handle this conversion. Especially as a breakpoint in the VisitParameter method is never hit.

Jim G.
  • 15,141
  • 22
  • 103
  • 166
Patrick
  • 173
  • 9
  • The answer by @Guru below gives you a way to do this, but I wonder if you could remove the `ForDateTime` property altogether and just use a generic version. For example: `public Expression>? ForType() => (Expression>)(object)For;` and then use it like this: ` – DavidG Aug 11 '23 at 10:29
  • What the question asks is how Blazor data-binding already works. You don't need `For` to bind a component property to a value, or cascade a parameter to a nested component. One option is to have the nested component bind to a component property that's bound in the page's model. The other is to use cascading parameters so you don't have to specify any intermediate properties – Panagiotis Kanavos Aug 11 '23 at 11:08
  • @PanagiotisKanavos I use `For` to get some custom data annotations via reflection, e.g. to display an Asterisk `*` if a property is required and extract the label names based on data annotations. – Patrick Aug 11 '23 at 11:12
  • `() => (DateTime?)value`? – Pieterjan Aug 11 '23 at 11:29
  • @Pieterjan This does not help as I need to use the expression directly. When using a parameter in between reflection gives me the value of that parameter but nor from the original property. – Patrick Aug 11 '23 at 15:14
  • Unrelated, but you don't need the `else`. Just `return null`. – Anderson Pimentel Aug 11 '23 at 16:55

1 Answers1

0

You can try using object indirection, but only if the T is constrained to struct (due to specifics of how T? is handled with introduction of nullable reference types - for example see Nullability and generics in .NET 6):

class Test<T> where T : struct
{
    public Expression<Func<T?>>? For { get; set; }

    public Expression<Func<DateTime?>>? ForDateTime
    {
        get
        {
            if (typeof(T) == typeof(DateTime))
            {
                return (Expression<Func<DateTime?>>)(object)For;
            }
            // ...
            else
            {
                return null;
            }
        }
        set
        {
            //For = set value of Expression<Func<T?>>? here
        }
    }
}

UPD:

If you can't constraint the class then you can rebuild expression tree. Something to get you started:

class Test<T>
{
    public Expression<Func<T?>>? For { get; set; }

    public Expression<Func<DateTime?>>? ForDateTime
    {
        get
        {
            if (typeof(T) == typeof(DateTime))
            {
                if (For is not null)
                {
                    var newBody = Expression.Convert(For.Body, typeof(DateTime?));
                    return Expression.Lambda<Func<DateTime?>>(newBody);
                }
                return null;
            }
            if (typeof(T) == typeof(DateTime?))
            {
                
                return  (Expression<Func<DateTime?>>)(object)For;
            }
            // ...
            else
            {
                return null;
            }
        }
        set
        {
            //For = set value of Expression<Func<T?>>? here
        }
    }
}
var forDateTime = new Test<DateTime>
    {
        For = () => DateTime.Now
    }
    .ForDateTime
    .Compile()();

var forDateTimeNull = new Test<DateTime?>
    {
        For = () => null
    }
    .ForDateTime
    .Compile()();
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
  • While this will work, there's no reason for such code in the first place. Blazor data binding works without requiring `Expression` properties. I suspect the OP is trying to rebuild data binding – Panagiotis Kanavos Aug 11 '23 at 10:55
  • Unfortunately I can not constrain it so struct – Patrick Aug 11 '23 at 14:54
  • @Patrick then you will need to reassemble the expression tree and use some conversions during the process, because without constraint for `Test` `T?` will also be `DateTime`., i.e. stored `For` would actually be `Expression>` and not `Expression>` – Guru Stron Aug 11 '23 at 16:38
  • @GuruStron I just tried that using a custom `TypeConverterVisitor` class (added to my question). But as I am pretty new to Expressions etc. I am stuck now. – Patrick Aug 11 '23 at 16:53
  • @Patrick `Func` does not have parameters =)) – Guru Stron Aug 11 '23 at 17:11
  • @Patrick also see the update. – Guru Stron Aug 11 '23 at 17:18
  • As `Expression.Convert` returns a `UnaryExpression` which `MudBlazor` cannot handle, I tried to convert it to a `MemberExpression` using `if (unaryExpression.Operand is MemberExpression newBody) return Expression.Lambda>(newBody);` But then I get the same issue again: `Expression of type 'System.DateTime' cannot be used for return type 'System.Nullable\`1[System.DateTime]'` – Patrick Aug 12 '23 at 11:05