aspnet / Routing

[Archived] Middleware for routing requests to application logic. Project moved to https://github.com/aspnet/AspNetCore
Apache License 2.0
272 stars 122 forks source link

Retrieving Route Data from Virtual Path in MVC Router #823

Closed Eilon closed 6 years ago

Eilon commented 6 years ago

From @knyzorg on Monday, 13 August 2018 20:01:26

I am working on an .NET Core MVC application which requires alternative controller/action names to be allowed. To accomplish this, I am using my own Router on a MapRoute:

app.UseMvc(routes =>
        {
            routes.Routes.Add(new CustomRouter(routes.DefaultHandler));
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });

My custom router observes the requested controller and action, and based on it places a new value into the RouteData in the request:

public async Task RouteAsync(RouteContext context)
{
   [...]
   if (requestedAction == "fakeAction")
    context.RouteData.Values["action"] = "realAction";

However, to determine the value of the requestedAction, I am basically taking the requested path, splitting it and getting the value of it that way. This seems suboptimal.

What I would like to do would look something like this:

var rr = new RouteBuilder(app);
var myRoute = rr.MapRoute(...).Build();
var myRouteData = myRoute.GetRouteData(context);
myRouteData["action"] == "fakeAction";

Another solution to this problem which I would very much enjoy is if I could do the following:

   app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "something",
                template: "{controller=Home}/{action=Index}/{id?}");
            routes.Routes.Add(new CustomRouter(routes.DefaultHandler));
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });

But NOT have my something route actually route anything and only serve as a way to define the RouteData for my CustomRouter.

Is either of these possible? I do not like the idea us uncleanly implementing existing functionality as it is a both a code smell and a potential maintenance difficulty in the future.

Copied from original issue: aspnet/Home#3427

mkArtakMSFT commented 6 years ago

Thanks for contacting us, @knyzorg. @rynowak, what are your thoughts regarding this?

rynowak commented 6 years ago

@knyzorg - at a high level what are you trying to accomplish?

vezaynk commented 6 years ago

Really I just want to be able to have synonyms for controller and action names. I want /foo/bar to trigger the same code paths as /oof/rab if I so desire.

The reason for this is URL internationalization, I want to have urls look like en/hello/world and fr/salut/monde which map to the same page. I would leave MVC behind completely in favor of writing my own convention, but the convenience of having url templates and generating anchor tags automatically is too good to pass up.

Another need which I have, is for it not to break virtual paths (IIS).

Ideally, none of the synonyms would be hard-coded, I would rather be able to have some logic deciding what action (if any) the requested one is synonymous to during a request.

Anything like this possible?

rynowak commented 6 years ago

Thanks, that's what I thought you might be asking 😀

to trigger the same code paths as /oof/rab if I so desire

Should this include views? If you have views, will this include different .cshtml files for each locale?

How do you want/expect link generation to work? Do you want something like:

Url.Action("World", "Hello", new { locale = "fr" }) -> /fr/salut/monde

OR

Url.Action("Monde", "Salut") -> /fre/salut/monde

I'm asking for clarification because there are a few difference schemes for how to implement something like this. The major pivot is whether you want to introduce another route value that represents the locale, or whether you want to localize the route values themselves.

vezaynk commented 6 years ago

If the link generation could work by allowing me to specify the local myself, that would really be ideal if possible. So the first variant please.

Url.Action is basically the same thing as the <a> helper tag, right?

rynowak commented 6 years ago

Url.Action is basically the same thing as the helper tag, right?

Yes, exactly.

rynowak commented 6 years ago

Give me a bit, I'll whip up a sample of this.

rynowak commented 6 years ago

@knyzorg - reading through your question again, I'm wondering if I forgot to point out something obvious.

You can change routes.DefaultHandler so that it points to your custom route. Then you'll be called with the route values on the way in, and when links are being generated.

Inside RouteAsync translate the values from the locale to whatever language you're writing code in, and then do the reverse in GetVirtualPath

routes.DefaultHandler = new TranslationRoute(routes.DefaultHandler);

Is this what you're looking for?


Another option that I've helped people used in the past is to use IActionModelConvention to 'multiply' actions - you can do all of the translations at startup time, and we'll handle all of the processing for you.

This won't be useful to you if you need to loop up the strings when each request is processed. Let me know if you're interested in this instead and I'll put together a sample.

vezaynk commented 6 years ago

How would TranslationRoute be implemented?

rynowak commented 6 years ago

Inside RouteAsync translate the values from the locale to whatever language you're writing code in, and then do the reverse in GetVirtualPath

vezaynk commented 6 years ago

It's really going over my head. I don't know to implement a route by myself. I thought of extending the Route class to make a TranslationRoute but Route wants a third parameter in the constructor involving inline constraints which I never even heard of before.

This is my first project in .NET and I may need some hand holding.

rynowak commented 6 years ago

OK thanks for the feedback, I'll get back to you in a bit with a more fleshed out sample.

rynowak commented 6 years ago

Here's a sample: https://github.com/aspnet/Mvc/compare/rynowak/localization-routing?expand=1

There's a few things that are slightly non-obvious and clunky.

Creating the routes themselves is kinda gross because there are so many parameters that are rarely used https://github.com/aspnet/Mvc/compare/rynowak/localization-routing?expand=1#diff-cd035ee26336357cbade86282aad4a22R73

If you're using default values in the routes, you have 'translate' them in the defaults as well. https://github.com/aspnet/Mvc/compare/rynowak/localization-routing?expand=1#diff-cd035ee26336357cbade86282aad4a22R87

In the route, when you're doing link generation (GetVirtualPath) you will need to copy the 'language' from ambient values to values, in order for the correct route to be selected. https://github.com/aspnet/Mvc/compare/rynowak/localization-routing?expand=1#diff-42421a02d0010b370b9d53f0cee75762R31

If you're planning on using other localization features in ASP.NET Core then it would probably help to read over: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization?view=aspnetcore-2.1

In particular if you're using other MVC localization features then you will probably want to write an IResourceFilter that sets the current culture based on the language route value.

vezaynk commented 6 years ago

It seems to be essentially what I was looking for. Thanks. I will try it out as soon as I can and will get back to you.

rynowak commented 6 years ago

Great, I'm going to close this. Feel free to open another issue if you need something else.

vezaynk commented 6 years ago

Hey @rynowak, I plugged it in and it seems to be working. I also managed to understand why it works which is great.

However, why are you repeating the creation of routes while a single route seems to be working fine:

app.UseMvc(routes =>
             {
                 routes.Routes.Add(new TranslationRoute(
                    translations,
                    routes.DefaultHandler,
                    routeName: null,
                    routeTemplate: "{language=en}/{controller=Home}/{action=Index}/{id?}",
                    defaults: new RouteValueDictionary(new {  }),
                    constraints: null,
                    dataTokens: null,
                    inlineConstraintResolver: routes.ServiceProvider.GetRequiredService<IInlineConstraintResolver>()));

             });

(Moved defaults language to the route template)

rynowak commented 6 years ago

This would also work fine. My example (multiple routes) limits the number of languages that can match. In your example it would match any string with that first segment: /foobar/Home/Index and bind it to the language route value.