Search code examples
asp.net-coreasp.net-core-mvc

Always add existing route parameter in Url.Action


I'd like to be able to force Url.Action to always add one of my route values to the URL. Basically, I need to be able to add a customer ID in the URL. My routes look like this:

app.MapControllerRoute(
     name: "Areas",
     pattern: "{customerid:int}/{area:exists}/{controller}/{action=Index}");

app.MapControllerRoute(
     name: "default",
     pattern: "{customerid:int}/{controller=Home}/{action=Index}/{id?}");

I'm not concerned with exposing the Customer ID since it will always be protected against the authenticated user. These routes will allow a user to view/manage different customers across multiple tabs.

If i navigate to /99/Admin/User, I can correctly read RouteData.Values["customerid"]. If I use @Url.Action("Index", "Role") in the view, it correctly renders the URL "/99/Admin/Role". Since I'm not jumping across areas, it's smart enough to pass along the customerid route value. However, what if I want to leave to an empty area? For example, using @Url.Action("Index", "Home", new { area = "" }). Of course, this technically correctly renders /Home/Index, but is it possible to have it "remember" and use the customerid route value across areas to render /99/Home/Index?

I know I can use @Url.Action("Index", "Home", new { area = "", customerid = ViewContext.RouteData.Values["customerid"] }) to get the proper path. Or even just create an extension/overload to UrlHelper to sniff the customerid route value. I'm fine with either. Unfortunately, this is an improvement to a large existing application, so making sure to hit every single Url.Action or Html.ActionLink is going to be a daunting task. Albeit, one I think I might have to endure. I was just hoping somebody may know of a way to create a "sticky" route of {domain}/{customerid}/....


Solution

  • You can implement this feature by using custom IActionFilter.

    Test Result

    enter image description here

    enter image description here

    Here is the sample code for you.

    AppendCustomerIdFilter

    using Microsoft.AspNetCore.Mvc.Filters;
    
    namespace WebApplication4
    {
        public class AppendCustomerIdFilter : IActionFilter
        {
            public void OnActionExecuted(ActionExecutedContext context)
            {
                //throw new NotImplementedException();
            }
    
            public void OnActionExecuting(ActionExecutingContext context)
            {
                var routeValues = context.RouteData.Values;
    
                if (!routeValues.ContainsKey("customerid"))
                {
                    var customerid = context.HttpContext.Request.RouteValues["customerid"];
                    if (customerid != null)
                    {
                        context.RouteData.Values["customerid"] = customerid;
                    }
                }
            }
        }
    }
    

    Register it

    using WebApplication4;
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    // Register it
    builder.Services.AddControllersWithViews(options =>
    {
        options.Filters.Add<AppendCustomerIdFilter>();
    });
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/Home/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }
    
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    
    app.UseRouting();
    
    app.UseAuthorization();
    
    //app.MapControllerRoute(
    //    name: "default",
    //    pattern: "{controller=Home}/{action=Index}/{id?}");
    // Change code like below
    app.MapControllerRoute(
        name: "default",
        pattern: "{customerid:int}/{controller=Home}/{action=Index}/{id?}");
    
    app.Run();
    

    My HomeController.cs

    using Microsoft.AspNetCore.Mvc;
    using System.Diagnostics;
    using WebApplication4.Models;
    
    namespace WebApplication4.Controllers
    {
        public class HomeController : Controller
        {
            private readonly ILogger<HomeController> _logger;
    
            public HomeController(ILogger<HomeController> logger)
            {
                _logger = logger;
            }
    
            public IActionResult Index()
            {
                ViewBag.CustomerId = RouteData.Values["customerid"];
                return View();
            }
    
            public IActionResult Privacy()
            {
                return View();
            }
    
            [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
            public IActionResult Error()
            {
                return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
            }
        }
    }
    

    Privacy .cshtml

    @{
        ViewData["Title"] = "Privacy Page";
    }
    
    <h2>Privacy Page</h2>
    <a href="@Url.Action("Index", "Home")">Back to Home</a>
    

    Index.cshtml

    @{
        ViewData["Title"] = "Home Page";
    }
    
    <h2>Customer ID: @ViewBag.CustomerId</h2>
    <a href="@Url.Action("Privacy", "Home")">Privacy Page</a>