.Net Api - Parent route matched for invalid nested route - c#

I have a .Net 5 API and some nested routes as follows:
[Route("api/v{version:apiVersion}/orders")]
[ApiVersion("1.0")]
[ApiController]
public class OrdersController: ControllerBase
{
[HttpGet("{userId:required}")]
public async Task<ActionResult> Get(string userId,
CancellationToken cancellationToken = default)
{
// return the orders corresponding to the userId
}
}
[Route("api/v{version:apiVersion}/orders/details")]
[ApiVersion("1.0")]
[ApiController]
public class OrdersDetailsController: ControllerBase
{
[HttpGet("{orderId:required}")]
public async Task<ActionResult> Get(string orderId,
CancellationToken cancellationToken = default)
{
// return the order details
}
}
Below is the list of responses I get when making requests to the API:
GET /orders/some_dummy_user_id returns the orders for userId="some_dummy_user_id", which is OK
GET /orders/details/some_dummy_order_id returns the details of orderId="some_dummy_order_id", which is OK
GET /orders/details/ tries to return the orders corresponding to userId="details" which is Not OK
The question is: is it possible to make the GET /orders/details/ request match the OrderDetailsController route and therefore return a 404 because of the missing orderId URL parameter?

try this
[Route("api/v{version:apiVersion}/orders")]
[ApiVersion("1.0")]
[ApiController]
public class OrdersController: ControllerBase
{
[HttpGet("{userId:required}")]
public async Task<ActionResult> Get(string userId,
CancellationToken cancellationToken = default)
{
if(userId.ToLower=="details") throw new HttpException(404, "not found");
// return the orders corresponding to the userId
}
}

It seems that unfortunately it's not possible to achieve the required scenario using only the .Net routing features. The possible solutions I see in this situation are:
As mentioned in Serge's answer, introduce a manual check and throw a 404 Exception if the userId matches the sub-route
Update the routes schema in order to prevent this scenario
As I'd like to achieve this using only the .Net routing features I've proceeded with the 2nd solution (especially as the impact on the overall routes schema wasn't major).

Related

ASP.NET Core Web API : route by query parameter

I am coming from a heavy Java/Spring background and trying to transition some knowledge over to ASP.NET Core 6.
In Spring, on a RestController, I am able to route the request based on the presence of a query parameter.
So a HttpRequest with the uri: /students?firstName=Kevin can be routed to a different controller method than a HttpRequest with the uri: /students.
In ASP.NET Core 6, I am unable to determine if the equivalent is possible after working through some examples and reading the documentation for Web API.
Here is what I am trying to achieve, is this possible using two methods and routing configuration that will discern which controller method to invoke based on the query parameter?
[ApiController]
[Route("Students")]
public class StudentHomeProfileController : ControllerBase
{
[HttpGet] //Route here when no parameters provided
public async Task<ActionResult<IEnumerable<Student>>> GetStudentAsync()
{
/* Code omitted */
}
[HttpGet] //Route here when firstName query param provided
public async Task<ActionResult<IEnumerable<Student>>> SearchStudentAsync([FromQuery] string firstName)
{
/* Code omitted */
}
}
While filtering by query parameters does not come with ASP.NET Core out of the box, it's not too hard to supply this functionality on your own.
When it comes to extensibility, ASP.NET has some superpowers, one of them is IActionConstraint, which
Supports conditional logic to determine whether or not an associated action is valid to be selected for the given request. (Source)
Creating an annotation to filter for query parameters is as easy as
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class QueryParameterConstraintAttribute : Attribute, IActionConstraint
{
private readonly string _parameterName;
public QueryParameterConstraintAttribute(string parameterName)
{
this._parameterName = parameterName;
}
public bool Accept(ActionConstraintContext context)
{
return context.RouteContext.HttpContext.Request.Query.Keys.Contains(this._parameterName);
}
public int Order { get; }
}
All that's left is annotating your controller method with that constraint
[HttpGet] //Route here when firstName query param provided
[QueryParameterConstraint("firstName")]
public async Task<ActionResult<IEnumerable<Student>>> SearchStudentAsync([FromQuery] string firstName)
{
/* Code omitted */
}
In a quick test I was able to confirm that it seems to work as intended, even if you add multiple of those attributes for different query parameters (if all conditions match, the route is called).
(Please note, this was tested with .NET Core 2.1. Anyway, it shuold be pretty much the same with .NET 6)
I think you are looking for something like this, you need to specify the parameter in the "HttpGet" attribute
https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-6.0#attribute-routing-with-http-verb-attributes
[Route("api/[controller]")]
[ApiController]
public class Test2Controller : ControllerBase
{
[HttpGet] // GET /api/test2
public IActionResult ListProducts()
{
return ControllerContext.MyDisplayRouteInfo();
}
[HttpGet("{id}")] // GET /api/test2/xyz
public IActionResult GetProduct(string id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
[HttpGet("int/{id:int}")] // GET /api/test2/int/3
public IActionResult GetIntProduct(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
[HttpGet("int2/{id}")] // GET /api/test2/int2/3
public IActionResult GetInt2Product(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}
You are trying to differentiate API calls using query params. this is not the way to do this. if you want to separate the calls you should probably use path params instead.
Read more about Routing in ASP.NET Core - https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-6.0

Cannot return HTML from a Controller

My code fails to return HTML from a Controller. The browser returns HTTP ERROR 500 - "This page does not work".
The Controller is written in .net core 3.1. Here is the code:
[ApiController]
[Route("")]
public class MyController : ControllerBase
{
[HttpGet]
public async Task<ActionResult<ContentResult>> GetInformation()
{
string s = #"<!DOCTYPE html><html><head><title>.....";
return base.Content(s, "text/html");
}
}
However, returning a plain string works: (using string instead of ContentResult. However, this string is not interpreted as HTML and is written directly to the browser)
[ApiController]
[Route("")]
public class MyController : ControllerBase
{
[HttpGet]
public async Task<ActionResult<string>> GetInformation()
{
string s = #"something";
return s;
}
}
public async Task<ActionResult<ContentResult>> GetInformation()
Because you declare that the action returns an ActionResult<ContentResult>, I expect that the ASP.NET Core serialisation process attempts to serialise the ContentResult you return as a JSON object. This is neither intended nor something the framework can manage.
Controller action return types in ASP.NET Core web API goes into the details, but a more typical method signature for your scenario would look like this:
public async Task<IActionResult> GetInformation()
You could also specify ContentResult, which implements IActionResult, if you'd prefer to do that:
public async Task<ContentResult> GetInformation()
IActionResult allows an action to return different responses in different situations, using e.g. BadRequest, Content, or Ok.

ASP.NET Core API - Dynamic Routing (Make API route behave like "one-parameter query string")

Aloha :D
I would like to create a dynamic route binding.
What I mean by this, is basically replacing the Query String with a dynamic route.
Example:
Instead of this:
POST http://localhost:5000/api/documents?templatename=individualemploymentagreement
this:
POST http://localhost:5000/api/documents/individualemploymentagreement
Note: after "http://localhost:5000/api/documents/" I want to put anything I want, but this route will always be used and what comes after should be used like a variable. Obviously, this will lead to a non-existing API Route at the moment. But is there any way to deal with this?
Note 2: The reasons I want to use this are:
- According to RESTful services "rules", query strings should be used just for queries, In this case I'm not using a query, I'm calling a generic document service, which however, treats every document slightly different when needed. So query strings are not recommended in my case.
- This service will deal with hundreds of document types, so I can't really make a different path / api for each one of them. So this is not recommended as well.
My code (In which I'm using a query string for {templateName}:
namespace DocumentGenerator.Api.Controllers
{
[Route("api/{controller}")]
[ApiController]
public class DocumentsController : ControllerBase
{
//useless details
[HttpPost]
public async Task<IActionResult> Generate([FromQuery] string templateName, [FromBody] object properties)
{
// according to {templateName} do this or that...
// useless details
}
}
}
What I would want in code:
namespace DocumentGenerator.Api.Controllers
{
[Route("api/{controller}")]
[ApiController]
public class DocumentsController : ControllerBase
{
//useless details
[HttpPost("{templateName}"]
public async Task<IActionResult> Generate([FromBody] object properties)
{
// according to {templateName} do this or that...
// useless details
}
}
}
You can specify the parameter name as a route attribute value in HttpPost :
[HttpPost("{templateName}"]
public async Task<IActionResult> Generate(string templateName, [FromBody] object properties)
{
}
or even
[HttpPost("/api/documents/{templateName}"]
public async Task<IActionResult> Generate(string templateName, [FromBody] object properties)
{
}

Swashbuckle crashes by just adding another action method in controller

I have just added another post method in a controller, and the Swagger Swashbuckle crashed.
How to solve this ?
[HttpPost]
public IActionResult CreateCars(List<Car> cars)
{
_carService.CreateCars(cars);
return NoContent();
}
System.NotSupportedException: HTTP method "POST" & path "api/Cars" overloaded by actions - IrkcnuApi.Controllers.CarsController.Create (WebApi),MyWebAPI.Controllers.CarsController.CreateCars (MyWebApi). Actions require unique method/path combination for OpenAPI 3.0. Use ConflictingActionsResolver as a workaround
at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateOperations(IEnumerable`1 apiDescriptions, SchemaRepository schemaRepository)
at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GeneratePaths(IEnumerable`1 apiDescriptions, SchemaRepository schemaRepository)
at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GetSwagger(String documentName, String host, String basePath)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
You already have a method in your controller that is attributed with a HttpPost attribute.
Since you do not explicitly specify a route, these operations clash.
You'll solve this by specifying a route for these POST operations, for instance:
[HttpPost("createMultiple")]
public IActionResult CreateCars(List<Car> cars) {}
[HttpPost()]
public IActionResult CreateCar(Car car) {}
The above suggestion is offcourse not that 'RESTfull', since you have verbs in your URLs.
I'd suggest to modify your code so that you only have one 'Create' method, since the above 2 operations are actually implicitely the same (I guess). Calling the CreateCars operation with a collection of Cars that only contains one item is in a sense actually identical to calling the CreateCar operation.
Use the following code to resolve the issue,
services.AddSwaggerGen(options =>
{
options.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
});
In my code i use Swagger Swashbuckle 5.5.1 and Microsoft.AspNetCore.Mvc.Versioning 4.1.1
For me [ApiExplorerSettings(GroupName = "vx.0")] where x is version of multiple action in the same controller or other controller, works fine.
I also use MapToApiVersion attribute togheter but the attribute ApiExplorerSettings avoid conflict.
See https://www.myget.org/feed/domaindrivendev/package/nuget/Swashbuckle.AspNetCore.Swagger
at "Decorate Individual Actions"
In my test i have 2 controllers. First controller maps version 1.0 and 2.0. Second controller map only version 3.0
First controller:
[Authorize]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("viewqlikapi")]
[Route("ViewQlikApi/v{version:apiVersion}")]
[ApiController]
public class QlikController : ControllerBase, IQlikController
And two action that have the same route
// Dati Pratica Audit
[HttpGet]
[ApiExplorerSettings(GroupName = "v1.0")]
[Route("datipraticaaudit")]
public RisultatoElementiPagina<ViewQlikDatiPraticaAudit GetElementiPaginaDatiPraticaAudit(int numeroElementi, int indicePagina)....
[HttpGet]
[MapToApiVersion("2.0")]
[ApiExplorerSettings(GroupName = "v2.0")]
[Route("datipraticaaudit")]
public RisultatoElementiPagina<ViewQlikDatiPraticaAudit> GetElementiPaginaDatiPraticaAuditV2(int numeroElementi, int indicePagina, int other)...
and the second controller..
[Authorize]
[ApiVersion("3.0")]
[Route("ViewQlikApi/v{version:apiVersion}")]
[ApiController]
public class QlikV2Controller : ControllerBase
and the action
[HttpGet]
[MapToApiVersion("3.0")]
[ApiExplorerSettings(GroupName = "v3.0")]
[Route("auditoperativoaccesso")]
public RisultatoElementiPagina<ViewQlikAuditOperativoAccesso> GetElementiPaginaAuditOperativoAccesso(int numeroElementi, int indicePagina, int o)

Web API: How to get a route parameter when controller action does not have corresponding argument

We build multi-tenancy application with Web API 2. We want to embed tenant info into the URL so that every request can get it. E.g. http://localhost/tenant1/api/test.
We added an action filter that should extract tenant from the request. However, it works only when controller action method signature has corresponding parameter.
Here is the code:
public class ValidationFilter : ActionFilterAttribute
{
public override Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
{
// works only if controller action has that argument
var tenant = actionContext.ActionArguments["tenant"];
Console.Out.WriteLine("tenant = {0}", tenant);
return base.OnActionExecutingAsync(actionContext, cancellationToken);
}
}
It works well when controller is like that:
[RoutePrefix("{tenant}")]
public class TestController : ApiController
{
[Route("api/test")]
public string Get(string tenant) // we don't need it here
{
return "hello";
}
}
The tenant is going to be used only in action filter, so adding it to each and every controller method seems to be a silly thing. We want that controller to be:
public string Get() {...}
Is there any way to obtain that value in action filter when controller action does not have a corresponding argument?
Found answer just after posting the question :)
actionContext.ControllerContext.RouteData.Values["tenant"]
just works even when action does not have that argument.

Categories