7

I need to map two variables that could contain slashes, to a controller, in my ASP MVC application. Let's see this with an example.

enter image description here

  • Repository and Path will be URL-encoded parameters.
  • Repository can have 0 slashes or 1 slash as a maximum (rep or rep/module)
  • Path can have an arbitrary number of slashes.

For example these are valid URLs:

http://mysite/rep/Items
http://mysite/rep/module/Items/foo/bar/file.c

Someone could give some suggestions about how to define this route?

asked Jul 24, 2014 at 11:32
4
  • Does it have to be that exact URL, or can those parameters be URL-encoded? (I'm guessing the former, since the latter is a lot easier. But it's worth explicitly asking.) Commented Jul 24, 2014 at 11:34
  • If you can't URL encode, the only thing you can do is take the entire URL as a string and parse it yourself. Commented Jul 24, 2014 at 11:40
  • 2
    If they're URL-encoded, then the actual URL would be: http://mysite/rep%2Fmodule/Items/foo%2Fbar.c I'm not 100% sure on the routing since I haven't done much with routes, but that change to the URL makes a significant difference. When they're URL-encoded the slashes really don't matter anymore because they're not slashes anymore. Commented Jul 24, 2014 at 11:41
  • @David: The following route works, "{repository+}/Items/{*path}", new { controller = "Items", action = "Index", path = "/" } but it forms an URL with old fashion parameters http://mysite/Items?repository=rep%2fmodule Commented Jul 24, 2014 at 12:18

4 Answers 4

2

Looks like a custom route might cut the mustard:

public class MyRoute: Route
{
 public MyRoute()
 : base("{*catchall}", new MvcRouteHandler())
 {
 }
 public override RouteData GetRouteData(HttpContextBase httpContext)
 {
 var rd = base.GetRouteData(httpContext);
 if (rd == null)
 {
 // we do not have a match for {*catchall}, although this is very
 // unlikely to ever happen :-)
 return null;
 }
 var segments = httpContext.Request.Url.AbsolutePath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
 if (segments.Length < 4)
 {
 // we do not have the minimum number of segments
 // in the url to have a match
 return null;
 }
 if (!string.Equals("items", segments[1], StringComparison.InvariantCultureIgnoreCase) &&
 !string.Equals("items", segments[2], StringComparison.InvariantCultureIgnoreCase))
 {
 // we couldn't find "items" at the expected position in the url
 return null;
 }
 // at this stage we know that we have a match and can start processing
 // Feel free to find a faster and more readable split here
 string repository = string.Join("/", segments.TakeWhile(segment => !string.Equals("items", segment, StringComparison.InvariantCultureIgnoreCase)));
 string path = string.Join("/", segments.Reverse().TakeWhile(segment => !string.Equals("items", segment, StringComparison.InvariantCultureIgnoreCase)).Reverse());
 rd.Values["controller"] = "items";
 rd.Values["action"] = "index";
 rd.Values["repository"] = repository;
 rd.Values["path"] = path;
 return rd;
 }
}

which could be registered before the standard routes:

public static void RegisterRoutes(RouteCollection routes)
{
 routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 routes.Add("myRoute", new MyRoute());
 routes.MapRoute(
 "Default", // Route name
 "{controller}/{action}/{id}", // URL with parameters
 new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
 );
}

And if you intend to put arbitrary strings in the path portion of your urls I hope you are aware of the Zombie Operating Systems which might surprise you.

answered Jul 24, 2014 at 15:56
Sign up to request clarification or add additional context in comments.

4 Comments

Wouldn't "myRoute" then simply become the default route? It would catch every single URL.
Ah. Sorry. I just noticed the return null in the case that it's determined it doesn't match the catch all route you're trying to parse.
@Darin Dimitrov: Thanks for your answer. I suspect that this solution is not generic. Could be possible to implement this for ANY controller and ANY action?
@DanielPeñalba, of course that it is possible. As you can see I have hardcoded the controller and action route values, but you can set them to whatever value you want.
2

Finally, based in the answer of Darin Dimitrov, I implemented the following custom route, that solves my problem:

public class RepositoryRoute : Route
{
 public RepositoryRoute(string name, string url, object defaults)
 : base(url, new RouteValueDictionary(defaults), new MvcRouteHandler())
 {
 string moduleUrl = url.Replace(
 REPOSITORY_PARAMETER, REPOSITORY_PARAMETER + MODULE_PARAMETER);
 mModuleRoute = new Route(
 moduleUrl, new RouteValueDictionary(defaults), new MvcRouteHandler());
 }
 public override RouteData GetRouteData(HttpContextBase httpContext)
 {
 RouteData rd = mModuleRoute.GetRouteData(httpContext);
 if (rd == null)
 return base.GetRouteData(httpContext);
 if (!rd.Values.ContainsKey(MODULE))
 return rd;
 // set repository as repo/submodule format
 // if a submodule is present in the URL
 string repository = string.Format("{0}/{1}",
 rd.Values[REPOSITORY],
 rd.Values[MODULE]);
 rd.Values.Remove(MODULE);
 rd.Values[REPOSITORY] = repository;
 return rd;
 }
 Route mModuleRoute;
 const string REPOSITORY = "repository";
 const string MODULE = "module";
 const string REPOSITORY_PARAMETER = "{" + REPOSITORY + "}/"; // {repository}/
 const string MODULE_PARAMETER = "{" + MODULE + "}/"; // {module}/
}

Which is registered in the following way:

 routes.Add(new RepositoryRoute(
 "Items",
 "{repository}/Items/{*path}",
 new { controller = "Items", action = "Index", path = "/" }
 ));

The route uses an internal route, that defines a module parameter, and if it's found, I concat it to the repository, and remove it. So mapping repository or repository/module is transparent.

answered Jul 25, 2014 at 7:48

Comments

0

If you cannot live with "old fashioned" style parameters and URL encoding, I think the only way you can achieve it is like this. Note that this is not tested but should basically work. Also I've put the controller name at the start and the Items separator is now essentially meaningless apart from acting as a delimiter.

Controller

Create a controller with a single, parameterless method:

public class GetRepo : Controller
{
 public ActionResult Index()
 {
 //TBC
 return View();
 }
}

Routing

Ensure routing is set up to allow http://www.example.com/GetRepo/ anything to route to your index method.

Note that the GetRepo part is important as otherwise what happens if your URL is www.example.com/blah/repo/items/other/stuff and you happen to have a controller called blah?

The Magic

Now you deconstruct the Url manually by using Request.Url.AbsolutePath.

var urlPath = Request.Url.AbsolutePath;
//Split the path by '/', drop the first entry as it's the action method
var parts = urlPath.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries)
 .Skip(1).ToList();
//Find where "Items" separator appears:
var posOfItems = parts.IndexOf("Items");
//Everything before separator is the repo:
var repo = string.Join("/", parts.Take(posOfItems));
//Everything after separator is the path:
var path = string.Join("/", parts.Skip(posOfItems + 1));
//Now do something with repo/path variables
answered Jul 24, 2014 at 14:31

3 Comments

Thanks for the answer. This solution is not valid for me, beacuse we need to maintain compatibility with old URLs. Old URL only support repos (without modules). I just need that if you add a module to the repo (repo/module) it must just work in the same way. I'm breaking my head to figure out how to do it.
well you may be able to get away with it provided you have a single controller and nothing else in the solution that may conflict.
Or at least you are confident that a repo path won't conflict with a controller name.
-1

You cannot do this mapping correctly, because of nature of this problem. Try to get a pencil and map following URL to repository and path:

http://mysite/rep/Items/Items/Items/Items/Items/Items/Items/Items/Items

There are multiple mappings:

1) Repository = Items Path = Items/Items/Items/Items/Items/Items/Items

2) Repository = Items/Items Path = Items/Items/Items/Items/Items/Items

and so on....

So either you should

  1. Pass parameters as query string
  2. Define multiple routes for each of format of repository (and add parts to full repository name in controller method)
answered Jul 24, 2014 at 12:45

1 Comment

The Items is a constant (is the controller name) so you can discretize: all chunks on the left of Controller name is the repository, and all chunks on the right of the Controller name, is the path.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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.