ASP.NET MVC routing without controller or action name in URL - asp.net-mvc-5

My URL structure is like http://website.com/city-state/category e.g. /miami-fl/restaurants or /los-angeles-ca/bars. In order to send it to the correct controller, I have a class derived from RouteBase, which splits the request path and figures out the city, state and category. This is working fine for incoming URLs.
public class LegacyUrlRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData routeData = new RouteData();
routeData.RouteHandler = new MvcRouteHandler();
string url = httpContext.Request.Path.ToLower(); //e.g. url = /los-angeles
string[] path = url.TrimStart('/').Split('/'); //
if (path.Length > 1)
{
string[] locale = path[0].Split('-');
if (locale.Length > 1) //This is a city page, so send it to category controller
{
string stateName = locale[locale.Length - 1];
string cityName = path[0].Replace(stateName, "").TrimEnd('-');
routeData.Values.Add("controller", "Category");
routeData.Values.Add("action", "City");
routeData.Values.Add("categoryname", path[1]);
routeData.Values.Add("city", cityName);
routeData.Values.Add("state", stateName);
}
}
}
}
However, when I try to use Html.ActionLink to create a link, it doesn't pick up from this class.
#Html.ActionLink("Restaurants in Miami", "Index", "Category", new {categoryname="Restaurants", state="FL", city="Miami"})
gives me a url of /category/index?categoryname=restaurants&state=fl&city=miami.
How do I generate accurate links for my URL structure.

If you want your outgoing URLs to function, you must implement GetVirtualPath, which converts a set of route values into a URL. It should typically be the mirror image of your GetRouteData method.
The simplest way to implement it would just be to make a data structure that maps your route values to URLs and then use it in both methods as in this example.

Related

struggling with an ASP.NET MVC5 routing issue

So, I have an MVC5 site that uses the default routing template {controller}/{action}/{id} and this works fine. Most everything in the site requires a login (i.e. [Authorize] attribute is used almost everywhere), and this works fine.
Well, now I have a need to allow anonymous access to select pages when a certain kind of link pattern is used: App/{token}/{action}. The {token} is a random string associated with something in my database. I can issue and deactivate these tokens at will.
I got this new App/{token}/{action} routing working by implementing a custom RouteBase that parses the incoming URL for these tokens, and, crucially, adds the the token value to the RouteData.DataTokens so that my App controller can make use of it without needing an explicit action argument for it. So, I added this new route to the route table ahead of the default routing like this:
// new route here
routes.Add("AppToken", new AnonAppAccessRoute());
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
Here is the problem/question: adding this now route has now made my default route stop working -- everything is now going through AnonAppAccessRoute which of course is meant to work only for a few things. I don't understand how to make my AnonAppAccessRoute apply only to URLs with a certain pattern. The MapRoute method accepts a URL pattern, but Adding a route doesn't seem to let you put a filter on it. What am I missing? I've looked around quite a bit at various blogs and documentation about routing, but I've not found good info about using the DataTokens collection (which I feel is important to my approach), and I'm not seeing a good explanation of the difference between Adding a route explicitly vs calling MapRoute.
Here's the code of my custom RouteBase:
public class AnonAppAccessRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
string[] pathElements = httpContext.Request.Path.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
if (pathElements.Length > 0)
{
string token = TryGetArrayElement(pathElements, 1);
if (!string.IsNullOrEmpty(token))
{
result = new RouteData(this, new MvcRouteHandler());
result.DataTokens.Add("appToken", token);
result.Values.Add("controller", "App");
result.Values.Add("action", TryGetArrayElement(pathElements, 2, "Index"));
}
}
return result;
}
private string TryGetArrayElement(string[] array, int index, string defaultValue = null)
{
try
{
return array[index];
}
catch
{
return defaultValue;
}
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return null;
}
}
I got this to work by dropping the custom RouteBase and instead used this MapRoute call like this:
routes.MapRoute(
name: "AppAnon",
url: "App/{token}/{action}",
defaults: new { controller = "App", action = "Index" }
);
Then, in my App controller, I did this in the Initialize override:
protected AppToken _appToken = null;
protected override void Initialize(RequestContext requestContext)
{
base.Initialize(requestContext);
string token = requestContext.RouteData.Values["token"]?.ToString();
_appToken = Db.FindWhere<AppToken>("[Token]=#token", new { token });
if (!_appToken?.IsActive ?? false) throw new Exception("The token is not found or inactive.");
}
This way, my "token" is available to all controller actions via the _appToken variable, and already validated. I did not need to use RouteData.DataTokens. Note that my Db.FindWhere statement is ORM-specific and not really related to the question -- it's just how I look up a database record.

Get a content edit URL as a string without UrlHelpers or Orchard tokens

I'm trying to get the edit URL of a content as a string from backend, the catch is I'm inside a workflow activity, so I can't use Url.Action... or Url.ItemEditLink... or other UrlHelpers as if it were a controller or a view. Also, although I'm inside a workflow, the contents I need it for are not part of the workflowContext or the activityContext, so I can't use those or tokens either.
A solution could be to get the content metadata and the site baseUrl and try to build it manually, but I think this way is prone to errors.
Thanks.
This is how I build a Uri in an activity:
public class MyClass : Task
{
private readonly RequestContext _requestContext;
...
public MyActivity(RequestContext requestContext, ...)
{
_requestContext = requestContext;
...
}
...
public override IEnumerable<LocalizedString> Execute(WorkflowContext workflowContext, ActivityContext activityContext)
{
var content = ... get using ID
var helper = new UrlHelper(_requestContext);
var baseurl = new Uri(_orchardServices.WorkContext.CurrentSite.BaseUrl);
Uri completeurl = new Uri(baseurl, helper.ItemDisplayUrl(content));
yield return T("Done");
}
}
Turns out that I actually do build the Uri semi-manually, but I haven't had issues with this method. You may be able to use just the ItemDisplayUrl for navigation inside of Orchard; I had to get the full URL because the string gets sent to an outside program (Slack).

Custom routes and namespaces in MVC5

I'm trying to implement some domain name logic in my existing MVC5 app. The problem I'm running in to is if I try to use my custom subclass from Route, it doesn't respect the Namespaces field and throws an error because I have 2 different User controllers.
As a control, this works perfectly fine:
routes.MapRoute("Login",
"login/",
new { controller = "User", action = "Login" },
new[] { "Quotes.Web.Controllers" });
My DomainRoute class inherits from Route and just adds a Domain property. Here is the relevant constructor:
public DomainRoute(string domain, string url, object defaults, string[] namespaces = null)
: base(url, new RouteValueDictionary(defaults), new MvcRouteHandler())
{
Domain = domain;
DataTokens = new RouteValueDictionary {["Namespaces"] = namespaces};
}
and I register it like:
var loginRoute = new DomainRoute(
domain,
"login/",
new { controller = "User", action = "Login" },
new[] { "Quotes.Web.Controllers" });
routes.Add("Login", loginRoute);
DataTokens looks identical between the working version and my broken version yet it seems to ignore the fact that my DomainRoute has a Namespace entry
Multiple types were found that match the controller named 'User'. This can happen if the route that services this request ('login/') does not specify namespaces to search for a controller that matches the request. If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter.
What am I missing?
I think,this will help you, i had the same issue, solved this by adding the below code
var dataTokens = new RouteValueDictionary();
var ns = new string[] {"MyProject.Controllers"};
dataTokens["Namespaces"] = ns;
routes.Add("Default", new CultureRoute(
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
null /*constraints*/,
dataTokens
));
I switched my DomainRoute class with the much improved version found here: https://gist.github.com/IDisposable/77f11c6f7693f9d181bb
Now my route creation is just:
var clientRoutes = new DomainRouteCollection("mydomain",
"Quotes.Web.Controllers",
routes);
clientRoutes.MapRoute("Login", "login/", new { controller = "User", action = "Login" });
...which is more concise and even more importantly, it works.

Generate Url during runtime from asp.net mvc controller/action

I have such a url:
www.test.com/MyAreaName/MyControllerName/MyAction?key1=value&key2=value
I need a method like:
string generatedUrlWithQueryParams = Url.Action<MyController>(x => x.MyAction(MyViewModel));
I need to call the above method from a .cs class NOT from razor html file.
How can I do that? I heard of asp.net mvc futures but I can not find the appropriate method or namespace to use that method.
Are you looking for something like this:
string url = new UrlHelper(System.Web.HttpContext.Current.Request.RequestContext);
If you have the Request available(it is available in your controller actions), you can use the UrlHelper class.
var urlBuilder = new UrlHelper(Request.RequestContext);
var url = urlBuilder .Action("YourAction", new YourViewModel { Age = 44, Code = "Test"});
or
var url = urlBuilder .Action("YourAction", "YourController",
new YourViewModel { Age = 44, Code = "Test"});
Assuming YourViewModel has Age and Code property and you need those as the route values of the generated url.
If you are invoking this code from another class, you may pass the RequestContext to that from your controller/action.

Generating a URL to a service in ServiceStack

How would I generate a URL to a specific service defined in ServiceStack?
I want to include full or relative URLs to other endpoints as part of the response DTO. RestServiceBase contains RequestContext.AbsoluteUri, but that is entirely dependent on the request.
Reverse Routing
The Reverse Routing section in the wiki shows how to use extension methods on a popualated Request DTO to generate relative and absolute URI's:
If you use [Route] metadata attributes (as opposed to the Fluent API) you will be able to generate strong-typed URI's using just the DTOs, letting you create urls outside of ServiceStack web framework as done with .NET Service Clients using the ToUrl(HttpMethod) and ToAbsoluteUri(HttpMethod), e.g:
[Route("/reqstars/search", "GET")]
[Route("/reqstars/aged/{Age}")]
public class SearchReqstars : IReturn<ReqstarsResponse>
{
public int? Age { get; set; }
}
var relativeUrl = new SearchReqstars { Age = 20 }.ToGetUrl();
var absoluteUrl = new SearchReqstars { Age = 20 }.ToAbsoluteUri();
relativeUrl.Print(); //= /reqstars/aged/20
absoluteUrl.Print(); //= http://www.myhost.com/reqstars/aged/20
The Email Contacts demo shows an example of using the above Reverse Routing extension methods to populate routes for HTML Forms and Links in Razor Views.
Other Reverse Routing Extension methods
new RequestDto().ToPostUrl();
new RequestDto().ToPutUrl();
new RequestDto().ToDeleteUrl();
new RequestDto().ToOneWayUrl();
new RequestDto().ToReplyUrl();
Accessing Http Request
You can also inspect the incoming underlying httpRequest with:
var httpReq = base.RequestContext.Get<IHttpRequest>();
As well as the underlying ASP.NET (or HttpListener) Request object with:
var aspNetReq = httpReq.OriginalRequest;
They should contain additional properties that should be more useful.

Resources