Deep nesting with Restful routes?

Imported from the CodePlex archive for reference purposes. Support for MvcCodeRouting has ended.

Commented on

Hello, great work on this so far!

I'd like to have routes like this:

 

Root: Agents

Agents/Registration/55/Personnel

This would mean I've navigated into the Registration with id 55 to see the list of Personnel.

Agents/Registration/55/Personnel/12

Then, within that scope, I've selected Personnel of ID 12.

How can I achieve this? I tried a few things:

        [CustomRoute("{id}")]
        public ActionResult Personnel([FromRoute]int id)
        {
            return Content("Id: " + id.ToString());
        }

 

This resulted in /Agents/Registration/Personnel/55 working but not what I wanted above.

 

When I try this way, it does work.... But this is at a "higher" level in the namespace tree.

namespace App.Modules.Agents.Controllers.Registration

    public class RegistrationController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        [CustomRoute("{id}/Personnel")]
        public ActionResult Personnel([FromRoute]int id)
        {
            return Content("Id: " + id.ToString());
        }

I'd like to be ale to have it like this:

 

namespace Modules.Agents.Controllers.Registration.Personnel
{
    public class PersonnelController : Controller
    {

        [CustomRoute("{personnelId}")]
 

        // Also tried: [CustomRoute("{registrationId}/Personnel/{personnelId}")]
 

       //  I thought maybe this would work: [CustomRoute("~/Agents/Registration/{registrationId}/Personnel/{personnelId}")]


        public ActionResult Detail([FromRoute]int registrationId, [FromRoute]int personnelId)
        {
            return Content("registrationId:" + registrationId.ToString() + " and personnel Id:" + personnelId.ToString());
        }

   }
}

I was thinking that because Personnel is "nested" within the Registration namespace, that registrationId would inherit, then I could just specify the personnelId in my additional custom info.

That did not work, so I thought maybe I could use a ~/ to tell it to use an "absolute" path for it.

I did try to dig into the code, but it's too much to take in at the moment. Maybe you could give me some tips where to start if I wanted to implement the ~/ "absolutification" support.....or something more elegant and inherited from the "parent" route.

 

Thanks!

Commented on link

Here was a quick hacked up way to make what I need work:

    public class PersonnelController : Controller
    {
        [CustomRoute("~/Agents/Registration/{registrationId}/Personnel/{personnelId}")]
        public ActionResult Detail([FromRoute]int registrationId, [FromRoute]int personnelId)
        {
            return Content("registrationId:" + registrationId.ToString() + " and personnel Id:" + personnelId.ToString());
        }

   }

--------------

      public static ICollection<Route> MapCodeRoutes(this RouteCollection routes, string baseRoute, Type rootController, CodeRoutingSettings settings) {

         if (routes == null) throw new ArgumentNullException("routes");
         if (rootController == null) throw new ArgumentNullException("rootController");

         var registerInfo = new RegisterInfo(null, rootController) {
            BaseRoute = baseRoute,
            Settings = settings
         };

         Route[] newRoutes = RouteFactory.CreateRoutes(registerInfo);

         foreach (var route in newRoutes)
         {
             // TODO: JSG Hack remove
             if (route.Url.Contains("~/"))
             {
                 var fixedUpUrl = route.Url.Substring(
                     route.Url.IndexOf("~/") + 2);
                 route.Url = fixedUpUrl;
             }
             routes.Add(route);
         }

Routes.axd shows:

routes.MapRoute(null, "Agents/Registration/{registrationId}/Personnel/{personnelId}", new { controller = @"Personnel", action = @"Detail" }, new { registrationId = @"0|-?[1-9]\d*", personnelId = @"0|-?[1-9]\d*" }, new[] { "Modules.Agents.Controllers.Registration.Personnel" }); // MvcCodeRouting.RouteContext: "Agents/Registration"

I like this feature, and dislike my implementation :)

If you tell me a better place to achieve this, I will submit a patch.

I kind of do like the ~/ for the mechanism, though maybe an "IsAbsolute" property on CustomRoute would be better? (Or maybe both should be supported?)

Josh

Commented on link

Me again.

Here is where I believe a better place would be for the above functionality:

What do you think? Or, am I just missing a way to do this already? :)

 

RouteFactory.cs:

      static CodeRoute CreateRoute(IEnumerable<ActionInfo> actions) {

........


         bool actionFormat = actionMapping.Any(p => !String.Equals(p.Key, p.Value, StringComparison.Ordinal));
         bool requiresActionMapping = actionFormat && includeActionToken;

         string url = string.Empty;

         if (first.CustomRoute != null) {
             if (first.CustomRoute.Contains("~/"))
             {
                 var absoluteUrl = first.CustomRoute.Substring(
                     first.CustomRoute.IndexOf("~/") + 2);
                 url = absoluteUrl;
             }
             else
             {
                 segments.Add(first.CustomRoute);
             }
         } else {
            segments.Add(!includeActionToken ? first.ActionSegment : String.Concat("{action}"));
            segments.AddRange(first.RouteParameters.Select(r => r.RouteSegment));
         }

         if (url == string.Empty)
         {
             url = String.Join("/", segments.Where(s => !String.IsNullOrEmpty(s)));
         }

 

 

Commented on link

Thanks for the idea. Using a custom route that starts with ~/ is a very elegant solution to the problem, and makes MvcCodeRouting an alternative to other attribute-based routing libraries.

This is now implemented in the latest revision, it only required a small modification. I will continue to test it and see how it affects the rest of the features, I'm confident this will make the next release.

Commented on link

To answer the original question, the only way to do it is to put the action in the Registration controller and use a custom route, like this:

namespace App.Modules.Agents.Controllers.Registration
{
    public class RegistrationController : Controller
    {
        [CustomRoute("{id}/{action}/{personnelId}")]
        public ActionResult Personnel([FromRoute]int id, [FromRoute]int personnelId)
        {
            ...
        }
    }
}
Commented on link

Ok, thank you, I think I did try something to the effect successfully, but really wanted it in the nested controller instead, hence the tilde solution.

The thing I dislike about the tilde solution is the need to hard code the "parent" paths...so I imagine with the segments approach you have I could figure a "relative path" way, perhaps something like:

CustomRoute("../{registrationId}/Personnel/{personnelId}")

In this, it would just build route all the way to it's parent, then tack on the rest.

But, I could even generalize this:

I am thinking an attribute like:

[Inherit]
ActionResult Personnell(int registrationId, int personnelId)

Then, given that the parent has:

/Root/Parent/{registrationId}

(and that maps to some action)

When [Inherit] is there, it will

Thus build a route that is, by convention:

/Agent/Registration/{registrationId}/Personnel/{personnelId}

Now......if such a mechanism could be multiply nested to:

/Agents/Registration/45/Personnel/2/ContactInfo/Home/Email

Where "Home" would actually be an "id" of sorts, as would "Email" ......

well..I am not sure about that, but I will give the simple case a shot :)

On Jun 5, 2012 10:35 PM, "maxtoroq" <notifications@codeplex.com> wrote:

From: maxtoroq

To answer the original question, the only way to do it is to put the action in the Registration controller and use a custom route, like this:

namespace App.Modules.Agents.Controllers.Registration
{
    public class RegistrationController : Controller
    {
        [CustomRoute("{id}/{action}/{personnelId}")]
        public ActionResult Personnel([FromRoute]int id, [FromRoute]int personnelId)
        {
            ...
        }
    }
}

Read the full discussion online.

To add a post to this discussion, reply to this email (mvccoderouting@discussions.codeplex.com)

To start a new discussion for this project, email mvccoderouting@discussions.codeplex.com

You are receiving this email because you subscribed to this discussion on CodePlex. You can unsubscribe on CodePlex.com.

Please note: Images and attachments will be removed from emails. Any posts to this discussion will also be available online at CodePlex.com

Commented on link

I tried the "InheritRoute" idea, and it works...but is very limited in scope (But kind of works for what I need at the moment :)  )

 

namespace Nsar.Modules.SelectAgentData.Controllers.Registration.Personnel
{
    public class PersonnelController : Controller
    {
        [InheritRoute]
        public ActionResult Detail(int registrationId, int personnelId)
        {
            return Content("registrationId:" + registrationId.ToString() + " and personnel Id:" + personnelId.ToString());
        }
   }
}

Produces this route:

http://localhost:12795/SelectAgentData/Registration/55/Personnel/Detail/44

And int RouteFactory above the previous bit of code:

        if (first.GetCustomAttributes(typeof(InheritRouteAttribute), false).Length > 0)
        {
            var rootUrl = segments[0].Substring(0, segments[0].LastIndexOf("/"));
            var controllerName = segments[0].Substring(segments[0].LastIndexOf("/") + 1);
            var paramIndex = 0;

            if (first.Parameters.Count > 1)
            {
                url += string.Format("/{0}", "{" + first.Parameters[0].Name + "}");
                url += string.Format("/{0}", controllerName);
                url += string.Format("/{0}", first.ActionSegment);
                paramIndex = 1;
            }

            url += string.Format("/{0}", "{" + first.Parameters[paramIndex].Name + "}");

            url = rootUrl + url;
        }
        else if  (first.CustomRoute != null) {
             if (first.CustomRoute.Contains("~/"))
             {

 

 

 

 

Commented on link

Your InheritRoute solution isn't general, since your are asuming the first action parameter goes before the controller token.

Commented on link

Yeah...any ideas for making it general?

Commented on link

CustomRoute("../{registrationId}/{controller}/{personnelId}") doesn't look bad, but I'm afraid to overcomplicate things. Should be easy to implement though.

Another option, that is consistent to how CustomRoute works for actions methods, is to use CustomRoute on the controller, like this:

namespace Nsar.Modules.SelectAgentData.Controllers.Registration.Personnel {

   [CustomRoute("{registrationId}/{controller}")]
   public class PersonnelController : Controller {
      
      [FromRoute]
      public int registrationId { get; set; }

      protected override void Initialize(RequestContext requestContext) {
         this.BindRouteProperties();
      }

      [CustomRoute("{id}")]
      public ActionResult Details([FromRoute]int id) {
         ...
      }
   }
}

Commented on link

I've implemented the CustomRoute on controllers, it works very well. Give it a try.

Commented on link

Thanks! I downloaded, and tried, but I'm not sure how to use it properly...

I get exception here:

ControllerContext is null:

      bindingContext.ValueProvider = (hasCustomValues) ?
            new DictionaryValueProvider<object>(values, CultureInfo.InvariantCulture)
            : new RouteDataValueProvider(controller.ControllerContext);

----

Controller:

    [CustomRoute("{registrationId}/{controller}")]
    public class PersonnelController : NsarControllerBase
    {
        [FromRoute]
        public int RegistrationId { get; set; }

        protected override void Initialize(RequestContext requestContext)
        {
            this.BindRouteProperties();
        }

        [CustomRoute("{id}")]
        public ActionResult Details([FromRoute] int id)
        {
            return Content("registrationId:" + RegistrationId.ToString() + " and personnel Id:" + id.ToString());
        }
    }

 

Routes.axd

routes.MapRoute(null, "SelectAgentData/Registration/{registrationId}/{controller}/{id}", new { action = @"Details" }, new { controller = @"Personnel", RegistrationId = @"0|-?[1-9]\d*", id = @"0|-?[1-9]\d*" }, new[] { "Nsar.Modules.SelectAgentData.Controllers.Registration.Personnel" }); // MvcCodeRouting.RouteContext: "SelectAgentData/Registration"

Commented on link

You have to call the base Initialize method which sets ControllerContext:

protected override void Initialize(RequestContext requestContext)
{
    base.Initialize(requestContext);
    this.BindRouteProperties();
}


Commented on link

Ok, I will try, thanks!

I think this should do what I need, but am wondering, will it work with deeper nesting?

Suppose I have:

/SelectAgentData/Registrations/55/Labs/5/Agents/22

Or:

/SelectAgentData/Registrations/55/Labs/5/Personnel/12/Clearances/5

Josh

Commented on link

No, the only way to go deeper, without using an absolute custom route, is to implement them on the Labs controller and use literal segments instead of namespaces:

[CustomRoute("{registrationId}/{controller}")]
public class LabsController : Controller {
   
   [FromRoute]
   public int RegistrationId { get; set; }

   [CustomRoute("{id}/Personnel/{personnelId}/Clearances/{clearanceId}")]
   public ActionResult Clearance(...) {
      ...
   }
}

I don't know what kind of app you are working on, but I've never had the need for such a deep URL hierarchy, personally I would just go back to the root at some point, e.g. /Personnel/{personnelId}/Clearances/{clearanceId}, assuming those are unique identifiers.