Search code examples
asp.net.netvb.netlambda.net-4.8

Expression of type 'System.Boolean' cannot be used for return type 'System.Object'


I want to create an Expression(Of Func(Of TModel, TResult)) from the TModel and the string property name.

I've tried it like this:

Error from Expression.Lambda(): Expression of type 'System.Boolean' cannot be used for return type 'System.Object'

<Extension>
Public Function ModelEditor(Of TModel)(html As HtmlHelper(Of TModel)) As MvcHtmlString
        Dim meta = html.ViewData.ModelMetadata

        Dim htmlString As New StringBuilder()

        Dim paramExp = Expression.Parameter(GetType(TModel), "model")

        For Each editor In meta.Properties
            Dim memberExp = Expression.Property(paramExp, editor.PropertyName)
            Dim exp = Expression.Lambda(Of Func(Of TModel, Object))(memberExp, paramExp)
            htmlString.Append(html.EditorFor(exp))
        Next

        Return MvcHtmlString.Create(htmlString.ToString())    
End Function

And then I've tried to convert the value:

Error from EditorFor(): Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions.

Dim memberExp = Expression.Property(paramExp, editor.PropertyName)

' Convert to a Object
Dim convertExp = Expression.Convert(memberExp, GetType(Object))

Dim exp = Expression.Lambda(Of Func(Of TModel, Object))(convertExp, paramExp)
htmlString.Append(html.EditorFor(exp))

When you look at the source code you can see that the ExpressionType can only be: ExpressionType.ArrayIndex, ExpressionType.Call, ExpressionType.MemberAccess or ExpressionType.Parameter

How can I do this without getting errors? Or am I taking the wrong approach?


Solution

  • I found a solution myself by using a lot of ugly reflection. There's probably still a way better way to do it though.

    Option Strict Off
    
    <Extension>
    Public Function ModelEditor(Of TModel)(html As HtmlHelper(Of TModel)) As MvcHtmlString
            Dim meta = html.ViewData.ModelMetadata
    
            Dim htmlString As New StringBuilder()
    
            Dim paramExp = Expression.Parameter(GetType(TModel), "model")
    
            For Each prop In meta.Properties
                ' Equivalent to: Function(model) model.{PropertyName}
                Dim memberExp = Expression.Property(paramExp, prop.PropertyName)
    
                ' Ugly unsafe reflection
                Try
                    ' Use reflection to get this: Func(Of TModel, TValue)
                    Dim delegateType = GetType(Func(Of ,)).MakeGenericType(GetType(TModel), prop.ModelType)
    
                    ' Use reflection to call this: Expression.Lambda(Of Func(Of TModel, Object))(memberExp, paramExp)
                    ' Result: Expression(Of Func( TModel, TValue))
                    Dim exp = GetType(Expression).
                        GetMethods(BindingFlags.Public Or BindingFlags.Static).
                        FirstOrDefault(Function(x) x.Name = "Lambda" AndAlso x.IsGenericMethod AndAlso x.GetParameters.Length = 2 AndAlso x.GetParameters.Select(Function(y) y.ParameterType).Contains(GetType(ParameterExpression()))).
                        MakeGenericMethod(delegateType).
                        Invoke(Nothing, New Object() {memberExp, New ParameterExpression() {paramExp}})
    
                    htmlString.Append(EditorExtensions.EditorFor(html, exp))
                Catch
                    Continue For
                End Try
            Next
    
            Return MvcHtmlString.Create(htmlString.ToString())
        End Function