I have a complex ASP.NET MVC routing scenario and I want to be able to parse a URL that I pull from the 'Referrer' request header using the existing routes.
I have incoming requests that look like this:
http://hostname/{scope}/{controller}/{action}
With corresponding route mapping:
routes.MapRoute(
name: "scoped",
url: "{scope}/{controller}/{action}/{id}",
defaults: new { controller = "Equipment", action = "Index", id = UrlParameter.Optional, scope = "shared" }
);
In the OnActionExecuting method of the base class of my controllers I pull the resulting scope from the RouteData:
var scope= (filterContext.RouteData.Values["scope"] as string).ToLower();
I then use the scope to construct some filters for my database queries. It all worked perfectly fine until I moved all my Json-returning methods to a separate set of WebApi2 controllers. I now also have a route:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}"
);
All ajax requests are now made to the api controllers, which means that I do not have the scope value available. I want to solve this by using the 'Referrer' URL from the request header, which is is usually a URL that does include the scope.
What I would like to do is something like this when the ApiController initializes:
public void PullCurrentScopeDomainFromRequestHeader(System.Net.Http.Headers.HttpRequestHeaders headers) {
var refererUrl = headers.GetValues("Referer").First();
//do some magic to get the scope
}
The difficulty is that the scope can also have a default value ("shared"), in case a url like "http://hostname/controller/action" get's passed in. The best (and DRYest) way to get the scope from any URL, would be by somehow using the "scoped" route that I mapped in the routing config to parse the URL somehow. I just have no idea how to do that. Can anyone help?
You just need to build up a fake HTTP context based on your URL and then use the static RouteTable to parse the URL into a RouteValueDictionary.
// Create a fake HttpContext using your URL
var uri = new Uri("http://hostname/controller/action", UriKind.Absolute);
var request = new HttpRequest(
filename: string.Empty,
url: uri.ToString(),
queryString: string.IsNullOrEmpty(uri.Query) ? string.Empty : uri.Query.Substring(1));
// Create a TextWriter with null stream as a backing stream
// which doesn't consume resources
using (var nullWriter = new StreamWriter(Stream.Null))
{
var response = new HttpResponse(nullWriter);
var httpContext = new HttpContext(request, response);
var fakeHttpContext = new HttpContextWrapper(httpContext);
// Use the RouteTable to parse the URL into RouteData
var routeData = RouteTable.Routes.GetRouteData(fakeHttpContext);
var values = routeData.Values;
// The values dictionary now contains the keys and values
// from the URL.
// Key | Value
//
// controller | controller
// action | action
// id | {}
}
Note that you can also use a specific route from the RouteTable by specifying its name.
var routeData = RouteTable.Routes["scoped"].GetRouteData(fakeHttpContext);
Related
For every web page within my ASP.NET Core 3.1 C# application, I want to automatically generate an canonical link for SEO purposes.
Currently (POC phase), I have the following snippet in every controller action function:
Uri actualUrl = new Uri($"{HttpContext.Request.Scheme}://{HttpContext.Request.Host}{HttpContext.Request.Path}{HttpContext.Request.QueryString}");
RouteValueDictionary values = RouteData.Values;
values.Remove("controller");
values.Remove("action");
foreach (var q in HttpContext.Request.Query)
values.Add(q.Key, q.Value);
// Further route/query parameter "fixes" here.
Uri canonicalUrl = new Uri(Url.ActionLink(values: values));
if (!canonicalUrl.Equals(actualUrl))
return RedirectPermanentPreserveMethod(canonicalUrl.ToString());
This snippet first builds a Uri with the current actual URL. Then it may "fixes" some important route/query parameters (as shown below). Finally it compares the actual uri with the desired uri, and redirects to the desired uri, when the actual uri is different compared to the desired uri (case sensitive).
RouteData.Values["subpage"] = "Digital-Contents";
This process enables the web application to generate the correct canonical url ( http://example.com/MyController/MyAction/Digital-Contents ) for the following sample urls.
http://example.com/mycontroller/myaction/digital-contents
http://example.com/Mycontroller/Myaction/Digital-contents
http://example.com/myconTROLLer/myACTion/digital-Contents
However, the POC is a massive duplication of code, and thus not desirable itself.
My first thought was to use a middleware. However, with an middleware, the action controller cannot "fix" route/query parameters, which are out-of-scope of the regular routing construct (like the "id" route parameter which is shown in most ASP.NET examples). E.g. ActionLink is capable of producing the correct case sensitive url slugs for controller and action, but cannot process ("fix") other route/query parameters.
My second thought was to use a generic class, but there I lose the context.
The best solution would be a single (void) function call, which can be placed before the actual action heavy processing (inside the action controller before processing data and generating output).
How to deduplicate this "automatic canonical redirector" code?
This may not be the best solution, i just modified the case based on the code you provided:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CanonicalUrlAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var httpContext = filterContext.HttpContext;
Uri actualUrl = new Uri($"{httpContext.Request.Scheme}://{httpContext.Request.Host}{httpContext.Request.Path}{httpContext.Request.QueryString}");
RouteValueDictionary values = filterContext.RouteData.Values;
values.Remove("controller");
values.Remove("action");
foreach (var q in httpContext.Request.Query)
values.Add(q.Key, q.Value);
// Further route/query parameter "fixes" here.
Uri canonicalUrl = new Uri(new UrlHelper(filterContext).ActionLink(values));
if (!canonicalUrl.Equals(actualUrl))
filterContext.Result = new LocalRedirectResult(canonicalUrl.ToString());
}
}
Usage
[CanonicalUrl]
public class HomeController : Controller {
}
If you're using names from view models to generate urls like example.com/some-category/some-product then i would use the helper in this Link to generate a slug in kebab case based on the model name (in my case its saved to db on model creation)
then with a custom route :
endpoints.MapControllerRoute(
name: "category",
pattern: "{Category}/{Product}",
defaults: new { controller = "Product", action = "Index" });
This pattern omits the action and controller names from route (which i prefer)
and gives you something like this example.com/some-category/some-product and in your action you just compare the model's slug with the route segment that is provided by the user (using the route contraint) like this:
public async Task<IActionResult> Index([FromRoute,Required] Category,[FromRoute,Required] Product)
and you do a route redirect on mismatch like this:
return RedirectToRoutePermanent("category", new { Product = Product.Slug, Category = Product.Category.Slug });
Hope this helps.
So I'm having a little problem here with routing.
There are two parts to this web application:
1. Brochure / Display Website
2. Internal Site / Client Application
We wanted a way to release changes for the brochure without having to do a whole release of said Web application.
Visiting existing named views will take the user to a brochure page, however if it doesn't exist, it will act like they are a client and will redirect them to their company's login screen.
Global.asax:
//if view doesnt exist then url is a client and should be redirected
routes.MapRoute(
name: "Brochure",
url: "{id}",
defaults: new { controller = "brochure", action = "Brochure", id = "Index" },
namespaces: new[] { "Web.Areas.Brochure.Controllers" }
);
//This is home page
routes.MapRoute(
name: "HomeDefault",
url: "{client}/{action}",
defaults: new { controller = "home", action = "index" },
namespaces: new string[] { "Web.Controllers" }
);
Controller:
/// <summary> Check if the view exists in our brochure list </summary>
private bool ViewExists(string name) {
ViewEngineResult result = ViewEngines.Engines.FindView(ControllerContext, name, null);
return (result.View != null);
}
/// <summary> Generic action result routing for all pages.
/// If the view doesn't exist in the brochure area, then redirect to interal web
/// This way, even when we add new pages to the brochure, there is no need to re-compile & release the whole Web project. </summary>
public ActionResult Brochure(string id) {
if (ViewExists(id)) {
return View(id);
}
return RedirectToRoute("HomeDefault", new { client = id });
}
This code works fine up until we log in and go to the landing page. It seems to keep the Brochure action in the route and doesn't want to go to the subsequent controller which results in a 500 error.
e.g. 'domain/client/Brochure' when it needs to be: 'domain/client/Index'
Things tried but not worked:
Changing RedirectToRoute() to a RedirectToAction() - this results in a
finite loop of going back to the ActionResult Brochure(). So
changing controllers through that didn't work.
Create an ActionResult called Brochure() inside the 'HomeController'. It
doesn't even get hit.
Passed in namespaces for RedirectToRoute() as an attribute. I knew this would probably not work, but it was worth a try.
So the question is:
How can I get the route to act properly?
If you can restrict id to some subset of all values you can add that constraints to route (i.e. numbers only) to let default handle the rest.
routes.MapRoute(
name: "Brochure",
url: "{id}",
defaults: new { controller = "brochure", action = "Brochure", id = "Index" },
namespaces: new[] { "Web.Areas.Brochure.Controllers" }
constraints : new { category = #"\d+"}
);
If you can't statically determine restrictions - automatically redirecting in your BrochureController similar to your current code would work. The only problem with sample in the question is it hits the same route again and goes into infinite redirect loop - redirect to Url that does not match first rule:
// may need to remove defaults from second route
return RedirectToRoute("HomeDefault", new { client = id, action = "index" });
If standard constraints do not work and you must keep single segment in url - use custom constraints - implement IRouteConstraint and use it in first route. See Creating custom constraints.
There are several issues with your configuration. I can explain what is wrong with it, but I am not sure I can set you on the right track because you didn't provide the all of the URLs (at least not all of them from what I can tell).
Issues
Your Brouchure route, which has 1 optional URL segment named {id}, will match any URL that is 0 or 1 segments (such as / and /client). The fact that it matches your home page (and you have another route that is named HomeDefault that will never be given the chance to match the home page) leads me to believe this wasn't intended. You can make the {id} value required by removing the default value id = "Index".
The Brouchure route has a namespace that indicates it is probably in an Area. To properly register the area, you have to make the last line of that route ).DataTokens["area"] = "Brochure"; or alternatively put it into the /Areas/Brouchure/AreaRegistration.cs file, which will do that for you.
The only way to get to the HomeDefault route is to supply a 2 segment URL (such as /client/Index, which will take you to the Index method on the HomeController). The example URLs you have provided have 3 segments. Neither of the routes you have provided will match a URL with 3 segments, so if these URLs are not getting 404 errors they are obviously matching a route that you haven't provided in your question. In other words, you are looking for the problem in the wrong place.
If you provide your entire route configuration including all Area routes and AttributeRouting routes (including the line that registers them), as well as a complete description of what URL should go to what action method, I am sure you will get more helpful answers.
So the question is:
How can I get the route to act properly?
Unknown. Until you describe what properly is.
Related: Why map special routes first before common routes in asp.net mvc?
Two ways I could have solved this issue:
Way 1
I reviewed the redirect and just passed in an action in order to get a route that has 2 segments in the url. i.e. client/Index. The Index action now handles logins - going past a custom controller.
public class HomeController : CustomController
public ActionResult Brochure(string id, string action) {
if (ViewExists(id)) {
return View(id);
}
return RedirectToAction("Index", "Home", new { client = id, action = "Index" });
}
Way 2
(from #Alexei_Levenkov)
Create a custom Route constraint so the route will be ignored if the view cannot be found.
namespace Web.Contraints {
public class BrochureConstraint : IRouteConstraint {
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) {
//Create our 'fake' controllerContext as we cannot access ControllerContext here
HttpContextWrapper context = new HttpContextWrapper(HttpContext.Current);
RouteData routeData = new RouteData();
routeData.Values.Add("controller", "brochure");
ControllerContext controllerContext = new ControllerContext(new RequestContext(context, routeData), new BrochureController());
//Check if our view exists in the folder -> if so the route is valid - return true
ViewEngineResult result = ViewEngines.Engines.FindView(controllerContext, "~/Areas/Brochure/Views/Brochure/" + values["id"] + ".cshtml", null);
return result.View != null;
}
}
}
namespace Web {
public class MvcApplication : System.Web.HttpApplication {
//If view doesnt exist then url is a client so use the 'HomeDefault' route below
routes.MapRoute(
name: "Brochure",
url: "{id}",
defaults: new { controller = "brochure", action = "Brochure", id = "Index" },
namespaces: new[] { "Web.Areas.Brochure.Controllers" },
constraints: new { isBrochure = new BrochureConstraint() }
);
//This is home page for client
routes.MapRoute(
name: "HomeDefault",
url: "{client}/{action}",
defaults: new { controller = "home", action = "index" },
namespaces: new string[] { "Web.Controllers" }
);
}
}
I hope this helps someone else out there.
I am trying to prepare a 301 redirect for a typo I made 'recieved'
I am struggling to find a way of getting the url from the action and controller names.
I am aware of UrlHelper.Action but it does not exist within Global.asax. How do I gain access to this method?:
// Add permanent redirection for retired pages (Application_BeginRequest())
if (HttpContext.Current.Request.Url.LocalPath.ToLower().StartsWith("/blah/listrecieved"))
{
HttpContext.Current.Response.RedirectPermanent(/*Need url generated from action and controller*/);
}
Alternatively I have created a route, if that's how I should be getting the string, this is also fine but I am unsure of how:
routes.MapRoute(
name: "blah-list-received",
url: "blah/list-received",
defaults: new { controller = "Blah", action = "ListReceived" }
);
for example, it might look like this:
// Add permanent redirection for retired pages
if (HttpContext.Current.Request.Url.LocalPath.ToLower().StartsWith("/blah/listrecieved"))
{
HttpContext.Current.Response.RedirectPermanent(routes.GetUrl( "blah-list-received" ) );
}
You need to construct the UrlHelper yourself:
var url = new UrlHelper(HttpContext.Current.Request.RequestContext, RouteTable.Routes)
.Action("YourAction",
"YourController",
new { paramName = paramValue });
See MSDN
I have a situation that I am working on a blogging website which has urls of the form:
/blog/{id}/{title-slug}
The {title-slug} is purely a user friendly string designed to make the url look nicer. The id alone is enough to uniquely determine everything.
I have some code in a global ActionFilterAttribute which will recognise that the {title-slug} is missing (or incorrect) and it then does
filterContext.Result = new RedirectResult(url.Action(action, controller, routeValueDictionary));
This works mostly fine except for one problem. I have multiple routes that match the controller and action and it seems to just choose the first one...
Lets see some code. My relevant routes look like this:
routes.MapRoute
(
"ShowFeedAndFollow",
"blog/{shortcode}/{slug}/followme",
new { controller = "Public", action = "ShowFeed", shortcode = "", slug = "", followme = true } // Parameter defaults
);
routes.MapRoute
(
"ShowFeed",
"blog/{shortcode}/{slug}",
new { controller = "Public", action = "ShowFeed", shortcode = "", slug = "" } // Parameter defaults
);
My code to redirect if the slug is wrong looks (in simplified form) like this:
var slug = filterContext.RouteData.Values["slug"] == null ? "" : filterContext.RouteData.Values["slug"].ToString();
var action = filterContext.RouteData.Values["action"] == null ? "" : filterContext.RouteData.Values["action"].ToString();
var controller = filterContext.RouteData.Values["controller"] == null ? "" : filterContext.RouteData.Values["controller"].ToString();
RouteValueDictionary routeValueDictionary = new RouteValueDictionary(filterContext.RouteData.DataTokens);
var canonicalSlug = GetCanonnicalSlug();
routeValueDictionary["slug"] = canonicalSlug;
if (!String.IsNullOrEmpty(canonicalSlug) && !slug.Equals(canonicalSlug))
{
var url = new UrlHelper(filterContext.RequestContext);
filterContext.Result = new RedirectResult(url.Action(action, controller, routeValueDictionary));
}
As you can see I can get the action and controller from the current route data and make a new routeValueDictionary with the modified route data. The problem I have is that it picks up the url for the first route (called "ShowFeedAndFollow") and not the one I actually want and that it came in as, "ShowFeed".
I cannot hardcode the route name (because the same code currently works for blogs and posts in the blogs). I have the route object (filterContext.RouteData.Route) but I can't seem to find a way to generate a url given that object.
I can probably just reorder the routes so that the one I want is first. However this is clearly a fragile solution that breaks if I ever decide to do something that changes the order of these things.
So my question in summary then is this:given I am in a filter attribute and have all the data about the route that got me here and I want to just redirect to the exact same route/url but with one parameter changed, how can I best do this?
To do the routing with parameters in filterContext it will be better to use RedirectToRouteResult instead of RedirectResult
You can simply do this:
First collect all route data include Controller Name, Action Name and other parameters.
var routeData = filterContext.RouteData.Values;
Then make any change that needed in parameters like this:
routeData["slug"] = routeData["slug"].ToString().Replace("-", "_");
and finally use RedirectToRouteResult instead of RedirectResult:
filterContext.Result = new RedirectToRouteResult(routeData);
I have an asp.net Web API project, and in my WebApiConfig file, I have the following route defined:
config.Routes.MapHttpRoute(
name: "Web API Get",
routeTemplate: "api/{controller}",
defaults: new { action = "Get" },
constraints: new { httpMethod = new HttpMethodConstraint("GET") }
);
For integration testing purposes, I want to make a request to an HttpSelfHostServer to verify that we are receiving the proper data back from the api call. I am making the HttpRequestMessage as follows:
var httpMethod = new HttpMethod("GET");
var request = new HttpRequestMessage(httpMethod, "http://localhost:XXXX/api/User/");
var results = _client.SendAsync(request).Result;
I would expect that this would call the Get method on the UserController and then return the results as defined in that method. However, I instead get the following exception:
System.InvalidOperationException: The constraint entry 'httpMethod' on the route with route template 'api/{controller}' must have a string value or be of a type which implements 'IHttpRouteConstraint'
This same url (http://localhost:XXXX/api/User/) works without error when I use it in the browser, so I am pretty sure the issue has to be in the way I am sending the request to the HttpSelfHostServer through the HttpClient. I have tried using the HttpMethod.Get constant instead, but that also threw the same error.
Does anyone have any idea how I could resolve this issue?
Make sure that you are using the proper type for your constraint:
constraints: new { httpMethod = new System.Web.Http.Routing.HttpMethodConstraint(HttpMethod.Get) }
I guess you were using System.Web.Routing.HttpMethodConstraint which is an entirely different class used for ASP.NET MVC routing and which has nothing to do with ASP.NET Web API routing.