0

The nameof construct is a really great feature of C# 6.0, especially in ASP.NET Core: It avoids the usage of hard coded strings, like for action names. Instead we refer to a class/method, and get compining errors, if their naming changes.

Example:

public async IActionResult Home() {
    return RedirectToAction(nameof(PageController.Index), GetControllerName(nameof(PageController)), new { Area = KnownAreas.Content });
 }

Compared to the old version without nameof

public async IActionResult Home() {
    return RedirectToAction("Index", "Page", new { Area = KnownAreas.Content });
}

As this is great, it blow up the code: For example I had to define a base controller class with GetControllerName. This method removes the Controller prefix, cause the controller is named Page, not PageController (the last case would result in a 404 because we double the Controller suffix.

Since this is a common use case and I want to keep the code as clean as possible, I would like to reduce that call to something like this:

public async IActionResult Home() {
    return RedirectToActionClean(PageController.Index, PageController, new { Area = KnownAreas.Content });
 }

Or even the following (which seems not possible)

public async IActionResult Home() {
    return RedirectToActionClean(PageController.Index, new { Area = KnownAreas.Content });
 }

Internally, I want to use nameof. So I pass PageController.Index to my method, and I internally have this value as string. But this seems difficult, since nameof seems to be a language construct, which can't be set as a type like generics.

To make it clear, lets have a look at my RedirectToActionClean header:

void RedirectToActionClean(??????) {

}

The question is: What type can be used for the qestion marks, that I can pass any type without instance like on nameof?

I think this is not possible since nameof seems to be a language construct and not a type. But maybe I understood something wrong and there is a way to do this. I'm using ASP.NET Core 1.1 on the latest Visual Studio 2017 with C# 7.

svick
  • 236,525
  • 50
  • 385
  • 514
Lion
  • 16,606
  • 23
  • 86
  • 148
  • `typeof(Class).Name`??? – Samvel Petrosov May 25 '17 at 17:50
  • I already tried this approach, the problem is: I need to pass `Class` as parameter, which seems only possible as an object like `new PageController().Index`. Wasting resources by creating controller instances only for link generation seems not a good idea. That's the reason I like `nameof` since `nameof(PageController.Index)` is possible without having a Instance of `PageController`. We've the same issue with the Controller. The only workaround seems not to use a wrapper like in my first example, but that would blow up the code. – Lion May 25 '17 at 17:57
  • I don't think there is any other language construct in C# that allows you to use directly the declaration of an instance method. One alternative would be to declare use expressions, something like: `RedirectToAction(c => c.Index())` but it'll be a real pain to use if the controller action expects parameters – Kevin Gosse May 25 '17 at 18:06
  • What's wrong with `RedirectToActionClean(nameof(PageController.Index), ...)`? – Paulo Morgado May 25 '17 at 22:57
  • Its long and redudant. Given the fact that a controller's name doesn't include the `Controller` suffix I can't even simply use `nameof(PageController)`. So I need some helper function that converts `PageController` to `Page`. The hole call could be shortened to `RedirectToActionClean(x => x.Index(), KnownArea.Content)` which reduces errors and also improves coding speed/readability. – Lion May 26 '17 at 06:34

1 Answers1

1

I've used expressions in the past with good results:

//within a controller or base controller
private void SetRouteValues(string action, string controller, RouteValueDictionary routeValues)
{
    if (routeValues != null)
    {
        foreach (var key in routeValues.Keys)
        {
            RouteData.Values[key] = routeValues[key];
        }
    }

    RouteData.Values["action"] = action;
    RouteData.Values["controller"] = controller;
}

protected RedirectToRouteResult RedirectToAction<TController>(Expression<Func<TController, object>> actionExpression) where TController : Controller
{
    var controllerName = typeof(TController).GetControllerName();
    var actionName = actionExpression.GetActionName();
    var routeValues = actionExpression.GetRouteValues();

    SetRouteValues(actionName, controllerName, routeValues);

    return new RedirectToRouteResult("Default", RouteData.Values);
}

//a few helper methods
public static class AdditionUrlHelperExtensions
{
    public static string GetControllerName(this Type controllerType)
    {
        var controllerName = controllerType.Name.Replace("Controller", string.Empty);
        return controllerName;
    }

    public static string GetActionName<TController>(this Expression<Func<TController, object>> actionExpression)
    {
        var actionName = ((MethodCallExpression)actionExpression.Body).Method.Name;

        return actionName;
    }

    public static RouteValueDictionary GetRouteValues<TController>(this Expression<Func<TController, object>> actionExpression)
    {
        var result = new RouteValueDictionary();
        var expressionBody = (MethodCallExpression)actionExpression.Body;

        var parameters = expressionBody.Method.GetParameters();

        //expression tree cannot represent a call with optional params
        //so our method param count and should match the expression body arg count
        //but just the same, let's check...
        if (parameters.Length != expressionBody.Arguments.Count)
            throw new InvalidOperationException("Mismatched parameter/argument count");

        for (var i = 0; i < expressionBody.Arguments.Count; ++i)
        {
            var parameter = parameters[i];
            var argument = expressionBody.Arguments[i];

            var parameterName = parameter.Name;
            var argumentValue = argument.GetValue();

            result.Add(parameterName, argumentValue);
        }

        return result;
    }

    private static object GetValue(this Expression exp)
    {
        var objectMember = Expression.Convert(exp, typeof(object));
        var getterLambda = Expression.Lambda<Func<object>>(objectMember);
        var getter = getterLambda.Compile();

        return getter();
    }
}

Usage: RedirectToAction<HomeController>(c => c.Index("param1", 2, false))

You get the nice compile time safety net of ensuring you are redirecting to valid controller actions, along with correct argument types.

Dean Goodman
  • 973
  • 9
  • 22
  • 1
    It's probably possible to get type safety as well, if you use `RedirectToAction(Expression> action)` then call it like `RedirectToAction(c => c.DoSomething(1, 3))`. The expression parsing/reflection is trickier though – Kevin Gosse May 25 '17 at 18:39
  • 1
    @KevinGosse good point. I hadn't needed it in my previous use case, but that's probably a much better solution overall. – Dean Goodman May 25 '17 at 18:56
  • Wow, that sounds nice! But there is one thing I don't really understand: `RedirectToRouteResult x.Index())` let me access `Index` from `PageController`, but neither `Index` is static, nor it seems that using the Expression we generate a object of `PageController`. So how this is possible? I thought this couldn't work since an object would be required for this. – Lion May 25 '17 at 19:22
  • @Lion This may help: https://stackoverflow.com/questions/793571/why-would-you-use-expressionfunct-rather-than-funct – Dean Goodman May 25 '17 at 19:45