skip to Main Content

I have 2 routes mapping that can handle the same route, however one is dynamic from CMS and other uses different controller, something like

app.MapControllerRoute(
    name: "Dynamic",
    pattern: "/{urlPath:regex(urlA|urlB)}/{slug}",
    defaults: new { controller = "Controller1", action = "Index" });

 app.MapControllerRoute(
    name: "NonDynamic",
    pattern: "/{slug}/{*path}",
    defaults: new { controller = "Controller2", action = "Index" });

How can I try Controller2 or NonDynamic if Dynamic -> Controller1 returns 404.
Problem is that I don’t know if Controller1 can handle it until I try to handle it and check if CMS has that page, but if not, I want to handle it with completely different controller2
RedirectTo is not an option – I don’t want to have 301 or 302 redirects.
It does also seems to be not possible to use Action from different controller in another controller and keep context.

I could copy all the logic from Controller2 to Controller1, and just use different action based on cms response, but this is a mess and I don’t want to mix those 2 controllers.

Is there a way to return from controller and tell the app to keep looking for another route match ?

2

Answers


  1. Here’s a middleware that you can notify on the way down the pipeline by setting a magic HttpContext.Item. When it detects this, it finds the endpoint you want to execute and sends it back up the middleware pipeline. This is going to break in all kinds of ways, but it seems to work for the specific case in your question.

    public async Task InvokeAsync(HttpContext httpContext, EndpointDataSource endpointDataSource)
    {
        //on the way up, just proceed
        await next(httpContext);
    
        //on the way down, see if we got handed a route name to execute
        if (httpContext.Items["ReexecuteRouteName"] is string routeName)
        {
            httpContext.Items.Remove("ReexecuteRouteName");
    
            //find the endpoint by the given route name
            var endpoint = endpointDataSource.Endpoints.FirstOrDefault(e =>
                e.Metadata.GetMetadata<RouteNameMetadata>()?.RouteName == routeName);
    
            if (endpoint is null)
                return;
    
            //clear previously parsed route values
            var routeValues = httpContext.GetRouteData().Values;
            routeValues.Clear();
    
            //populate with new ones according to the new endpoint
            var route = endpoint.Metadata.GetMetadata<IRouteDiagnosticsMetadata>();
            var routeTemplate = TemplateParser.Parse(route?.Route);
            var templateMatcher = new TemplateMatcher(routeTemplate, new());
            templateMatcher.TryMatch(httpContext.Request.Path, routeValues);
    
            //set the endpoint
            httpContext.SetEndpoint(endpoint);
    
            //go back up the middleware pipeline
            await next(httpContext);
        }
    }
    

    In Controller1:

    public IActionResult Index(string urlPath, string slug)
    {
        var myPage = await db.QueryFirstOrDefaulf<PageStuff>(sql, new { slug });
    
        if (myPage is null) 
        {
            HttpContext.Items.Add("ReexecuteRouteName", "NonDynamic");
            //make sure not to start the response here
            return new NotFoundResult();
        }
    
        return View();
    }
    
    Login or Signup to reply.
  2. Instead of trying to fiddle with custom routing etc. why not have a single controller that handles both cases using an (injected) Chain of Responsibility?

    At the controller level, you might have something like this (I haven’t checked if this compiles, so there may be typos):

    public class MyController
    {
        private readonly IMyService myService;
    
        public MyController(IMyService myService)
        {
            this.myService = myService;
        }
    
        public IActionResult Index(string urlPath, string slug)
        {
            return myService.Handle(urlPath, slug);
        }
    }
    

    You’ll then have (at least) two implementation of the IMyService interface. The first one queries your CMS system:

    public class CmsService : IMyService
    {
        private readonly IMyService next;
    
        public CmsService(IMyService next)
        {
            this.next = next;
        }
    
        public IActionResult Handle(string urlPath, string slug)
        {        
            var result = // Look up the page in CMS
            if (result is null) // or however else you check existence
                return next.Handle(urlPath, slug);
            else
                return new OkResult(result);
        }
    }
    

    The second implementation of IMyService contains your fallback behaviour:

    public class DefaultService : IMyService
    {
        public IActionResult Handle(string urlPath, string slug)
        {
            // Implement default behaviour here...
        }
    }
    

    Finally, you wire up the (small) IMyService Chain of Responsibility like this:

    new CmsService(new DefaultService())
    

    It may be more complex, but it’s less complicated because you won’t be mucking around with exotic framework features, but rather taking advantage of well-understood design patterns.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search