Using Url.RouteUrl() with Route Names in an Area - asp.net-mvc-4

As a side note, I understand the whole ambiguous controller names problem and have used namespacing to get my routes working, so I don't think that is an issue here.
So far I have my project level controllers and then a User Area with the following registration:
public class UserAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "User";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"UserHome",
"User/{id}",
new { action = "Index", controller = "Home", id = 0 },
new { controller = #"Home", id = #"\d+" }
);
context.MapRoute(
"UserDefault",
"User/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
The "UserHome" route is there so I can allow the route /User/5 vs. /User/Home/Index/5 which looks cleaner IMO.
Ideally I would like to use Url.RouteUrl("UserHome", new { id = 5 }), to generate the route elsewhere, but this always either comes back blank or gives me an exception saying it cannot find the route name, which is obviously there.
However when I use Url.RouteUrl("UserHome", new { controller = "Home", action = "Index", id = 5 }) it works no problem.
Why do I have to specify the action and controller when they have defaults already in the route mapping? What am I missing?

Everyone please see #ryanulit's answer below. This issue may be fixed for you with a newer framework version.
Not sure if there has been a hot fix, but the behavior now is a bit different.
Using your exact code and trying:
Url.RouteUrl("UserHome", new { id = 5 })
I now get:
/User/5?httproute=True
This still looks awkward, so I experimented with the route and added another default param:
context.MapRoute(
"UserHome",
"User/{id}",
new { action = "Index", controller = "Home", area = "User", id = 0,
httproute = true },
new { controller = #"Home", id = #"\d+" }
);
Now when I use
Url.RouteUrl("UserHome", new { id = 5 })
I get a nice url of
/User/5
disclaimer There could be unwanted side effects of httproute=true in the route declaration.
Also, the more verbose use:
#Url.RouteUrl("UserHome", new { controller = "Home", action = "Index", id = 5 })
still works as well.

Try this:
#Url.Action("Index", "Home", new { Area = "User" })

I can confirm that with .NET 4.5.1 and MVC 5.2.2 at the minimum, that this behavior has been fixed and now works as is with this same exact code using Url.RouteUrl("UserHome", new { id = 5 }).
It looks like this was a bug that has been fixed since the time of my post.
Adding this as the answer since although TSmith's solution would work, you no longer need to do that extra work now that there is a fix.

Related

How to add tenant / organization id to every URL and read it inside controller

I am writing an ASP.NET Core web application. In my application, the user can be a member of one or more organizations (this information is stored in the DB). I would like to give the users the possibility to select the context of organization in which the whole app should be running. This context should be passed to every controller so that I can do proper checks and return only the data from the selected org.
In other words, I would like to achieve the state when the user can access the app via:
app.mydomain.com/org1/controller/action/id and using a dropdown they should be able to switch to org2 and access app.mydomain/org2/controller/action/id
In both cases I need to be able to read the organization inside the controller. This should apply to every controller.
How should this be done? Is it possible to construct a route that would handle it and just add new parameter to every controller, e.g. orgId? Or maybe there should be a service that reads this information from the URL and can be injected into controllers? How would the route look then? I have read some tutorials about handling multiple languages in a similar way (with the culture in URL) but I am not able to translate that into my case.
For how to pass the select org id to the controller:
View:
<form action="/Orgnization/Test">
<select id="select" name="select" asp-items="#ViewBag.Sel"></select>
</form>
#section Scripts
{
<script>
$(document).ready(function () {
$("#select").change(function () {
$('form').submit();
});
})
</script>
}
Configure selectlist:
public class HomeController : Controller
{
public IActionResult Index()
{
//for easy testing,I just set it manually...
ViewBag.Sel = new List<SelectListItem>() {
new SelectListItem() { Value = "-1", Text = "--- Select ---" },
new SelectListItem() { Value = "org1", Text = "org1" },
new SelectListItem() { Value = "org2", Text = "org2" },
new SelectListItem() { Value = "org3", Text = "org3" },
};
return View();
}
}
Controller:
public class OrgnizationController : Controller
{
public IActionResult Test(string select)
{
//the select is the value you choose
//do your stuff....
return View();
}
}
If you want to add prefix to the route and get it in the controller:
Startup.cs:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{orgid?}/{controller=Home}/{action=Privacy}/{id?}");
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Privacy}/{id?}");
});
Controller:
public class OrgnizationController : Controller
{
public IActionResult Test(string select)
{
var org = "";
if (select==null)
{
var path = Request.Path.ToString();
org = path.Split('/')[1];
}
//do your stuff....
return View();
}
}
Result:

how to render date part segments with anchor taghelper in ASP.NET Core

I'm trying to make a blog with a setting that allows a choice between 2 url formats for posts, one with the date as segments and one with only the slug. I want users to be able to choose either of these settings for their blog and it should not require changes in startup code to switch back and forth. In fact I'm trying to support multi-tenant blogs so each blog can have its own url format preference.
I have the following routes defined in Startup.cs
routes.MapRoute(
name: "blogcategory",
template: "blog/category/{category=''}/{pagenumber=1}"
, defaults: new { controller = "Blog", action = "Category" }
);
routes.MapRoute(
"blogarchive",
"blog/{year}/{month}/{day}",
new { controller = "Blog", action = "Archive", month = "00", day = "00" },
new { year = #"\d{4}", month = #"\d{2}", day = #"\d{2}" }
);
routes.MapRoute(
"postwithdate",
"blog/{year}/{month}/{day}/{slug}",
new { controller = "Blog", action = "PostWithDate" },
new { year = #"\d{4}", month = #"\d{2}", day = #"\d{2}" }
);
routes.MapRoute(
name: "blogpost",
template: "blog/{slug}"
, defaults: new { controller = "Blog", action = "Post" }
);
routes.MapRoute(
name: "blogindex",
template: "blog/"
, defaults: new { controller = "Blog", action = "Index" }
);
routes.MapRoute(
name: "pageindex",
template: "{slug=none}"
, defaults: new { controller = "Page", action = "Index" }
);
routes.MapRoute(
name: "def",
template: "{controller}/{action}"
);
routes.MapRoute(
"postwithdate",
"blog/{year}/{month}/{day}/{slug}",
new { controller = "Blog", action = "PostWithDate" },
new { year = #"\d{4}", month = #"\d{2}", day = #"\d{2}" }
);
My Blog controller has these methods related to the post routes
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> Post(string slug, string mode = "")
{
return await Post(0, 0, 0, slug, mode);
}
[HttpGet]
[AllowAnonymous]
[ActionName("PostWithDate")]
public async Task<IActionResult> Post(int year , int month, int day, string slug, string mode = "")
{
...
}
If I manually navigate to
http://localhost:60000/blog/2016/03/07/squirrel
the page works as expected
in my view I'm rendering the link to the post like this with the anchor tag helper
#if (Model.ProjectSettings.IncludePubDateInPostUrls)
{
<a asp-controller="Blog" asp-action="Post"
asp-route-year="#Model.TmpPost.PubDate.Year"
asp-route-month="#Model.TmpPost.PubDate.Month"
asp-route-day="#Model.TmpPost.PubDate.Day"
asp-route-slug="#Model.TmpPost.Slug"
itemprop="url">#Model.TmpPost.Title</a>
}
else
{
<a asp-controller="Blog" asp-action="Post" asp-route-slug="#Model.TmpPost.Slug" itemprop="url">#Model.TmpPost.Title</a>
}
but when I configure it to use the pubDate in the url it renders the link like this:
http://localhost:60000/blog/squirrel?year=2016&month=3&day=7
That url also works but how can I make it render like this:?
http://localhost:60000/blog/2016/03/07/squirrel
I tried also using a named route with the taghelper instead of controller and action, like this:
<a asp-route="postwithdate"
asp-route-year="#Model.TmpPost.PubDate.Year"
asp-route-month="#Model.TmpPost.PubDate.Month"
asp-route-day="#Model.TmpPost.PubDate.Day"
asp-route-slug="#Model.TmpPost.Slug"
itemprop="url">#Model.TmpPost.Title</a>
but that one renders completely wrong without even the slug like this:
http://localhost:60000/blog
I want to make the tagHelper render the url like this:
http://localhost:60000/blog/2016/03/07/squirrel
Can anyone see what I'm doing wrong or not doing right? Or would I need to implement a custom anchor taghelper for this?
ok, I found a solution using the named route, just need to make sure the month and day get formatted as 2 digits, after doing that it now renders as I wanted with the date segments
<a asp-route="postwithdate"
asp-route-year="#Model.TmpPost.PubDate.Year"
asp-route-month="#Model.TmpPost.PubDate.Month.ToString("00")"
asp-route-day="#Model.TmpPost.PubDate.Day.ToString("00")"
asp-route-slug="#Model.TmpPost.Slug"
itemprop="url">#Model.TmpPost.Title</a>

Differentiating Between MVC Routes

I have two similar seo friendly routes going to different controllers/actions:
routes.MapRoute(
"Locations",
"{controller}/{locationId}/{location}",
new { controller = "Locations", action = "Location", location = UrlParameter.Optional },
new { locationId = #"\d+" }
);
routes.MapRoute(
"News",
"{controller}/{newsId}/{newsTitle}",
new { controller = "News", action = "NewsItem", newsTitle = UrlParameter.Optional },
new { newsId = #"\d+" }
);
The News route returns a 404 error and visa versa if I swap them. I tried adding a default controller to each - this did nothing. I then tried replacing {controller} with the actual name of the controller. This generated a
The matched route does not include a 'controller' route value, which
is required
error message.
Here's a link for each:
#Html.ActionLink(x.newsTitle, "NewsItem", "News", new { newsId = x.newsID, newsTitle = x.newsTitle.ToSeoUrl() },null)
<i class="fa fa-map-marker fa-li red"></i>My Place Name
I 'fixed it' by switching locationId and location querystring elements. OK so it works, but I feel this isn't the right solution. What if I have a third controller action with a similar querystring pair?
A final attempt was to make the route more generic:
routes.MapRoute(
"SEO",
"{controller}/{Id}/{Title}",
new { Title = UrlParameter.Optional },
new { Id = #"\d+" }
);
Bu then I can't pass the action and I'd be back to a less SEO friendly URL.
In the end I would like to end up with:
Locations/locationId/my-place-name
News/newsId/my-news-item
I'm relatively new to MVC and I'm sure I'm missing something fundamental (or just being dumb).
The problem is that a URL for both actions will match the first route you have added. For example, this URL:
http://yoursite.com/News/123/some-news-article
Will attempt to call the action Locations in your News controller, which obviously doesn't exist and gives you a 404 error.
You can get around this by specifying the path in the URL of the route instead of using {controller}. For example:
routes.MapRoute(
"Locations",
"Locations/{locationId}/{location}",
new { controller = "Locations", action = "Location", location = UrlParameter.Optional },
new { locationId = #"\d+" }
);
routes.MapRoute(
"News",
"News/{newsId}/{newsTitle}",
new { controller = "News", action = "NewsItem", newsTitle = UrlParameter.Optional },
new { newsId = #"\d+" }
);

MVC Routing News Pages

I have managed to get my MVC project to present a list of news items in an SEO friendly manner:
/News/ - to present the list
/News/NewsItem/id/news-item-title - the individual news item
What I would really like is:
News/id/news-item-title
Exactly how Stackoverflow presents its questions.
However, I cant seem to get my head around how to do the routing to differentiate between two actions with the same controller action name (Index).
Any suggestions would be appreciated.
EDIT:
Here's my routes config:
routes.MapRoute(
"News",
"News/NewsItem/{newsId}/{newsTitle}",
new { controller = "News", action = "NewsItem", newsTitle = UrlParameter.Optional },
new { newsId = #"\d+" }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "SkipHire", action = "Index", id = UrlParameter.Optional }
);
EDIT 2:
This is what I've amended everything to:
Route
routes.MapRoute(
"News",
"{controller}/{id}/{newsTitle}",
new { action = "NewsItem", newsTitle = UrlParameter.Optional }
);
Controller
public class NewsController : Controller
{
public ActionResult Index()
{
var q = _ctx.tblNews.OrderBy(x => x.newsCreateDate)
.Where(x => x.WebsiteID == 2).ToList();
return View(q);
}
public ActionResult NewsItem(int newsId, string newsTitle)
{
return View();
}
}
View - Index (Segment)
<table>
#foreach (var x in Model)
{
<tr>
<td>#Html.ActionLink(x.newsTitle, "NewsItem", new { newsId = x.newsID, newsTitle = x.newsTitle.ToSeoUrl() })
</td>
</tr>
}
</table>
Actionlink produces: News/NewsItem?newsId=3&newsTitle=my-news-item
I want: News/3/my-news-item
One way you could do this is to introduce an additional route into the route configuration
RouteConfig.cs:
routes.MapRoute(
name: "News_seo_friendly",
url: "{controller}/{id}/{seo}",
defaults: new { action = "NewsItem", seo = UrlParameter.Optional }
);
*Note the action value in this route. You will need a corresponding action method on that controller.
Also, since this route is more specific it goes above the existing, more generic route(s)
An Alt RouteConfig.cs that might be safer:
routes.MapRoute(
name: "News_seo_friendly",
url: "News/{id}/{seo}",
defaults: new { controller = "News", action = "NewsItem", seo = UrlParameter.Optional }
);
NewsController:
public ActionResult NewsItem(string id)
{
return View();
}
Another way you could do this is to make "News" its own Area within the project. This gives you the opportunity to isolate your routes, if your app is larger, and flexibility for your controller name(s).
Edited after feedback
Wanted to draw attention to the fact that the parameter name on the controller's NewsItem() method should match what is being declared in the route settings. In the above scenario, url: "{controller}/{id}/{seo}"
should match the parameter name in NewsItem(string id)...or vice-versa.

MVC 4 Area Register Sorting

In MVC routing I have a problem.
In basic routing you can simply sort routes by writing them in order you wish. Like below :
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new [] {"SampleApp.UI.Web.Controllers"}
);
routes.MapRoute(
"CompanyRoute",
"{Company_url}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new[] { "SampleApp.UI.Web.Controllers" }
);
What about areas ?
In a spesific situation I need to register one of my areas before other one.
How can I sort area routes ?
From what I've figured out with the help of disassembler:
internal static void RegisterAllAreas(RouteCollection routes, IBuildManager buildManager, object state) {
List<Type> areaRegistrationTypes = TypeCacheUtil.GetFilteredTypesFromAssemblies(_typeCacheName, IsAreaRegistrationType, buildManager);
foreach (Type areaRegistrationType in areaRegistrationTypes) {
AreaRegistration registration = (AreaRegistration)Activator.CreateInstance(areaRegistrationType);
registration.CreateContextAndRegister(routes, state);
}
}
This is a method of an abstract class AreaRegistration where all your custom area registration classes are called. As you can see, there is no ordering. Deeper in the TypeCacheUtil.GetFilteredTypesFromAssemblies method no ordering either.
I can't see any way to use AreaRegistration class for orderable areas registration. The class hasn't any extension point which can be used for the purpose.