Roslyn: C# nameof() with substring specifier - nameof(AccountsController, 0, 8)

Created on 14 Jan 2017  路  10Comments  路  Source: dotnet/roslyn

Problem:

I'll use ASP.NET MVC as an example of a problem-space where this proposal would help:

ASP.NET MVC's RouteUrl function (and the Route collection in general) use magic-strings which match the names of controllers, except if the Controller's class name ends with the string "Controller" (such as "AccountsController") then RouteUrl requires the controllerName value to be only "Accounts", and it won't work if you pass-in "AccountsController".

This means that you cannot use nameof(AccountsController) (as in url.RouteUrl( "Index", nameof(AccountsController)) to avoid magic-strings in ASP.NET MVC projects - you need to manually code-in url.RouteUrl( "Index", "Accounts" ) - which predictably causes bugs in larger projects where you fail to update every string if you rename a controller.

(In my own projects I worked-around this by using T4 and DTE to generate a class which contains the controller names (sans "Controller" suffix) as const String members, but I'd like to avoid this as it complicates the build process).

Proposed solution:

This could be solved by making nameof() accept two or three const Int32 arguments that serve as a substring specifier with the same semantics as String.Substring - so nameof(AccountsController, 8) would evaluate to the string literal "Controller" and nameof(AccountsController, 0, 8) would evaluate to "Accounts" - I'd also like to propose allowing a negative startIndex parameter, so you can specify the length of a suffix to remove, so nameof(AccountsController, -10) would also evaluate to "Accounts".

There would be a compile-time error if the arguments are out of range (e.g. longer than the length of the ordinarily generated string).

If C# ever gets constexpr support, it would be nice if the integer literals could be replaced with compile-time computed lengths from other const strings, to avoid having to remember that "Controller" is 10 characters long, like so: nameof(AccountsController, -"Controller".Length) == "Accounts" or nameof(AccountsController, "Accounts".Length) == "Controller".

(Absent constexpr support - another feature request could be for a length() operator - or overload of sizeof which accepts a string literal and returns its length, which could be used with this proposed nameof: nameof(AccountsController), -sizeof("Controller") ) == "Accounts").

Area-Language Design Discussion

Most helpful comment

I'd prefer nameof(...).Substring(...) to be treated as a constant instead of new syntax (#12238).

All 10 comments

I'd prefer nameof(...).Substring(...) to be treated as a constant instead of new syntax (#12238).

First of all, I agree that this is a valid problem and has to be addressed.

The suggested substring parameters are useless without the length() operator.
The chance that you accidentally change the "Controller" part of "AccountsController" is simply too big, the compiler should be able to give some error; or rather, we should be able to tell it when to treat the result of the transformation as an error (out of range is just one out of many possible mistakes).

Now, as you said we'd need the length() operator, which will complicate things more.
It will be a step into the C++ direction where we have (sometimes confusing) preprocessor macros (and no way to easily debug them).
It will add to "feature creep" and worse: It actually does not even address the core of the issue:

I think what you want instead is a short, concise way to tell the compiler to apply some transformation to generate a string, at compile time.

But I think extending sizeof() and adding length() is definitely the wrong way to solve this.

I can imagine a ton of usecases where you'd want a similar result; but in those the two extensions you suggested won't help. (Parsing file formats, serializing data, handling network packets, generally improving performance, ...)

Something like constexpr is already a lot better.
Maybe enable String.Substring, String.Length, String.Remove, ... to be treated as constant expressions.
If implemented correctly we could debug const expressions.

But I think that people will eventually want more and more things to become "constant expression enabled".

So why bother with all that in the first place, when you could just do something like:

class AccountsController
{
    [MyCompileTimeNameRewriterThing]
    const string Name = "";
}

static class MyCompileTimeNameRewriterThing
{
    static void ProcessMember(MemberInfo member, Type containingType)
    {
         string name = containingType.Name;
         if(!name.EndsWith("Accounts"))
              throw CompileErrorException("Type has the wrong name format!");
        member.Name = name.Substring(0, name.Length - 10);
    }
}

Just a rough example. It would solve your problem and a TON of other problems at the same time.
Like this: #16160 or #5561

@asdfgasdfsafgsdfa

So why bother with all that in the first place, when you could just do something like:

This is good and all in theory but in practice it's a _terrible_ workaround to these kind of problems.

Each time you refactor the code you will have to recompile the generator so it won't throw an error, it can be a burden for large projects or even small projects, a generator is code like any other and it needs to be maintained like any other code, _moving_ the problem doesn't mean it was solved.

Generator are great for generating _patterns_ of code and _scaffolding_ but creating specific generators for a class or for specific pieces of code that can and probably will change seems like _killing flies with a sledgehammer_.

I mean the idea of code generators is awesome but it shouldn't be the silver bullet for everything...

What we _really_ need in C# is constexpr support we already have constant expressions in the language but there's no way to construct such expressions.

I disagree this is a problem with nameof, and it would make refactoring nightmare. I would suggest to fix the RouteUrl function instead.

@miloush I don't disagree with your suggestion but another approach is to pass it to another function that returns the name that RouteUrl expects, so something like MapControllerNameToRouteUrlName(nameof(AccountsController)) or/and have an extension method for it.

@eyalsk

Each time you refactor the code you will have to recompile the generator so it won't throw an error, it can be a burden for large projects or even small projects, a generator is code like any other and it needs to be maintained like any other code, moving the problem doesn't mean it was solved.

In my opinion you really want the thing to throw an error, at compiletime.
Manually recompiling the code generator/replacer should not be needed if the feature is implemented correctly.

And yes it needs to be maintained, just like all other code.
Forgive me if that question sounds dumb, but wouldn't you have to maintain all of those suggestions in one war or another??

If you change the name of AccountsController then obviously you need to take care of all the places that are affected by the change. I fail to see how a different solution makes the problem go away completely. You always need a human to do the refactoring like it is intended, and the only thing I'm arguing for is a system that:

  • gives you precise and meaningful error messages
  • makes it easy to add and implement them
  • allows you to express all sorts of constraints and rules

It seems to me that no matter what we do the problem always "just moves" (it just gets easier to check, and the errors get more reliable) and can never really be solved unless you get some sort of AI to do the refactorings for you...

Generator are great for generating patterns of code and scaffolding but creating specific generators for a class or for specific pieces of code that can and probably will change seems like killing flies with a sledgehammer. I mean the idea of code generators is awesome but it shouldn't be the silver bullet for everything...

Sounds like we two are imagining vastly different implementations for code generators.
Im not saying you are wrong; I just want to figure out why you think code generators would be overkill for something like this.

Btw, I totally agree that we should have some implementation of constexpr as well as code generators! :)

@asdfgasdfsafgsdfa

Forgive me if that question sounds dumb, but wouldn't you have to maintain all of those suggestions in one war or another??

Sure, I certainly would but the question should be about the time and efforts invested and I think that a solution should always be relatively simple to the complexity of the problem and I don't think that this is the case.

If you change the name of AccountsController then obviously you need to take care of all the places that are affected by the change.

We use refactoring tools to automate actions that otherwise we had to do manually so when I rename AccountsController I don't want to go to my generator change the _hardcoded_ name and then recompile it and I'm not speaking about the additional tests that I will need to write just for this generator, to me it doesn't seems ideal or realistic but then I respect your opinion if you think otherwise.

I fail to see how a different solution makes the problem go away completely.

There are two ways to handle this problem in a _meaningful_ way:

A. Introduce an extension method such as url.RouteUrlByFullName("Index", nameof(AccountsController)) this will remove the Controller suffix and call url.RouteUrl.

B. Add constexpr to the language that can solve this problem and many other issues then you would write a function that take the name and remove the suffix at compile-time.

Both of these approaches means that whenever you will rename the name of the class you won't need to do anything _special_ because nothing is _hardcoded_, it will just work.

You always need a human to do the refactoring like it is intended

Sure but once you refactor it applies to all the occurrences automatically, once you _hardcode_ values, you need to rename it yourself, you need to remember things and this can be a problem.

I'm arguing for is a system that:

gives you precise and meaningful error messages
makes it easy to add and implement them
allows you to express all sorts of constraints and rules

Sure, I'd love that but I'm not arguing about generators, in fact, I'd love this feature to be implemented I'm just saying that it's too _heavy_ to solve this specific problem.

_In fact, I don't know why the OP is using T4 to solve such a simple problem but then I don't know the whole story so I'm not going to pretend I do or give advice._

It seems to me that no matter what we do the problem always "just moves" (it just gets easier to check, and the errors get more reliable) and can never really be solved unless you get some sort of AI to do the refactorings for you...

I don't really know what you wanted to say with this because I didn't say that refactoring are done _automagically_ or anything. :)

The point of refactoring is to apply the same action over all of the occurrences in the code so if you rename AccountsController to ProfilesController and then you need to change the _hardcoded_ string inside your generator and finally compile it I'd argue that this problem shouldn't be dealt with a generator.

Sounds like we two are imagining vastly different implementations for code generators.
Im not saying you are wrong; I just want to figure out why you think code generators would be overkill for something like this.

Well, in my mind a code generator fits more into generating _repetitive code_ and it shouldn't contain knowledge about the application itself unless this knowledge is being passed as an argument like so:

class AccountsController
{
    [ControllerSuffixRemover(nameof(AccountsController))]
    const string Name = "";
}

class ControllerSuffixRemover :  Attribute
{
    private string _name;

    public ControllerSuffixRemover(string name)
    {
        _name = name;
    }

    void ProcessMember(MemberInfo member, Type containingType)
    {   
        member.Name = _name.Remove(_name.Length - 10);
    }
}

Now, I don't know why would you want to do that but still this is just an example, in practice I'd just use an extension method, the way I explained above.

Btw, I totally agree that we should have some implementation of constexpr as well as code generators! :)

Yeah, I completely agree! 馃槈

Somewhat related to #15079

@eyalsk

A. Introduce an extension method such as url.RouteUrlByFullName("Index", nameof(AccountsController)) this will remove the Controller suffix and call url.RouteUrl.

That would be a solution. But it would happen at runtime. With constexpr support it could be somehow optimized though (no calls to Remove or Substring or the likes at runtime)

Now I keep getting the feeling that you imagine the code generators approach that I suggested as something huge and complicated (relatively). Like using a sledgehammer to crack a nut.

But it doesn't have to be that way! Not the way I imagine the code generators feature at least.
I think it would derail the topic too much to explain my ideas for code generators here though.

I have some ideas how constexpr support could be implemented and I'll elaborate on that in #15079

@asdfgasdfsafgsdfa

But it doesn't have to be that way! Not the way I imagine the code generators feature at least.
I think it would derail the topic too much to explain my ideas for code generators here though.

It would be interesting to know, is it based on the feature described here?

I have some ideas how constexpr support could be implemented and I'll elaborate on that in #15079

That would be quite awesome! 馃憤

Was this page helpful?
0 / 5 - 0 ratings