I am using attribute routing and this is how I mapped one of my create actions:
[Route("Create/{companyId:guid?}/{branchId:guid?}/{departmentId:guid?}")]
[Route("Create/{companyId:guid?}/{departmentId:guid?}")]
public async Task<ActionResult> Create(Guid? companyId, Guid? branchId, Guid? departmentId)
I also tried with two different actions that map to the same action name, like this:
[Route("Create/{companyId:guid?}/{branchId:guid?}/{departmentId:guid?}")]
public async Task<ActionResult> Create(Guid? companyId, Guid? branchId, Guid? departmentId)
[Route("Create/{companyId:guid?}/{departmentId:guid?}")]
[ActionName("Create")]
public async Task<ActionResult> CreateWithDepartmentOnly(Guid? companyId, Guid? departmentId)
It's set up like this because there might not be a branchId
and you can't have an empty parameter.
Works OK when only the companyId
is provided, or when both the companyId
and the departmentId
are provided.
When the branchId
is provided, that ID is added as query string, not as a routing parameter. This happens when I had two different actions and also doesn't matter whether the departmentId
is provided.
My code works in all scenarios, I just want to clean up the URL and not have any query strings there. Any solution or workarounds?
EDIT 1
I tried both of Slicksim's ideas.
departmentId
which is the last parameter should have no effect, but instead the branchId
got ignoredEDIT 2
The logic goes like this:
Depending on where you're creating the the department from, those IDs are in the route. Is this a viable solution?
[Route("Create/{companyId:guid}")]
[Route("Create/{companyId:guid}/{branchId:guid?}")]
[Route("Create/{companyId:guid}/{departmentId:guid?}")]
[Route("Create/{companyId:guid}/{branchId:guid?}/{departmentId:guid?}")]
public async Task<ActionResult> Create(Guid companyId, Guid? branchId, Guid? departmentId)
Are the routes 2 and 3 going to work since they both have same number and type of parameters. If the Id is placed in the correctly named parameters in the action then I'm golden.
Check the following:
routes.MapMvcAttributeRoutes()
in your RouteConfig (or somewhere in your application startup).routes.MapMvcAttributeRoutes()
before the default route.public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// Add this to get attribute routes to work
routes.MapMvcAttributeRoutes();
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
Also, you are shooting yourself in the foot by making the parameters optional. Only 1 optional parameter can be used on a route and it must be the right most parameter. The second route here is an unreachable execution path because all 3 of the first parameters are optional.
[Route("Create/{companyId:guid?}/{branchId:guid?}/{departmentId:guid?}")]
[Route("Create/{companyId:guid?}/{departmentId:guid?}")]
Technically, you can do what (I think) you want with a single route.
[Route("Create/{companyId:guid}/{departmentId:guid}/{branchId:guid?}")]
public async Task<ActionResult> Create(Guid companyId, Guid departmentId, Guid? branchId)
Which will match
/Create/someCompanyId/someDepartmentId/someBranchId
/Create/someCompanyId/someDepartmentId
If you do in fact want all parameters to be optional, the best way is to use the query string. Routes aren't designed for that sort of thing, but if you really want to go there using routing, see this answer.
There is nothing wrong with removing the default route. However, your business logic is a bit too complex for the default routing to work. You are correct in your assessment that the 2nd and 3rd routes are indistinguishable from one another (even if you made the parameters required) because the patterns are the same.
But .NET routing is more flexible than what MapRoute
or the Route
attribute can provide alone.
Also, if these URLs are Internet facing (not behind a login), this is not an SEO friendly approach. It would be far better for search engine placement to use company, branch, department, and subdepartment names in the URL.
Whether or not you decide to stick with the GUID scheme, you have a unique set of URLs, which is all that is required to get a match. You would need to load each of the possible URLs into a cache that you can check against the incoming request, and then you can supply the route values manually by creating a custom RouteBase as in this answer. You just need to change the PageInfo
object to hold all 3 of your GUID values and update the routes accordingly. Do note however it would be better (less logic to implement) if you did a table join and came up with a single GUID to represent each URL as in the linked answer.
public class PageInfo
{
public PageInfo()
{
CompanyId = Guid.Empty;
BranchId = Guid.Empty;
DepartmentId = Guid.Empty;
}
// VirtualPath should not have a leading slash
// example: events/conventions/mycon
public string VirtualPath { get; set; }
public Guid CompanyId { get; set; }
public Guid BranchId { get; set; }
public Guid DepartmentId { get; set; }
}
GetRouteData
result.Values["controller"] = "CustomPage";
result.Values["action"] = "Details";
if (!page.CompanyId.Equals(Guid.Empty))
{
result.Values["companyId"] = page.CompanyId;
}
if (!page.BranchId.Equals(Guid.Empty))
{
result.Values["branchId"] = page.BranchId;
}
if (!page.DepartmentId.Equals(Guid.Empty))
{
result.Values["departmentId"] = page.DepartmentId;
}
TryFindMatch
Guid companyId = Guid.Empty;
Guid branchId = Guid.Empty;
Guid departmentId = Guid.Empty;
Guid.TryParse(Convert.ToString(values["companyId"]), out companyId);
Guid.TryParse(Convert.ToString(values["branchId"]), out branchId);
Guid.TryParse(Convert.ToString(values["departmentId"]), out departmentId);
var controller = Convert.ToString(values["controller"]);
var action = Convert.ToString(values["action"]);
if (action == "Details" && controller == "CustomPage")
{
page = pages
.Where(x => x.CompanyId.Equals(companyId) &&
x.BranchId.Equals(branchId) &&
x.DepartmentId.Equals(departmentId))
.FirstOrDefault();
if (page != null)
{
return true;
}
}
return false;