Skip to content

Async IRouteConstraint - IRouter.GetVirtualPath alternative in EndpointRouting? #18883

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
alienwareone opened this issue Feb 7, 2020 · 8 comments
Assignees
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates feature-routing investigate
Milestone

Comments

@alienwareone
Copy link

alienwareone commented Feb 7, 2020

There should be an async version of Microsoft.AspNetCore.Routing.IRouteConstraint.

Task<bool> MatchAsync(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
{
    return await dbContext.Products.Any(x => x.PageName == values[routeKey]);
}
@rynowak rynowak added the area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates label Feb 7, 2020
@rynowak
Copy link
Member

rynowak commented Feb 7, 2020

Route constraints have to run during URL generation which is synchronous, so it's not straightforward to add something like this. If you want to achieve the same effect, you could add a MatcherPolicy.

@alienwareone
Copy link
Author

alienwareone commented Feb 7, 2020

Thanks for your answer!

I guess I'll go with "DynamicRouteValueTransformer" to replace my custom IRouter implementation.

What is the "VirtualPathData GetVirtualPath(VirtualPathContext context)" alternative for complex url generation?

For example:

public override VirtualPathData GetVirtualPath(VirtualPathContext context)
{
  var routeContext = context["context"] as new ProductListRouteContext();
  var pathSegements = new List<string>();
  if (context?.Category?.PageName != null)
  {
    pathSegments.Add(context.Category.PageName);
  }
  // Add other pathSegements based on the context/filters...
  return new new VirtualPathData(this, string.Join("/", pathSegments));
}

In my view:

Url.RouteUrl("ProductList", new { context = Model.ProductListRouteContext });

@rynowak
Copy link
Member

rynowak commented Feb 7, 2020

We don't have one right now. Depending on your scenario you might want to stick with old routing in 3.X.

If you want to provide more info about what you need to accomplish, I can make sure to include it in 5.0 plans.

@alienwareone
Copy link
Author

alienwareone commented Feb 7, 2020

Right now there seems no way to generate an Url based on a DynamicRouteValueTransformer-Route.

// Currently:
routeBuilder.MapDynamicControllerRoute<ProductListTransformer>(pattern: "search/{*path}");

// Should be:
routeBuilder.MapDynamicControllerRoute<ProductListTransformer>(routeName: "ProductList", pattern: "search/{*path}");

// View:
Url.RouteUrl("ProductList", new { path = GeneratePath(Model.Context) });
// e.g. "search/filter1/filter2"

@javiercn
Copy link
Member

@alienwareone I think @rynowak is trying to ask for a use case so that he can understand if we need to add a feature here or if he can recommend an alternative way of achieving the same thing.

@javiercn javiercn added the Needs: Author Feedback The author of this issue needs to respond in order for us to continue investigating this issue. label Feb 10, 2020
@javiercn javiercn added this to the 5.0.0-preview2 milestone Feb 10, 2020
@alienwareone
Copy link
Author

Here is the relevant code for my use case. Just ask me if anything is unclear. Thanks.

// Startup.cs:
app.UseMvc(routes =>
{
    routes.Routes.Add(new ProductListRoute(RouteName.ProductIndex, GetControllerName<ProductsController>(), nameof(ProductsController.Index), "products", routes.DefaultHandler));
    // Now you can use "ProductListRoute" wherever you need the "ProductListRouteContext":
    routes.Routes.Add(new ProductListRoute(RouteName.ProductSearchForm, GetControllerName<ProductsController>(), nameof(ProductsController.SearchForm), "products/searchform", routes.DefaultHandler));
});

public class ProductListRoute : INamedRouter
{
    public ProductListRoute(string name, string controller, string action, string pathPrefix, IRouter next)
    {
        Name = name;
        Next = next;
        Controller = controller;
        Action = action;
        PathPrefix = pathPrefix;
    }

    public string Name { get; }
    public string Controller { get; }
    public string Action { get; }
    public string PathPrefix { get; }
    public IRouter Next { get; }

    public async Task RouteAsync(RouteContext context)
    {
        var routeContext = new ProductListRouteContext();
        var pathSegements = context.Path.Split('/').Skip(1/*Skip PathPrefix*/).ToArray();
        var dbContext = context.RequestServices.GetRequiredService<AppDbContext>();
       
        foreach (var pathSegment in pathSegements)
        {
            var category = await dbContext.Categories.AnyAsync(x => x.PageName = pathSegement);
            if (category != null)
            {
                routeContext.Category = category;
                continue;
            }

            var location = await dbContext.Locations.AnyAsync(x => x.PageName = pathSegement);
            if (location != null)
            {
                routeContext.Location = category;
                continue;
            }
        }

        int page;
        if (int.TryParse(query["page"], out page))
        {
            routeContext.Page = page;
        }
        else
        {
            routeContext.Page = 1;
        }

        // Minimum one active filter required or in the "root" path:
        if (routeContext.CountActiveFilters() == 0 || !context.Path.StartsWithSegments(PathPrefix))
        {
            return;
        }

        context.RouteData.Values["controller"] = Controller;
        context.RouteData.Values["action"] = Action;
        context.RouteData.Values["context"] = routeContext;

        await Next.RouteAsync(context);
    }

    public VirtualPathData GetVirtualPath(VirtualPathContext context)
    {
        var routeContext = context.Values["context"] ?? new ProductListRouteContext();

        var pathSegments = new Dictionary<string, string>();
        var query = new Dictionary<string, string>();

        // Filters:
        if (routeContext.Category != null)
        {
            pathSegments["Category"] = routeContext.Category.PageName;
        }
        if (routeContext.Location != null)
        {
            pathSegments["Location"] = routeContext.Location.PageName;
        }

        if (routeContext.Page > 1)
        {
            query["page"] = routeContext.Page.ToString();
        }

        // Generic QueryString values:
        // e.g. Html.RouteUrl(RouteName.ProductIndex, new { context = new ProductListRouteContext { ... }, @ref = "navigation-link-clicked" }}
        foreach (var item in context.Values)
        {
            if (item.Key != "context")
            {
                query[item.Key] = Uri.EscapeDataString(item.Value?.ToString() ?? string.Empty);
            }
        }

        return string.Format("{0}/{1}?{2}",
            PathPrefix,
            string.Join("/", pathSegments.Values),
            string.Join("&", query.Select(x => $"{x.Key}={x.Value}")))
            .TrimEnd('?');
    }
}

public class ProductsController : Controller
{
    public async Task<IActionResult> Index(ProductListRouteContext context)
    {
        var viewModel = new ProductIndexViewModel
        {
            ProductListRoute = context,
            Categories = await _dbContext.Categories.ToList()
        };
        return View(viewModel);
    }

    public async Task<IActionResult> SearchForm(ProductListRouteContext context)
    {
        var viewModel = new SearchFormViewModel
        {
            ProductListRoute = context,
            Categories = await _dbContext.Categories.ToList(),
            Locations = await _dbContext.Locations.ToList()
        };
        return View(viewModel);
    }
}

// View example for Category filters in Products/Index.cshtml:
@foreach (var category in Model.Categories)
{
    <a href="@Html.RouteUrl(RouteName.ProductIndex, new { context = new ProductListRouteContext(copy: Model.CurrentProductListRouteContext) { Category = category } })">@category.Name</a>
}

// View example for paging in Products/Index.cshtml:
@for (var page = 1; page < Model.Pages; page++)
{
    <a href="@Html.RouteUrl(RouteName.ProductIndex, new { context = new ProductListRouteContext(copy: Model.CurrentProductListRouteContext) { Page = page } })">Page @page</a>
}

// View example for an external SearchForm where we need the current "ProductListRouteContext":
<a href="@Html.RouteUrl(RouteName.ProductSearchForm, new { context = new ProductListRouteContext(copy: Model.CurrentProductListRouteContext) })">Show Filters</a>

@ghost ghost added Needs: Attention 👋 This issue needs the attention of a contributor, typically because the OP has provided an update. and removed Needs: Author Feedback The author of this issue needs to respond in order for us to continue investigating this issue. labels Feb 10, 2020
@alienwareone alienwareone changed the title Async IRouteConstraint Async IRouteConstraint - IRouter.GetVirtualPath alternative in EndpointRouting? Feb 12, 2020
@mkArtakMSFT mkArtakMSFT removed the Needs: Attention 👋 This issue needs the attention of a contributor, typically because the OP has provided an update. label Apr 27, 2020
@mkArtakMSFT
Copy link
Contributor

Thanks for contacting us. Given that this is possible to achieve using a custom policy we do not plan to do anything here.

@alienwareone
Copy link
Author

Thanks for contacting us. Given that this is possible to achieve using a custom policy we do not plan to do anything here.

It's not about policy. It's about "IRouter.GetVirtualPath alternative in EndpointRouting".

I made a separate "Feature request" to make this more clear.
#23041

@ghost ghost locked as resolved and limited conversation to collaborators Jul 17, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates feature-routing investigate
Projects
None yet
Development

No branches or pull requests

4 participants