ASP.NET Core map route to static file handler - asp.net-mvc

I'm working on a ASP.NET Core website (previously named ASP.NET 5 / vNext) with Angular. In order for Angular to work I need to have a catch-all route:
app.UseStaticFiles();
app.UseMvc(routes =>
{
// Angular fallback route
routes.MapRoute("angular", "{*url}", new { controller = "Home", action = "Index" });
});
I also have a few files/folders in wwwroot, like:
wwwroot/app
wwwroot/assets
wwwroot/lib
When any requests are made to these paths, for example http://example.com/assets/css/test.css, and the file (test.css) does NOT exist, it should not continue to the fallback route. It should return a 404.
Right now, if the file does not exist it returns the Angular HTML. So, how can I tell it that any path that starts with '/assets' should only be routed / served by UseStaticFiles?

This seems to work:
app.MapWhen(
context => {
var path = context.Request.Path.Value.ToLower();
return
path.StartsWith("/assets") ||
path.StartsWith("/lib") ||
path.StartsWith("/app");
},
config => config.UseStaticFiles());
However, I'm not sure if there are any performance (or other type of) implications. I'll update if I come across any.

It is strange that this common case (since many use SPA) is not covered almost anywhere and everyone has to invent something. I have found that the best way to do that is to add constraint (e.g. do not use the route if there is /api or "." in the path). Unfortunately this is not supported out of the box, but you can write this constraint yourself or copy the one I wrote from here.
There are a bit more details in this post. But generally the code looks like this:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "api",
template: "api/{controller}/{action}");
routes.MapRoute(
name: "angular",
template: "{*url}",
defaults: new {controller = "Home", action = "Index"},
constraints: new {url = new DoesNotContainConstraint(".", "api/") });
});
P.S. Perhaps this constraint exist out of the box now, but I have not found one. Alternatively a RegEx can be used, but simple one should be way faster.

If you are using attribute template routing then you can apply this elegant solution:
[HttpGet("")]
[HttpGet("{*route:regex(^(?!skipped).*$)}")]
public async Task<IActionResult> Default(string route) { throw new NotImplementedException(); }
So all routes started with skipped will be ignored by this action.
If you want multiple prefixes or anything else then just update regex for your needs.
First Get attribute is needed to handle root of site (in this case route variable is null).

Related

How to convert this routing configuration from .net3.1 to .net5?

I have an api running on localhost:5001, in my ASP.NET MVC application I was able to do this:
routes.MapRoute(
name: "Default",
url: "{*stuff}",
defaults: new { controller = "Default", action = "DefaultAction" }
);
this meant that i could simply type localhost:5001/aRandomStuff in my webbrowser and "aRandomStuff" would get passed in as a parameter in my actionresult DefaultAction inside my DefaultController.
What is the equivalence of doing this kind of routing in .net5 under
app.UseEndpoints(endpoints =>
{
//routing here
});
?
You could use the RouteAttribute directly on your endpoint.
Just map your default controller in your Startup.cs (if you need to have a default controller):
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
Then in your controller add the Route attribute on the action you want.:
[Route("{id}")]
public IActionResult DefaultAction(string id)
{
return View();
}
Since only the endpoint has a parameter {} route defined in this case and the controller doesn't, the above endpoint will be triggered from localhost:5001/whateverString and whateverString will be sent in as a value to the parameter id.
I needed to edit my original answer since it was meant for Api Endpoints. The edited answer should work for your MVC app instead.
They made many updates to routing in .Net 5 that really messed with how we perceive catch-all routing. These were actually intentional and technically how they want it to work. However, it introduced a lot of confusion from the POV from most of us. You can see some discussion and bug logs here, here, and here.
I have not been able to get it to work with UseEndpoints in startup. As the first link shows, the order of the controllers for precendence is also messing with things, and the other answers here could potentially cause route conflicts or other confusion for which route is most important.
The only way I have seen this to work is with route attributes in the controller where we set the order. This means that you know it will be the last pattern checked:
[Route("{*url}", Order = 999)]
public IActionResult MyCatchAll(string url)
{
...
}
This way the priority order will be last (I think most are between 0-5, so 999 is plenty high) and the url is passed as a parameter for you to manage as needed. And frankly, I prefer this but the option to do either would be nice.
If someone has a way to do this in app.UseEndpoints, I would love to see it, but all the samples fail or only work sporadically for me.
Use This Pattern on Your Action And Catch All Request And Get First Route Value Like This
[Route("/{**catchAll}")]
public IActionResult Index()
{
string randomQuery =HttpContext.Request.RouteValues["catchAll"]?.ToString();
return Content(randomQuery);
}
try this
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Default}/{action=DefaultAction}/{id?}");
});
[Route("{id}")]
[Route("~/{id}")]
public IActionResult DefaultAction(string id)
{
return View();
}

Multiple SPA Using MVC Areas - Navigate Outside SPA Router Routes

How can I escape client side routing when wanting to navigate outside the set list of routes setup in the router?
I created a project using Durandal.js and have created SPA's inside different Areas. The problem I ran into is that when I want to navigate outside the current SPA and into another or say back to the home page of the entire application which is not an SPA at all but simply a cshtml page.
What I have tried to do is use Durandal's mapUnknownRoutes handler to intercept and then use window.location.href to navigate out. This works, but when I want to go the home page of the application ("/"), the router matches the "root" of the SPA and doesn't navigate out to the home page but instead the SPA's root route.
The area route in this example is "/spaHome" whose MVC route looks like:
context.MapRoute(
"spaHome_default",
"spaHome/{*catchall}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
Here's what I've done to set up the Durandal router:
var routes = [
{ route: ['spaHome', ''], moduleId: 'spaHome', title: "spaHome", hash: "#spaHome"},
{ route: 'one/anotherPage', moduleId: 'one/anotherPage', title: "one/anotherPage", hash: "#one/anotherPage"}
];
router.makeRelative({ moduleId: 'viewmodels' });
router.map(routes)
.mapUnknownRoutes(function (instruction) {
utils.navigateToPath(instruction.fragment);
return false;
})
.activate({ pushState: true, root: "/spaHome" });
Any pointers or leads into the right direction for this would be much appreciated!
After some trial and error I was able to come up with a solution to what I was trying accomplish. It looks like a solid solution, so hopefully it doesn't cause any issues down the road.
I changed the routes array and removed the default route of '' from the first route. This allowed me to have a unknown route to go off of when wanting to hit the normal MVC homepage. I also had to remove the "root" property from the activate function of the router. Which in turn meant I had to explicitly declare the routes in the route array with the extra area portion or the URL ("spaHome/").
Then in my mapUnknownRoutes handler I checked the route for the area portion of the URL, and if that existed, I used the SPA router to show a notfound page for that SPA. Else I assumed that the route exists outside the area and I need to hard navigate to the URL using window.location.href.
var routes = [
{ route: ['spaHome'], moduleId: 'spaHome', title: "spaHome", hash: "#spaHome"},
{ route: 'spaHome/one/anotherPage', moduleId: 'one/anotherPage', title: "one/anotherPage", hash: "#spaHome/one/anotherPage"}
];
router.makeRelative({ moduleId: 'viewmodels' });
router.map(routes)
.mapUnknownRoutes(function (instruction) {
if (instruction.fragment.toLowerCase().indexOf("spaHome/", 0) === -1) {
utils.navigateToPath(instruction.fragment);
} else {
var notfoundRoute = "notfound";
instruction.config.moduleId = notfoundRoute;
history.navigate(notfoundRoute, { trigger: false, replace: true });
}
})
.activate({ pushState: true });
If anyone has a better solution please let me know!
EDIT
Ok, I ran into an issue with the history while doing this. I had to add a line to the Durandal router.js file to stop the previous route from being added to the history queue.
if (!instruction.cancelQueue) {
router.trigger('router:route:before-config', instruction.config, router);
router.trigger('router:route:after-config', instruction.config, router);
queueInstruction(instruction);
}
Edit 2
I also ran into an issue with this method where the navigation doesn't work quite right for IE9 and below.

MVC Routing Redirection

I have a webservice project that is being expanded to host multiple APIs instead of just one, so I wanted to cleanup the routes without breaking the old version. The main API used to sit off of a controller named API and accept parameters like this:
api/{language}/{action}/{*parameters}
Now, I have an Area named API that is going to house each of the APIs in their own controller and the route will look like this:
api/{controller}/{language}/{action}
I still need the old route to be usable for legacy apps already using the old route, I was hoping just to be able to create a 2nd "legacy" route that would catch the old pathing and use the new controller. I tried this but it only worked then with the new location and the ikd one returned a 404.
api/{language}/{action}/{*parameters}",
new { controller = "api1", action = "Index", language = "json" }
Any ideas on how to accomplish this? I tried RouteMagic but it didn't seem to work. Maybe I set the redirect up wrong though.
Ok, figured out after getting a sandwhich...just needed to add route constraints so that the old version would not catch on the new route format:
routes.MapRoute(
"API_default",
"api/{controller}/{language}/{action}",
new { controller = "api1", action = "Index", language = "json" },
new { language = "json|xml" }
);
routes.MapRoute(
"API_legacy",
"api/{language}/{action}/{*parameters}",
new { controller = "api1", action = "Index", language = "json", parameters = UrlParameter.Optional },
new { language = "json|xml" }
);

ASP.NET MVC - Catch All Route And Default Route

In trying to get my application to produce 404 errors correctly, I have implemented a catch all route at the end of my route table, as shown below:
routes.MapRoute(
"NotFound", _
"{*url}", _
New With {.controller = "Error", .action = "PageNotFound"} _
)
However, to get this working, I had to remove the default route:
{controller}/action/{id}
But now that the default has been removed, most of my action links no longer work, and the only way I have found to get them working again is to add individual routes for each controller/action.
Is there a simpler way of doing this, rather than adding a route for each controller/action?
Is it possible to create a default route that still allows the catch all route to work if the user tries to navigate to an unknown route?
Use route constraints
In your case you should define your default route {controller}/{action}/{id} and put a constraint on it. Probably related to controller names or maybe even actions. Then put the catch all one after it and it should work just fine.
So when someone would request a resource that fails a constraint the catch-all route would match the request.
So. Define your default route with route constraints first and then the catch all route after it:
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new { controller = "Home|Settings|General|..." } // this is basically a regular expression
);
routes.MapRoute(
"NotFound",
"{*url}",
new { controller = "Error", action = "PageNotFound" }
);
//this catches all requests
routes.MapRoute(
"Error",
"{*.}",
new { controller = "PublicDisplay", action = "Error404" }
);
add this route at the end the routes table
Ah, the problem is your default route catches all 3 segment URLs. The issue here is that Routing runs way before we determine who is going to handle the request. Thus any three segment URL will match the default route, even if it ends up later that there's no controller to handle it.
One thing you can do is on your controller override the HandleMissingAction method. You should also use the tag to catch all 404 issues.
Well, what I have found is that there is no good way to do this. I have set the redirectMode property of the customErrors to ResponseRewrite.
<customErrors mode="On" defaultRedirect="~/Shared/Error" redirectMode="ResponseRewrite">
<error statusCode="404" redirect="~/Shared/PageNotFound"/>
</customErrors>
This gives me the sought after behavior, but does not display the formatted page.
To me this is poorly done, as far as SEO goes. However, I feel there is a solution that I am missing as SO does exactly what I want to happen. The URL remains on the failed page and throws a 404. Inspect stackoverflow.com/fail in Firebug.
My Solution is 2 steps.
I originally solved this problem by adding this function to my Global.asax.cs file:
protected void Application_Error(Object sender, EventArgs e)
Where I tried casting Server.GetLastError() to a HttpException, and then checked GetHttpCode.
This solution is detailed here:
ASP.NET MVC Custom Error Handling Application_Error Global.asax?
This isn't the original source where I got the code. However, this only catches 404 errors which have already been routed. In my case, that ment any 2 level URL.
for instance, these URLs would display the 404 page:
www.site.com/blah
www.site.com/blah/blah
however, www.site.com/blah/blah/blah would simply say page could not be found.
Adding your catch all route AFTER all of my other routes solved this:
routes.MapRoute(
"NotFound",
"{*url}",
new { controller = "Errors", action = "Http404" }
);
However, the NotFound route does not seem to route requests which have file extensions. This does work when they are captured by different routes.
I would recommend this as the most readable version. You need this in your RouteConfig.cs, and a controller called ErrorController.cs, containing an action 'PageNotFound'. This can return a view. Create a PageNotFound.cshtml, and it'll be returned in response to the 404:
routes.MapRoute(
name: "PageNotFound",
url: "{*url}",
defaults: new { controller = "Error", action = "PageNotFound" }
);
How to read this:
name: "PageNotFound"
= create a new route template, with the arbitrary name 'PageNotFound'
url:"{*url}"
= use this template to map all otherwise unhandled routes
defaults: new { controller = "Error", action = "PageNotFound" }
= define the action that an incorrect path will map to (the 'PageNotFound' Action Method in the Error controller). This is needed since an incorrectly entered path will not obviously not map to any action method
I tried all of the above pattern without luck, but finally found out that ASP.NET considered the url I used as a static file, so none of my request was hidding the single controller endpoint. I ended up adding this snipper to the web.config
<modules runAllManagedModulesForAllRequests="true"/>
And then use the below route match pattern, and it solved the issue:
routes.MapRoute(
name: "RouteForAnyRequest",
url: "{*url}",
defaults: new { controller = "RouteForAnyRequest", action = "PageNotFound" }
);

Can .net mvc routing cause JavaScript errors?

I am having a lot of trouble using routing infrastructure of asp.net mvc2. I have following routes registered in my global.asax file
routes.MapRoute(
"strict",
"{controller}.mvc/{docid}/{action}/{id}",
new { action = "Index", id = "", docid = "" },
new { docid = #"\d+"}
);
routes.MapRoute(
"default",
"{controller}.mvc/{action}/{id}",
new { action = "Index", id = "" },
new { docConstraint = new DocumentConstraint() }
);
The problem is with first route ("strict"). Three kind of urls can match first route. mycontroller/23/myaction, mycontroller/23/myaction/12 or mycontroller/23/mvaction/stringid. If I try to use this route without specifying value of id everything works fine for example:
Html.ActionLink("Link text", "ActionName", new{docid = 23});
Everything goes well, but if I use links like:
Html.ActionLink("Link text", "ActionName", new{docid = 23, id = 223})
This will produce url currentcontroller.mvc/23/ActionName/223 that is absolutely correct but when it loads the page it gives a JavaScript error in jquery1.4.2.min.js file.
This is strange: if I change id to someid =223 it will reflect in query string and there will be no JS error.
Edit: I have done some further debugging and found when both id and docid are mentioned in route values one thing is ignored in global.asax that is the ignore path.
routes.RouteExistingFiles = false;
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.ignoreRoute is totally bypassed and I can see names of JS files in route value dictionary while debugging in my controller.
it gives javascript error in
jquery1.4.2.min.js file
The most likely cause for this is that something you are displaying on the page is different and you are performing an action that is causing the error. Can you supply enough of a sample from the rendered page to show what you are using jQuery for?
If we drag scripts from solution explorer to site.master it results in following output
<script type="text/javscript" src="../../scripts/jquery.min.js"></script>
The leading dots (..) are creating the problem. Putting source path in url.content or using /scripts instead of ../../scripts will solve the problem because these leading periods are forcing them to match some route in global.asax.

Resources