0

I'd want to refactor this code to get rid of relying on TModel, TValue

public static string DescriptionFor<TModel, TValue>(this IHtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
{
    // null checks

    DescriptionAttribute descriptionAttribute = null;
    if (expression.Body is MemberExpression memberExpression)
    {
        descriptionAttribute = memberExpression.Member
            .GetCustomAttributes(typeof(DescriptionAttribute), false)
            .Cast<DescriptionAttribute>()
            .SingleOrDefault();
    }

    return descriptionAttribute?.Description ?? string.Empty;
}

which has invocation like that:

@Html.DescriptionFor(x => x.MyModel.MyOtherModel.Property)

to something like that

public static string DescriptionFor2<T>(Expression<Func<T>> expression)
{
    if (expression == null)
        throw new ArgumentNullException(nameof(expression));

    if (!(expression.Body is MemberExpression memberExpression))
    {
        return string.Empty;
    }

    foreach (var property in typeof(T).GetProperties())
    {
        if (property == ((expression.Body as MemberExpression).Member as PropertyInfo))
        {
            var attr = property
                        .GetCustomAttributes(typeof(DescriptionAttribute), false)
                        .Cast<DescriptionAttribute>()
                        .FirstOrDefault();

            return attr?.Description ?? string.Empty;
        }
    }

    return string.Empty;       
}

with invocation like that:

@MyHtml.DescriptionFor2<MyOtherModel>(x => x.MyProperty);

But here's error:

Delegate 'Func' does not take 1 arguments

Joelty
  • 1,751
  • 5
  • 22
  • 64

1 Answers1

1

You can do this:

void Main()
{
    Console.WriteLine(MyHtml.DescriptionFor2<Test>((t) => t.StringMember));
    Console.WriteLine(MyHtml.DescriptionFor2<Test>((t) => t.BooleanMember));
    Console.WriteLine(MyHtml.DescriptionFor2<Test>((t) => t.IntegerMember));
}

public class Test
{
    [Description("This is a string member")]
    public string StringMember { get; set; }
    [Description("This is a boolean member")]
    public bool BooleanMember { get; set; }
    [Description("This is a integer member")]
    public int IntegerMember { get; set; }
}

public static class MyHtml
{
    public static string DescriptionFor2<T>(Expression<Func<T, dynamic>> expression)
    {
        var result = string.Empty;
        var member = GetMemberExpression(expression)?.Member?.Name;
        if (member != null)
        {
            var property = typeof(T).GetProperty(member);
            if (property != null)
            {
                var attr = property
                            .GetCustomAttributes(typeof(DescriptionAttribute), false)
                            .Cast<DescriptionAttribute>()
                            .FirstOrDefault();

                result = attr?.Description ?? string.Empty;
            }
        }

        return result;
    }

    private static MemberExpression GetMemberExpression<T>(Expression<Func<T, dynamic>> expression)
    {
        var member = expression.Body as MemberExpression;
        var unary = expression.Body as UnaryExpression;
        return member ?? (unary != null ? unary.Operand as MemberExpression : null);
    }
}

public class DescriptionAttribute : Attribute
{
    public string Description { get; set; }

    public DescriptionAttribute(string description)
    {
        Description = description;
    }
}

It involves getting the body of the expression as a member and using its name to resolve the property on the object. The dynamic data type is used to get rid of the second type parameter, however you can also define it explicitly for better compile-time error catching:

public static string DescriptionFor2<T, U>(Expression<Func<T, U>> expression)

and calling it:

MyHtml.DescriptionFor2<Test, string>((t) => t.StringMember);
MyHtml.DescriptionFor2<Test, bool>((t) => t.BooleanMember);
MyHtml.DescriptionFor2<Test, int>((t) => t.IntegerMember);

EDIT: edited answer because initial solution did not work for value types, see Expression for Type members results in different Expressions (MemberExpression, UnaryExpression)

Jeroen V
  • 308
  • 1
  • 6
  • I see you were already halfway there with your most recent edit... My answer still stands, the `Func` needs an input and an output type. – Jeroen V Mar 27 '19 at 14:37
  • I'm not sure why, but for ``string property`` it worked fine, but for ``bool`` part ``(expression.Body as MemberExpression);`` is null - using dynamic approach – Joelty Mar 27 '19 at 14:48
  • I found out that this does not work for value types, see https://stackoverflow.com/questions/12975373/expression-for-type-members-results-in-different-expressions-memberexpression for a solution. – Jeroen V Mar 27 '19 at 14:59