AspNetCore.Docs icon indicating copy to clipboard operation
AspNetCore.Docs copied to clipboard

Add documentation for DynamicRouteValuesTransformer

Open rynowak opened this issue 5 years ago • 18 comments

This is a new feature that allows users to migrate IRouter code that dynamically manipulates route values to endpoint routing.

https://github.com/aspnet/AspNetCore/issues/4221

rynowak avatar Jun 25 '19 03:06 rynowak

Summary

DynamicRouteValueTransformer is a new feature in Endpoint Routing that supports create of slug-style routes. Implementing a DynamicRouteValueTransformer allows an application to programmatically select a controller or page to handle the request, usually based on some external data.

Writing a transformer

First, subclass DynamicRouteValueTransformer, and override the TransformAsync method. Inside TransformAsync the transformer should compare route values and details of the request to map the request to an appropriate handler.

Usually this process entails reading some known route value from the current route values, and performing a database lookup to get a new set of values.

The DynamicRouteValueTransformer implementation can access services from dependency injection through the constructor.

Example:

    public class Transformer : DynamicRouteValueTransformer
    {
        private readonly ArticleRepository _respository;

        public Transformer(ArticleRepository repository)
        {
            _respository = repository;
        }

        public override async ValueTask<RouteValueDictionary> TransformAsync(HttpContext httpContext, RouteValueDictionary values)
        {
            var slug = values["article"] as string;

            var article = await _respository.GetArticleBySlug(slug);
            if (article == null)
            {
                return null;
            }

            return new RouteValueDictionary()
            {
                { "page", article.Page },
                { "id", article.Id },
            };
        }
    }

Returning null from TransformAsync will treat the route as a failure (did not match anything).

Returning route values from TransformAsync will perform a further lookup to find matching pages or controllers. The matching logic is similar to how conventional routing selects controllers, by matching the area, action, and controller route values (or area, and page in the case of pages).

Registering a transformer

A DynamicRouteValueTransformer must be registered with the service collection in ConfigureServices using its type name.

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<ArticleRepository>();
            services.AddScoped<Transformer>();
            services.AddRazorPages();
        }

The service registration can use any lifetime.

Additionally, the transformer needs to be attached to a route inside of UseEndpoints()

Example (using a transformer with pages):

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
                endpoints.MapDynamicPageRoute<Transformer>("blog/{**article}");
            });

Example (using a transformer with controllers):

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDynamicControllerRoute<Transformer>("blog/{**article}");
            });

rynowak avatar Jun 27 '19 00:06 rynowak

@Rick-Anderson can this be picked up by you?

mkArtakMSFT avatar Sep 05 '19 21:09 mkArtakMSFT

@Rick-Anderson can this be picked up by you?

Yes, assigned to @scottaddie. @scottaddie can probably do this after the high priority 3.0 docs issues are completed.

Rick-Anderson avatar Sep 05 '19 21:09 Rick-Anderson

@serpent5 are you interested in writing a sample using the above code? Sometime next year? No hurry.

Rick-Anderson avatar Dec 16 '19 23:12 Rick-Anderson

Yeah, this looks good. I'll add it to my list.

serpent5 avatar Dec 17 '19 23:12 serpent5

@serpent5 we'd really appreciate a PR from you on this.

Rick-Anderson avatar Jan 17 '20 23:01 Rick-Anderson

Where should this content live? I'm not sure that it fits into any of the existing topics.

serpent5 avatar Jan 19 '20 22:01 serpent5

@JamesNK @scottaddie please advise where documentation for DynamicRouteValuesTransformer should go.

Rick-Anderson avatar Jan 20 '20 00:01 Rick-Anderson

Going into detail on DynamicRouteValueTransformer (along with MapDynamicPageRoute and MapDynamicControllerRoute) isn't useful without knowing when it should be used. First need to discuss with you would want to use dynamic endpoint routing verses traditional endpoint routing. Talk about some scenarios like translation.

Content should either be a new section on the routing page - https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-3.1 - or a sub-article. "Dynamic routing"? Can also cover https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.routing.idynamicendpointmetadata?view=aspnetcore-3.1. I think routing is an enormous topic and should be split up into multiple pages. This could be step 1 😄

It should also be mentioned that dynamic routing offers an migration path for some custom IRouter implementations that can't be implemented using endpoint routing's more static model.

JamesNK avatar Jan 20 '20 00:01 JamesNK

Routing is in PR now #16456

Rick-Anderson avatar Jan 20 '20 01:01 Rick-Anderson

@Rick-Anderson It looks like there's explanation needed around why this would be needed, etc, that I just can't cover nearly well enough. I think this would be better tackled by one of the experts.

serpent5 avatar Jan 25 '20 19:01 serpent5

It is not clear what IDynamicEndpointMetadata is used for, when using a dynamic route, it is not added.

rjperes avatar Apr 24 '20 15:04 rjperes

I'm using this to replace my ASP.NET Core 2.2 LoginRouter that basically redirects (internally) all requests over to the login page unless the user is already authenticated. This keeps the original URL intact and after login the user will just see the intended page. No need for ugly and insecure returnToUrl parameters or other hacks commonly in place. Request the URL, you get either a login form or the requested resource. No other URLs involved.

So this covers all URLs in the entire application. What should I specify for the pattern in the MapDynamicControllerRoute call? "*" or ""? The existing explanation only covers blog article style URLs in a separate path but nothing really dynamic.

ygoe avatar May 06 '20 19:05 ygoe

Also, this whole things fails because the parameter RouteValueDictionary values is null, always. What should I do?

Update: I could fix(?) that null thing with a dummy pattern. Now my Transform method is called for every request (as far as I could see). But there's the next problem: httpContext.User.Claims is always empty in this method. It contains the expected data elsewhere but the copy that's available here is useless. That makes the whole routing idea useless. Where can I get a proper claims object in this Transform method?

Update 2: No, it's only called for the start page (URL: /), no other URLs. Somehow this whole dynamic routing thing doesn't want to work for me. Additional documentation is really needed to make use of it.

Here's some code:

public class LoginRouter : DynamicRouteValueTransformer
{
	public override async ValueTask<RouteValueDictionary> TransformAsync(HttpContext httpContext, RouteValueDictionary values)
	{
		await Task.CompletedTask;   // Fake async

		// Route unauthenticated users to the login action
		if (values != null && !httpContext.User.Claims.Any())
		{
			values["controller"] = "Account";
			values["action"] = "Login";
		}
		return values;
	}
}

public class Startup
{
	// ...
	public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
	{
		// ...
		app.UseStaticFiles();
		app.UseRouting();
		app.UseAuthentication();
		app.UseAuthorization();
		app.UseEndpoints(endpoints =>
		{
			// Globally re-route unauthenticated requests
			endpoints.MapDynamicControllerRoute<LoginRouter>("{whatever?}");

			// Default routes
			endpoints.MapControllerRoute(
				name: "defaultNoAction",
				pattern: "{controller}/{id:int}",
				defaults: new { action = "Index" });
			endpoints.MapControllerRoute(
				name: "default",
				pattern: "{controller=Home}/{action=Index}/{id?}");
		});
	}
}

public class AccountController : Controller
{
	public IActionResult Login()
	{
		return View(new AccountLoginViewModel());
	}

	[HttpPost]
	[ValidateAntiForgeryToken]
	public async Task<IActionResult> Login(AccountLoginViewModel login)
	{
		// ...
		// Everything is checked, perform the login
		var claims = new List<Claim>
		{
			new Claim("UserId", user.Id.ToString()),
			new Claim(ClaimTypes.Name, user.LoginName)
		};
		var claimsIdentity = new ClaimsIdentity(
			claims,
			CookieAuthenticationDefaults.AuthenticationScheme);
		await HttpContext.SignInAsync(
			CookieAuthenticationDefaults.AuthenticationScheme,
			new ClaimsPrincipal(claimsIdentity),
			new AuthenticationProperties
			{
				IsPersistent = true
			});
		return Redirect(Request.Path);
	}
}

ygoe avatar May 06 '20 20:05 ygoe

@ygoe it doesn't work anyway as far as I can tell (I raised this a while ago, but haven't had time to try the suggested alternative): https://github.com/dotnet/aspnetcore/issues/18688 ). Not sure if your issue in "Update 2" is the same as I was seeing or not.

jsabrooke avatar May 22 '20 11:05 jsabrooke

It doesn't work for me either, and values is also null so I create a new dictionary. The transformer gets hit on a request, but then 404. Almost a year later y'all, and still no documentation that works. 😕

return ValueTask.FromResult(new RouteValueDictionary
{
    { "controller", controller },
    { "action", action }
});

SteveAndrews avatar Mar 12 '21 07:03 SteveAndrews

it also does not work for me. My project is still using .NET core 2.1 :)

nguyenhuuloc304 avatar Jul 25 '21 12:07 nguyenhuuloc304

Any updates on this? Proper documentation is really needed

Misiu avatar Nov 23 '21 21:11 Misiu