How to convert this routing configuration from .net3.1 to .net5? - asp.net-mvc

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();
}

Related

Include a file extension in ASP.NET Core MVC routes

I'm porting a legacy application to ASP.NET Core and I'm trying to keep the URLs consistent. Ideally I need to include a "file extension" in the URL, e.g. /Home/Index.ext should route to the Index action on HomeController.
I've started with the standard web application template in Visual Studio and tried modifying the default route:
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}.ext/{id?}");
A request for /Home/Index.ext then gives the error:
An unhandled exception occurred while processing the request.
AmbiguousMatchException: The request matched multiple endpoints. Matches:
WebApplication1.Controllers.HomeController.Index (WebApplication1)
WebApplication1.Controllers.HomeController.Privacy (WebApplication1)
WebApplication1.Controllers.HomeController.Error (WebApplication1)
Microsoft.AspNetCore.Routing.Matching.DefaultEndpointSelector.ReportAmbiguity(CandidateState[] candidateState)
I can't see why it's not taking the action name from this URL to select the correct action. Is there some way I can change this route pattern to get it to match this correctly?
You can do this with the IOutboundParameterTransformer that's inside Microsoft.AspNetCore.Routing.
Simply create a transformer as follows (if you want the .ext extension):
public class AddExtensionTransformer : IOutboundParameterTransformer
{
public string TransformOutbound(object value)
=> value + ".ext";
}
Then you register it as a constraint (I used the name addextension):
services.Configure<RouteOptions>(options =>
{
options.ConstraintMap["addextension"] = typeof(AddExtensionTransformer);
});
And then in your route, you simply use addextension as a route constraint (using the : delimiter).
For the default route from your post, it will look like this:
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action:addextension=Index}/{id?}");
ASP.NET Core will now also automatically generate URLs like /Home/Index.ext whenever you tell it to generate an action URL.
So it not only parses them but also generates them (works both ways).
You can then use an Attribute Route like this:-
[Route("Home/Index.ext")]
public ActionResult Index() {... }
For more information read this Documentation
You can use URL Rewriting Middleware like below(html extension as a sample):
1.Custom a class containing ReWriteRequests
public class RewriteRules
{
public static void ReWriteRequests(RewriteContext context)
{
var request = context.HttpContext.Request;
if (request.Path.Value.EndsWith(".html", StringComparison.OrdinalIgnoreCase))
{
context.HttpContext.Request.Path = context.HttpContext.Request.Path.Value.Replace(".html", "");
}
}
}
2.Startup.cs:
Be sure use URL Rewriting Middleware before app.UseStaticFiles();
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRewriter(new RewriteOptions()
.Add(RewriteRules.ReWriteRequests)
);
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
}
Reference:
URL Rewriting Middleware in ASP.NET Core

ASP.NET Core map route to static file handler

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).

ASP.NET MVC QueryString defaults overriding supplied values?

Using ASP.NET MVC Preview 5 (though this has also been tried with the Beta), it appears that querystring defaults in a route override the value that is passed in on the query string. A repro is to write a controller like this:
public class TestController : Controller
{
public ActionResult Foo(int x)
{
Trace.WriteLine(x);
Trace.WriteLine(this.HttpContext.Request.QueryString["x"]);
return new EmptyResult();
}
}
With route mapped as follows:
routes.MapRoute(
"test",
"Test/Foo",
new { controller = "Test", action = "Foo", x = 1 });
And then invoke it with this relative URI:
/Test/Foo?x=5
The trace output I see is:
1
5
So in other words the default value that was set up for the route is always passed into the method, irrespective of whether it was actually supplied on the querystring. Note that if the default for the querystring is removed, i.e. the route is mapped as follows:
routes.MapRoute(
"test",
"Test/Foo",
new { controller = "Test", action = "Foo" });
Then the controller behaves as expected and the value is passed in as the parameter value, giving the trace output:
5
5
This looks to me like a bug, but I would find it very surprising that a bug like this could still be in the beta release of the ASP.NET MVC framework, as querystrings with defaults aren't exactly an esoteric or edge-case feature, so it's almost certainly my fault. Any ideas what I'm doing wrong?
The best way to look at ASP.NET MVC with QueryStrings is to think of them as values that the route does not know about. As you found out, the QueryString is not part of the RouteData, therefore, you should keep what you are passing as a query string separate from the route values.
A way to work around them is to create default values yourself in the action if the values passed from the QueryString are null.
In your example, the route knows about x, therefore your url should really look like this:
/Test/Foo or /Test/Foo/5
and the route should look like this:
routes.MapRoute("test", "Test/Foo/{x}", new {controller = "Test", action = "Foo", x = 1});
To get the behavior you were looking for.
If you want to pass a QueryString value, say like a page number then you would do this:
/Test/Foo/5?page=1
And your action should change like this:
public ActionResult Foo(int x, int? page)
{
Trace.WriteLine(x);
Trace.WriteLine(page.HasValue ? page.Value : 1);
return new EmptyResult();
}
Now the test:
Url: /Test/Foo
Trace:
1
1
Url: /Test/Foo/5
Trace:
5
1
Url: /Test/Foo/5?page=2
Trace:
5
2
Url: /Test/Foo?page=2
Trace:
1
2
Hope this helps clarify some things.
One of my colleagues found a link which indicates that this is by design and it appears the author of that article raised an issue with the MVC team saying this was a change from earlier releases. The response from them was below (for "page" you can read "x" to have it relate to the question above):
This is by design. Routing does not
concern itself with query string
values; it concerns itself only with
values from RouteData. You should
instead remove the entry for "page"
from the Defaults dictionary, and in
either the action method itself or in
a filter set the default value for
"page" if it has not already been set.
We hope to in the future have an
easier way to mark a parameter as
explicitly coming from RouteData, the
query string, or a form. Until that is
implemented the above solution should
work. Please let us know if it
doesn't!
So it appears that this behaviour is 'correct', however it is so orthogonal to the principle of least astonishment that I still can't quite believe it.
Edit #1: Note that the post details a method of how to provide default values, however this no longer works as the ActionMethod property he uses to access the MethodInfo has been removed in the latest version of ASP.NET MVC. I'm currently working on an alternative and will post it when done.
Edit #2: I've updated the idea in the linked post to work with the Preview 5 release of ASP.NET MVC and I believe it should also work with the Beta release though I can't guarantee it as we haven't moved to that release yet. It's so simple that I've just posted it inline here.
First there's the default attribute (we can't use the existing .NET DefaultValueAttribute as it needs to inherit from CustomModelBinderAttribute):
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class DefaultAttribute : CustomModelBinderAttribute
{
private readonly object value;
public DefaultAttribute(object value)
{
this.value = value;
}
public DefaultAttribute(string value, Type conversionType)
{
this.value = Convert.ChangeType(value, conversionType);
}
public override IModelBinder GetBinder()
{
return new DefaultValueModelBinder(this.value);
}
}
The the custom binder:
public sealed class DefaultValueModelBinder : IModelBinder
{
private readonly object value;
public DefaultValueModelBinder(object value)
{
this.value = value;
}
public ModelBinderResult BindModel(ModelBindingContext bindingContext)
{
var request = bindingContext.HttpContext.Request;
var queryValue = request .QueryString[bindingContext.ModelName];
return string.IsNullOrEmpty(queryValue)
? new ModelBinderResult(this.value)
: new DefaultModelBinder().BindModel(bindingContext);
}
}
And then you can simply apply it to the method parameters that come in on the querystring, e.g.
public ActionResult Foo([Default(1)] int x)
{
// implementation
}
Works like a charm!
I think the reason querystring parameters do not override the defaults is to stop people hacking the url.
Someone could use a url whose querystring included controller, action or other defaults you didn't want them to change.
I've dealt with this problem by doing what #Dale-Ragan suggested and dealing with it in the action method. Works for me.
I thought the point with Routing in MVC is to get rid of querystrings. Like this:
routes.MapRoute(
"test",
"Test/Foo/{x}",
new { controller = "Test", action = "Foo", x = 1 });

Is it possible to run ASP.NET MVC routes in different AppDomains?

I am having problems with thinking up a solution for the following. I got a blog which I recently upgraded from web forms to MVC. The blog is avalible in both swedish and english on two different domains and are running in the same web site in IIS.
The problem is that I would like language specific urls on the both sites, like this:
English: http://codeodyssey.com/archive/2009/1/15/code-odyssey-the-next-chapter
Swedish: http://codeodyssey.se/arkiv/2009/1/15/code-odyssey-nasta-kapitel
At the moment I have made this to work by registering the RouteTable on each request depending on which domain is called. My Global.asax Looks something like this (not the whole code):
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
string archiveRoute = "archive";
if (Thread.CurrentThread.CurrentUICulture.ToString() == "sv-SE")
{
archiveRoute = "arkiv";
}
routes.MapRoute(
"BlogPost",
archiveRoute+"/{year}/{month}/{day}/{slug}",
new { controller = "Blog", action = "ArchiveBySlug" }
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
routes.MapRoute(
"404-PageNotFound",
"{*url}",
new { controller = "Error", action = "ResourceNotFound" }
);
}
void Application_BeginRequest(object sender, EventArgs e)
{
//Check whcih domian the request is made for, and store the Culture
string currentCulture = HttpContext.Current.Request.Url.ToString().IndexOf("codeodyssey.se") != -1 ? "sv-SE" : "en-GB";
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(currentCulture);
Thread.CurrentThread.CurrentUICulture = new CultureInfo(currentCulture);
RouteTable.Routes.Clear();
RegisterRoutes(RouteTable.Routes);
Bootstrapper.ConfigureStructureMap();
ControllerBuilder.Current.SetControllerFactory(
new CodeOdyssey.Web.Controllers.StructureMapControllerFactory()
);
}
protected void Application_Start()
{
}
This works at the moment but I know it not a great solution. I have been getting a "Item has already been added. Key in dictionary" error when stating up this app and it does not seems stable at times.
I would like to only set up my routes in the Application_Start as they should and not having to clear them on every request like I am doing now. Problem is that the request object does not exist and I have no way of knowing which of the language specific routes I should register.
Been reading about the AppDomain but could not find many examples on how to use it on a web site. I'we been thinking to star something like this:
protected void Application_Start()
{
AppDomain.CreateDomain("codeodyssey.se");
AppDomain.CreateDomain("codeodyssey.com");
}
Then registring each web sites routes in each app domain and send the requests to one of them based on the url. Can't find any examples on how to work with AppDomains in this manner.
Am I completely off track? Or is there a better solution for this?
The ASP.Net runtime manages AppDomains for you, so its probably not a good idea to create AppDomains in your code.
However, if you can, I would suggest creating multiple IIS Applications (one for http://codeodyssey.com and one for http://codeodyssey.se). Point both applications at the same directory on disk. This will give you the two AppDomains you are looking for.
Then, in your Application_Start code, you can check the domain and build routes accordingly.

Asp.Net MVC: How do I enable dashes in my urls?

I'd like to have dashes separate words in my URLs. So instead of:
/MyController/MyAction
I'd like:
/My-Controller/My-Action
Is this possible?
You can use the ActionName attribute like so:
[ActionName("My-Action")]
public ActionResult MyAction() {
return View();
}
Note that you will then need to call your View file "My-Action.cshtml" (or appropriate extension). You will also need to reference "my-action" in any Html.ActionLink methods.
There isn't such a simple solution for controllers.
Edit: Update for MVC5
Enable the routes globally:
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapMvcAttributeRoutes();
// routes.MapRoute...
}
Now with MVC5, Attribute Routing has been absorbed into the project. You can now use:
[Route("My-Action")]
On Action Methods.
For controllers, you can apply a RoutePrefix attribute which will be applied to all action methods in that controller:
[RoutePrefix("my-controller")]
One of the benefits of using RoutePrefix is URL parameters will also be passed down to any action methods.
[RoutePrefix("clients/{clientId:int}")]
public class ClientsController : Controller .....
Snip..
[Route("edit-client")]
public ActionResult Edit(int clientId) // will match /clients/123/edit-client
You could create a custom route handler as shown in this blog:
http://blog.didsburydesign.com/2010/02/how-to-allow-hyphens-in-urls-using-asp-net-mvc-2/
public class HyphenatedRouteHandler : MvcRouteHandler{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
requestContext.RouteData.Values["controller"] = requestContext.RouteData.Values["controller"].ToString().Replace("-", "_");
requestContext.RouteData.Values["action"] = requestContext.RouteData.Values["action"].ToString().Replace("-", "_");
return base.GetHttpHandler(requestContext);
}
}
...and the new route:
routes.Add(
new Route("{controller}/{action}/{id}",
new RouteValueDictionary(
new { controller = "Default", action = "Index", id = "" }),
new HyphenatedRouteHandler())
);
A very similar question was asked here: ASP.net MVC support for URL's with hyphens
I've developed an open source NuGet library for this problem which implicitly converts EveryMvc/Url to every-mvc/url.
Uppercase urls are problematic because cookie paths are case-sensitive, most of the internet is actually case-sensitive while Microsoft technologies treats urls as case-insensitive. (More on my blog post)
NuGet Package: https://www.nuget.org/packages/LowercaseDashedRoute/
To install it, simply open the NuGet window in the Visual Studio by right clicking the Project and selecting NuGet Package Manager, and on the "Online" tab type "Lowercase Dashed Route", and it should pop up.
Alternatively, you can run this code in the Package Manager Console:
Install-Package LowercaseDashedRoute
After that you should open App_Start/RouteConfig.cs and comment out existing route.MapRoute(...) call and add this instead:
routes.Add(new LowercaseDashedRoute("{controller}/{action}/{id}",
new RouteValueDictionary(
new { controller = "Home", action = "Index", id = UrlParameter.Optional }),
new DashedRouteHandler()
)
);
That's it. All the urls are lowercase, dashed, and converted implicitly without you doing anything more.
Open Source Project Url: https://github.com/AtaS/lowercase-dashed-route
Here's what I did using areas in ASP.NET MVC 5 and it worked liked a charm. I didn't have to rename my views, either.
In RouteConfig.cs, do this:
public static void RegisterRoutes(RouteCollection routes)
{
// add these to enable attribute routing and lowercase urls, if desired
routes.MapMvcAttributeRoutes();
routes.LowercaseUrls = true;
// routes.MapRoute...
}
In your controller, add this before your class definition:
[RouteArea("SampleArea", AreaPrefix = "sample-area")]
[Route("{action}")]
public class SampleAreaController: Controller
{
// ...
[Route("my-action")]
public ViewResult MyAction()
{
// do something useful
}
}
The URL that shows up in the browser if testing on local machine is: localhost/sample-area/my-action. You don't need to rename your view files or anything. I was quite happy with the end result.
After routing attributes are enabled you can delete any area registration files you have such as SampleAreaRegistration.cs.
This article helped me come to this conclusion. I hope it is useful to you.
Asp.Net MVC 5 will support attribute routing, allowing more explicit control over route names. Sample usage will look like:
[RoutePrefix("dogs-and-cats")]
public class DogsAndCatsController : Controller
{
[HttpGet("living-together")]
public ViewResult LivingTogether() { ... }
[HttpPost("mass-hysteria")]
public ViewResult MassHysteria() { }
}
To get this behavior for projects using Asp.Net MVC prior to v5, similar functionality can be found with the AttributeRouting project (also available as a nuget). In fact, Microsoft reached out to the author of AttributeRouting to help them with their implementation for MVC 5.
You could write a custom route that derives from the Route class GetRouteData to strip dashes, but when you call the APIs to generate a URL, you'll have to remember to include the dashes for action name and controller name.
That shouldn't be too hard.
You can define a specific route such as:
routes.MapRoute(
"TandC", // Route controllerName
"CommonPath/{controller}/Terms-and-Conditions", // URL with parameters
new {
controller = "Home",
action = "Terms_and_Conditions"
} // Parameter defaults
);
But this route has to be registered BEFORE your default route.
If you have access to the IIS URL Rewrite module ( http://blogs.iis.net/ruslany/archive/2009/04/08/10-url-rewriting-tips-and-tricks.aspx ), you can simply rewrite the URLs.
Requests to /my-controller/my-action can be rewritten to /mycontroller/myaction and then there is no need to write custom handlers or anything else. Visitors get pretty urls and you get ones MVC can understand.
Here's an example for one controller and action, but you could modify this to be a more generic solution:
<rewrite>
<rules>
<rule name="Dashes, damnit">
<match url="^my-controller(.*)" />
<action type="Rewrite" url="MyController/Index{R:1}" />
</rule>
</rules>
</rewrite>
The possible downside to this is you'll have to switch your project to use IIS Express or IIS for rewrites to work during development.
I'm still pretty new to MVC, so take it with a grain of salt. It's not an elegant, catch-all solution but did the trick for me in MVC4:
routes.MapRoute(
name: "ControllerName",
url: "Controller-Name/{action}/{id}",
defaults: new { controller = "ControllerName", action = "Index", id = UrlParameter.Optional }
);

Resources