'UrlHelper.Action without hard-coded strings for controllerName and controllerAction

I'd like to create a convenient helper method which helps construct an URL from the given controller type and the action/method on that controller. In ASP.NET Core MVC there is a IUrlHelper.Action(string actionName, string controllerName) method that does the same thing.

But, instead of this: UrlHelper.Action("MyAction", "MyController")

I'd like to use: UrlHelper.Action<MyController>(c => c.MyAction)

Now, I've only partially managed to solve this problem, which allows me to do something like this: UrlHelper.Action<MyController>(c => c.MyAction()) (note the parenthesis after the action name) using the following code:

    public static string ActionFor<TController>(this IUrlHelper source, Expression<Func<TController, object>> memberExpression) where TController : Controller
    {
        if (!(memberExpression.Body is MemberExpression body)) throw new ArgumentException($"Expression must be a {typeof(MemberExpression).Name}.", nameof(memberExpression));

        return source.Action(body.Member.Name, typeof(TController).RemoveControllerSufix());
    }

but I don't know how to tell the compiler that I'd like that memberExpression parameter to accept a method group (or similar), so I could write the action name without parenthesis.

P.S. We can also do something like: UrlHelper.Action<MyController>(nameof(MyController.MyAction)) which also avoids hard-coding of strings, but still duplicates the controller type, that would be great to avoid, too.

I think I saw a post by Jon Skeet somewhere on SO, related to something similar, but I just can't find it now...



Solution 1:[1]

Getting the name of the method group is a bit fiddly. This is how it works (tested in dotnet6/C#10.0).

Call to get the name of the method group:

var name = GetMethodGroupName<Foo>(f => f.Bar);
// name: "Bar"

Method with pattern matching

private static string GetMethodGroupName<TController>(Expression<Func<TController, object>> memberExpression)
{
    if (memberExpression.Body is UnaryExpression
        { Operand: MethodCallExpression
            { Object: ConstantExpression
                { Value: MethodInfo me } } })
        return me.Name;

    throw new ArgumentException(
        "expression does not look like a method group.", 
        nameof(memberExpression));
}

... or alternatively with concatinaded checks ...

private static string GetMethodGroupName<TController>(Expression<Func<TController, object>> memberExpression)
{
    if (memberExpression.Body is UnaryExpression ue)
        if (ue.Operand is  MethodCallExpression mce)
            if (mce.Object is ConstantExpression ce)
                if (ce.Value is MethodInfo me)
                    return me.Name;

    throw new ArgumentException(
        "expression does not look like a method group.", 
        nameof(memberExpression));
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Sebastian