This is a new feature that allows users to migrate IRouter code that dynamically manipulates route values to endpoint routing.
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.
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:
```C#
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.
```C#
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):
```C#
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapDynamicPageRoute
});
Example (using a transformer with controllers):
```C#
app.UseEndpoints(endpoints =>
{
endpoints.MapDynamicControllerRoute<Transformer>("blog/{**article}");
});
@Rick-Anderson can this be picked up by you?
@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.
@serpent5 are you interested in writing a sample using the above code? Sometime next year? No hurry.
Yeah, this looks good. I'll add it to my list.
@serpent5 we'd really appreciate a PR from you on this.
Where should this content live? I'm not sure that it fits into any of the existing topics.
@JamesNK @scottaddie please advise where documentation for DynamicRouteValuesTransformer should go.
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.
Routing is in PR now
@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.
It is not clear what IDynamicEndpointMetadata is used for, when using a dynamic route, it is not added.
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.
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 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.
Most helpful comment
Yes, assigned to @scottaddie. @scottaddie can probably do this after the high priority 3.0 docs issues are completed.