How to work around "A catch-all parameter can only appear as the last segment of the route template." - asp.net-core

If I have a controller with an action method that uses attribute based routing and declare it like this, all is well:
[HttpGet]
[Route("/dev/info/{*somevalue}")]
public IActionResult Get(string somevalue) {
return View();
}
I can route to the above action method for example by specifying a url that ends in /dev/info/hello-world or /dev/info/new-world
However my business requirement is to have a urls that look like this: /dev/hello-world/info or /dev/new-world/info And there is an endless set of such urls that all need to route to the same action method on the controller.
I thought to set up the attribute based route on the action method as follows:
[HttpGet]
[Route("/dev/{*somevalue}/info/")]
public IActionResult Get(string somevalue) {
return View();
}
But when I do that I get the following error as soon as the web project runs:
An unhandled exception occurred while processing the request.
RouteCreationException: The following errors occurred with attribute routing information:
For action: 'App.SomeController.Get (1-wwwSomeProject)'
Error: A catch-all parameter can only appear as the last segment of the route template.
Parameter name: routeTemplate
Microsoft.AspNetCore.Mvc.Internal.AttributeRoute.GetRouteInfos(IReadOnlyList actions)
There has to be some way to work around this error. Know a way?

Middleware is the way to achieve this.
If you need an api response is easy to implement inline.
if (app.Environment.IsDevelopment())
{
app.Use(async (context, next) =>
{
Console.WriteLine(context.Request.Path);
if (context.Request.Path.ToString().EndsWith("/info"))
{
// some logic
await context.Response.WriteAsync("Terminal Middleware.");
return;
}
await next(context);
});
}
If you need to call a controller you can simply edit request path via middleware to achieve your requirement.
You can find an example here: https://stackoverflow.com/a/50010787/3120219

It is possible to achieve this by using the regular expression:
[HttpGet]
[Route(#"/dev/{somevalue:regex(^.*$)}/info/")]
public IActionResult Get(string somevalue)
{
return View();
}
About routing constrain using the regular expressions see in the documentation: Route constraint reference
The regular expression tokens explanation:
Token
Explanation
^
Asserts position at start of a line
.
Matches any character (except for line terminators)
*
Matches the previous token between zero and unlimited times, as many times as possible
$
Asserts position at the end of a line
If it's required to have the “world”suffix in the second segment then add this suffix to the pattern like the following: [Route(#"/dev/{somevalue:regex(^.*world$)}/info/")].

Related

How to return a status code from an endpoint that can then be handled by app.UseStatusCodePages() middleware?

If I return StatusCode(403) or any other error code from an endpoint, any configuration of app.UseStatusCodePages<whatever> will be ignored.
I believe this is because the StatusCode(<whatever>) will automatically create a result object, and UseStatusCodePages only kicks in if there is an error status code and no content.
So how do I set a status code result in an IActionResult type endpoint and then return without setting any content so that UseStatusCodePages will handle the job of providing a suitable resonse?
As far as I know, the UseStatusCodePages will just be fired when the action result is the StatusCodeResult.
If you put some value inside the status codes, it will return the object result which will not trigger the UseStatusCodePages.
So I suggest you could directly use StatusCodeResult(403), then if you want to put some value to the StatusCodeResult, I suggest you could put it inside the httpcontext's item.
More details, you could refer to below codes:
public IActionResult OnGet()
{
HttpContext.Items.Add("test","1");
return StatusCode(403);
}
Program.cs:
app.UseStatusCodePages(async statusCodeContext =>
{
var status = statusCodeContext.HttpContext.Items["test"];
// using static System.Net.Mime.MediaTypeNames;
statusCodeContext.HttpContext.Response.ContentType = Text.Plain;
await statusCodeContext.HttpContext.Response.WriteAsync(
$"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});
Result:
The issue was that I have the ApiController attribute on the endpoint controller. One of the things this attribute does is to automatically create a ProblemDetails response body for any failed requests, and it is this that prevents UseStatusCodePages from having any effect.
The solution is to either remove the ApiController attribute if you do not require any of its features, or alternatively its behaviour of automatically creating ProblemDetails responses can be disabled using the following configuration in Program.cs (or Startup.cs in old style projects).
builder.Services.AddControllers().ConfigureApiBehaviorOptions(options =>
{
options.SuppressMapClientErrors = true;
});

Using dash in the URL query in ASP.NET Core

Can we use dashes (-) in the Route template in ASP.NET Core?
// GET: api/customers/5/orders?active-orders=true
[Route("customers/{customer-id}/orders")]
public IActionResult GetCustomerOrders(int customerId, bool activeOrders)
{
.
.
.
}
(The above code doesn't work)
The route parameters usually directly map to the action's variable name, so [Route("customers/{customerId}/orders")] should work since that's the name of your variable (int customerId).
You don't need dashes there, the part within the curly braces {} will never appear as a part of the generated url, it will always be replaced by the content you pass from browser or the variables you pass to the url generator.
customers/{customerId}/orders will always be customers/1/orders when customerId is set to 1, so there's no point trying to force it to {customer-id}.
However, you can try public
[Route("customers/{customer-id}/orders")]
IActionResult GetCustomerOrders([FromRoute(Name = "customer-id")]int customerId, bool activeOrders)
to bind the customerId from a unconventional route name, if you wish. But I'd strongly advise against it, as it just adds unnecessary code which has absolutely zero-effect on your generated urls.
The above generates (and parses) the exactly same url as
[Route("customers/{customerId}/orders")]
IActionResult GetCustomerOrders(int customerId, bool activeOrders)
and is much more readable code.
For the query part, as you figured it out in the comments, it makes sense to add the dashes via [FromQuery(Name = "active-orders")] bool activeOrders, since that really affects the generated url.
New in ASP.NET Core 2.2
In ASP.NET Core 2.2 you'll get a new option to 'slugify' your routes (only supported when using the new Route Dispatcher instead of the default Mvc Router).
A route of blog\{article:slugify} will (when used with Url.Action(new { article = "MyTestArticle" })) generate blog\my-test-article as url.
Can also be used in default routes:
routes.MapRoute(
name: "default",
template: "{controller=Home:slugify}/{action=Index:slugify}/{id?}");
For further details see the ASP.NET Core 2.2-preview 3 annoucement.
Just expanding on Tseng answer to the question. for ASP NET CORE to use "slugify" transformer you need to register it first like so:
public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
public string TransformOutbound(object value)
{
if (value == null) { return null; }
return Regex.Replace(value.ToString(),
"([a-z])([A-Z])",
"$1-$2",
RegexOptions.CultureInvariant,
TimeSpan.FromMilliseconds(100)).ToLowerInvariant();
}
}
and then in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddRouting(options =>
{
options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
});
}
Code from Microsoft

Route to allow a parameter from both query string and default {id} template

I have an action in my ASP.Net Core WebAPI Controller which takes one parameter. I'm trying to configure it to be able to call it in following forms:
api/{controller}/{action}/{id}
api/{controller}/{action}?id={id}
I can't seem to get the routing right, as I can only make one form to be recognized. The (simplified) action signature looks like this: public ActionResult<string> Get(Guid id). These are the routes I've tried:
[HttpGet("Get")] -- mapped to api/MyController/Get?id=...
[HttpGet("Get/{id}")] -- mapped to api/MyController/Get/...
both of them -- mapped to api/MyController/Get/...
How can I configure my action to be called using both URL forms?
if you want to use route templates
you can provide one in Startup.cs Configure Method Like This:
app.UseMvc(o =>
{
o.MapRoute("main", "{controller}/{action}/{id?}");
});
now you can use both of request addresses.
If you want to use the attribute routing you can use the same way:
[HttpGet("Get/{id?}")]
public async ValueTask<IActionResult> Get(
Guid id)
{
return Ok(id);
}
Make the parameter optional
[Route("api/MyController")]
public class MyController: Controller {
//GET api/MyController/Get
//GET api/MyController/Get/{285A477F-22A7-4691-AA51-08247FB93F7E}
//GET api/MyController/Get?id={285A477F-22A7-4691-AA51-08247FB93F7E}
[HttpGet("Get/{id:guid?}"
public ActionResult<string> Get(Guid? id) {
if(id == null)
return BadRequest();
//...
}
}
This however means that you would need to do some validation of the parameter in the action to account for the fact that it can be passed in as null because of the action being able to accept api/MyController/Get on its own.
Reference Routing to controller actions in ASP.NET Core

Appending hash/fragment to RedirectResult results in cumbersome code

The code works but is silly.
When the View is returned to the user the page scrolls to the companyId anchor.
Silly is that I have to expose another public action with another route (without 'terms')
I want to redirect to /terms/companyId but then I get an ambigiousAction exception that this action with same routes already exists...
How to solve that dilemma if possible not change the first route?
[HttpGet("~/terms/{companyId}")]
public IActionResult Home(string companyId})
{
string url = Url.Action(nameof(HomeForRedirect), new { companyId}) + "#conditions";
return new RedirectResult(url);
}
[HttpGet("{companyId}")]
public IActionResult HomeForRedirect(string companyId)
{
Viewbag.CompanyId = companyId;
return View(nameof(Home));
}
If I'm understanding your code, you essentially want the URL /terms/{companyId} to redirect to /{controller}/{companyId}#conditions? The easiest path would be to attach both routes to the same action and do the redirect in a conditional. Something like:
[HttpGet("{companyId}", Order = 1)]
[HttpGet("~/terms/{companyId}", Order = 2)]
public IActionResult Home(string companyId)
{
if (Context.Request.Path.StartsWith("/terms"))
{
var url = Url.Action(nameof(Home), new { companyId }) + "#conditions";
return Redirect(url);
}
ViewBag.CompanyId = companyId;
return View();
}
An even better method would be to simply do the redirect directly in IIS. There's a not insignificant amount of processing that needs to occur to handle a request in ASP.NET Core machinery, and it's totally wasted effort simply to redirect. Use the URL Rewrite module in IIS to set up your redirect for this URL, and then your application doesn't have to worry about it at all. You just have your normal run-of-the-mill Home action that returns a view, and everything will just work.
A few other notes since it seems like you're new to this:
It's better to use the Route attribute rather than the more specific HttpGet etc. The default is GET.
Return the controller methods like Redirect rather than instances of IActionResult (i.e. new RedirectResult(...)).
The default is to return a view the same name as the action. So, assuming your action is Home, you can just do return View(), rather than return View(nameof(Home)).

Adding a WEB API method ruins my SWAGGER UI

This first method is fine. But when I add the second method the body of the SWAGGER UI is a bunch of html gibberish. And I creating the route the wrong way?
// GET api/checklist/1288
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
var model = _checkListService.Get(id);
return Ok(model);
}
// http://localhost:64783/api/checklist/GetDelinquentItems?id=1288
[Route("GetDelinquentItems")]
public async Task<IActionResult> GetDelinquentItems(int id)
{
var model = _checkListService.GetDelinquentItems(id);
return Ok(model);
}
That 'html gibberish' (indeed not the most elegant way to show an error) still contains some useful information. The first line says:
500 internal server error
and in the last three lines you can read:
Ambiguos HTTP method for action...CheckListController.GetDelinquentItems... Actions require explicit HttpMethod binding for Swagger
therefore another
[HttpGet("{id}")]
before the GetDelinquentItems() method should solve the problem.