1

So, I have a configured a route in my MVC application, that looks like this:

Routing.xml

<route name="note" url="{noteId}-{title}">
     <constraints>
          <segment name="noteId" value="\d+" />
     </constraints>
</route>

Don't pay attention to the XML format.

So, i have a little dash "-" between the segments, it seems MVC Routing Handler has some issues with this.

If i go to an url like /45689-anything-here/, the route is not found.

However, if i change that dash to an underscore "_" in the routing configuration:

{noteId}_{title}

The route is correctly mapped and i can go to /45689_anything-here/

It seems the problem was the dash. Unfortunately, i need to have that dash in the url, do you guys know how to solve this problem?

Thanks in advance!

3 Answers 3

1

I searched for hours without results. The best way I found is to use the Catch All with an ErrorController looping through the mapped routes and testing them the regex way. So when a request has no matching route, it goes in the ErrorController, it is tested my way for all existing routes (so there's no duplicating of the routes) and if it matches, the action is called. So here's the code.

Last route :

routes.MapRoute(
    "NotFound",
    "{*url}",
    new { controller = "Error", action = "PageNotFound" }
);

ErrorController: The PageNotFound action is called by the route. If there's a match, the private Run action is called to run the action with parameters.

public class ErrorController : Controller
{
    private static readonly List<string> UrlNotContains = new List<string>{ "images/" };
    // GET: Error
    public ActionResult PageNotFound(string url)
    {
        if (url == null)
            return HttpNotFound();

        var routes = System.Web.Routing.RouteTable.Routes; /* Get routes */
        foreach (var route in routes.Take(routes.Count - 3).Skip(2)) /* iterate excluding 2 firsts and 3 lasts (in my case) */
        {
            var r = (Route)route;
            // Replace parameters by regex catch groups
            var pattern = Regex.Replace(r.Url, @"{[^{}]*}", c =>
            {
                var parameterName = c.Value.Substring(1, c.Value.Length - 2);
                // If parameter's constaint is a string, use it as the body
                var body = r.Constraints.ContainsKey(parameterName) && r.Constraints[parameterName] is string
                        ? r.Constraints[parameterName]
                        : @".+";
                return $@"(?<{parameterName}>{body})";
            });
            // Test regex !
            var regex = new Regex(pattern);
            Match match = regex.Match(url);
            if (!match.Success)
                continue;
            // If match, call the controller
            var controllerName = r.Defaults["controller"].ToString();
            var actionName = r.Defaults["action"].ToString();
            var parameters = new Dictionary<string, object>();
            foreach(var groupName in regex.GetGroupNames().Skip(1)) /* parameters are the groups catched by the regex */
                parameters.Add(groupName, match.Groups[groupName].Value);

            return Run(controllerName, actionName, parameters);
        }

        return HttpNotFound();
    }
    private ActionResult Run(string controllerName, string actionName, Dictionary<string, object> parameters)
    {
        // get the controller
        var ctrlFactory = ControllerBuilder.Current.GetControllerFactory();
        var ctrl = ctrlFactory.CreateController(this.Request.RequestContext, controllerName) as Controller;
        var ctrlContext = new ControllerContext(this.Request.RequestContext, ctrl);
        var ctrlDesc = new ReflectedControllerDescriptor(ctrl.GetType());

        // get the action
        var actionDesc = ctrlDesc.FindAction(ctrlContext, actionName);
        // Change the route data so the good default view will be called in time
        foreach (var parameter in parameters)
            if (!ctrlContext.RouteData.Values.ContainsKey(parameter.Key))
                ctrlContext.RouteData.Values.Add(parameter.Key, parameter.Value);
        ctrlContext.RouteData.Values["controller"] = controllerName;
        ctrlContext.RouteData.Values["action"] = actionName;

        // To call the action in the controller, the parameter dictionary needs to have a value for each parameter, even the one with a default
        var actionParameters = actionDesc.GetParameters();
        foreach (var actionParameter in actionParameters)
        {
            if (parameters.ContainsKey(actionParameter.ParameterName)) /* If we already have a value for the parameter, change it's type */
                parameters[actionParameter.ParameterName] = Convert.ChangeType(parameters[actionParameter.ParameterName], actionParameter.ParameterType);
            else if (actionParameter.DefaultValue != null) /* If we have no value for it but it has a default value, use it */
                parameters[actionParameter.ParameterName] = actionParameter.DefaultValue;
            else if (actionParameter.ParameterType.IsClass) /* If the expected parameter is a class (like a ViewModel) */
            {
                var obj = Activator.CreateInstance(actionParameter.ParameterType); /* Instanciate it */
                foreach (var propertyInfo in actionParameter.ParameterType.GetProperties()) /* Feed the properties */
                {
                    // Get the property alias (If you have a custom model binding, otherwise, juste use propertyInfo.Name)
                    var aliasName = (propertyInfo.GetCustomAttributes(typeof(BindAliasAttribute), true).FirstOrDefault() as BindAliasAttribute)?.Alias ?? propertyInfo.Name;
                    var matchingKey = parameters.Keys.FirstOrDefault(k => k.Equals(aliasName, StringComparison.OrdinalIgnoreCase));
                    if (matchingKey != null)
                        propertyInfo.SetValue(obj, Convert.ChangeType(parameters[matchingKey], propertyInfo.PropertyType));
                }
                parameters[actionParameter.ParameterName] = obj;
            }
            else /* Parameter missing to call the action! */
                return HttpNotFound();
        }
        // Set culture on the thread (Because my BaseController.BeginExecuteCore won't be called)
        CultureHelper.SetImplementedCulture();

        // Return the other action result as the current action result
        return actionDesc.Execute(ctrlContext, parameters) as ActionResult;
    }
}

Trust me, it's the only good way I found to do that. So the key of success here is that this system uses the parameters constraints to match. So in this case, The problem would be solved because your noteId parameter has a constraint.

Limitations :

  • If there is more than one ambiguous parameter that you can't define by a regex
Sign up to request clarification or add additional context in comments.

Comments

0

This is quite a bit of a guess (admittedly), but could it be due to ambiguity presented by the value in parsing it for segments? I.e. I can see two interpretations of the example:

Interpretation 1:

/45689-anything-here/
 ||||| \\\\\\\\\\\\\
nodeid    title

Interpretation 2:

/45689-anything-here/
 |||||||||||||| \\\\
 nodeid          title

... and so it bums out?

6 Comments

That's a good guess, but note that "noteId" has a constraint "\d+" which means it can only be composed of numeric digits. The second interpretation would not be correct. I'm pretty sure this problem has something to do with ambiguity though
@MatiCicero The thing is, I believe constraints are used in order to accept / reject route (or rather route template). I don't think MVC uses constraints to reject interpretations and try a different interpretation for the same route. Perhaps that's where MVC bails out? Have you tried to use RouteDebugger to see where exactly it rejects it?
I did not try that. I'm gonna try it and come back with news. Thanks!
It seems my routing is working with 123456-mati, but it fails with 123456-mati-cicero. I have a feeling you were right about the using of constraints by MVC
@MatiCicero Oh, and if that doesn't work, then a workaround may be with using URLRewrite module to change dddd-abc-def to dddd/abc-def and then template is easy (unless you use route for link generation, then a different pain comes back).
|
0

This is one way to do it where you parse the route values in the action.

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {            
        routes.MapRoute(
            name: "CatchAll",
            url: "{*catchall}",
            defaults: new { controller = "Home", action = "CatchAll" }
        );
    }
}

public class HomeController : Controller
{       
    public ActionResult CatchAll(string catchall)
    {
        catchall = catchall ?? "null";
        var index = catchall.IndexOf("-");
        if (index >= 0)
        {
            var id = catchall.Substring(0, index);
            var title = catchall.Substring(index+1);
            return Content(string.Concat("id: ", id, " title: ", title));
        }
        return Content(string.Concat("No match: ", catchall));
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.