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
Related
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)
{
}
This is my api configuration class:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}"
);
}
}
This is my api controller class:
public class RoleController : ApiController
{
// Some action that works fine...
// Another action that works fine...
public Result Delete([FromBody]int RoleID)
{
return RoleBL.Delete(RoleID);
}
}
I am calling my actions using POST and they are working fine.
But, when I try to call the Delete action using POST I get the following error:
405 Method Not Allowed
The requested resource does not support http method 'POST'.
Clearly, this is because ApiController enforces REST convention
which expects DELETE verb for Delete action.
Now, how do I disable this REST convention constraints
and write my actions in a classic manner?
You can use the HttpPostAttribute to enforce the Action to accept only POST:
public class RoleController : ApiController
{
[HttpPost]
public Result Delete([FromBody]int RoleID)
{
return RoleBL.Delete(RoleID);
}
}
You may want to keep the REST conventions while allowing certain clients (like HTML forms) to properly use you actions.
So, you can use a combination of HttpPostAttribute and HttpDeleteAttribute or AcceptVerbsAttribute (which allows multiple verbs) to allow multiple verbs:
public class RoleController : ApiController
{
[HttpPost, HttpDelete]
// OR
[AcceptVerbs("DELETE", "POST")
public Result Delete([FromBody]int RoleID)
{
return RoleBL.Delete(RoleID);
}
}
If you don't want magic verbs and magic action names you can use route attributes.
Delete config.Routes.MapHttpRoute and set:
config.MapHttpAttributeRoutes();
Now you have to set the routes manually:
[RoutePrefix("~/Role")]
public class RoleController : ApiController
{
[HttpPost]
[Route("~/Delete")]
public Result Delete([FromBody]int RoleID)
{
return RoleBL.Delete(RoleID);
}
}
In your case I'd stop using any kind of REST conventions.
Instead of having a Delete method on the Role controller you can have a DeleteRole method and allow POST on it. This way nothing will interfere with what you want to do. Nothing forces you to build a REST api if that's not what you need.
There are several things you could do to still build a nice api.
For example, you could return an IHttpActionResult
your method could look like this:
public class RoleController : ApiController
{
[HttpPost]
public IHttpActionResult DeleteRole([FromBody]int RoleID)
{
if ( RoleID <= 0 )
{
return BadRequest();
}
var deleteResult = RoleBL.Delete(RoleID);
return Ok(deleteResult);
}
}
You still return the same object but it's wrapped inside an object with a proper http code so your code which deals with the result, won't change much.
Looking for best practices when working with nested routes in .NET Core MVC.
Let's say CampusController.cs works with a base model:
[Route("api/campus/")]
public class CampusController : Controller
{
...
[HttpGet]
[Route("{campusId}")]
public IActionResult GetCampusInfo ([FromQuery]int campusId) { ... }
}
And BuildingController.cs works with a child model:
[Route("api/campus/{campusId}/building")]
public class BuildingController : Controller
{
...
[HttpGet]
[Route("{buildingId}")]
public IActionResult GetBuilding ([FromQuery]int buildingId) { ... }
[Route("{buildingId}/")]
public IActionResult GetBuilding ([FromQuery]int buildingId) { ... }
....
(more Action Methods)
}
If buildingId maps directly to the database it could retrieved even if the provided campusId isn't the parent. To keep the URL clean when calling /api/campus/{campusId}/building/{buildingId} I'd like to validate {campusId} and return a 4xx coded IActionResult if it's invalid. There has to be a better way than including validation logic in every Action Method inside BuildingController.
Is there a way to cascade multiple Action methods on different controllers? So that a validation method on CampusController would be called first and in turn call a method onBuildingController?
Is there a way to have a controller-level verification of campusId that could short circuit and return a ActionResult if validation fails?
EDIT: When I refer to validation logic I mean API signals; not the business-logic that actually determines if campusId is/isn't valid.
Thanks in advance!
If using placeholder in the route prefix you would also need to include it in the action itself
[Route("api/campus/{campusId:int}/building")]
public class BuildingController : Controller {
//...
[HttpGet]
[Route("{buildingId:int}")] // Matches GET api/campus/123/building/456
public IActionResult GetBuilding ([FromRoute]int campusId, [FromRoute]int buildingId) {
//... validate campus id along with building id
}
}
If concerned about repeated code for validation then create a base controller for campus related request and have a shared validation method.
Another option is to have a service/repository that can be used to verify campus id and its relation to the provided building id if needed.
It sounds like you want your users to provide a campusId when talking to the BuildingController, and your BuildingController to validate campusId in a DRY kind of way.
If that's the case, you can create an input model for your BuildingController methods:
public class BuildingIdInput
{
[Required]
public int? CampusId { get; set; }
[Required]
public int? BuildingId { get; set; }
}
Then you can let MVC bind user input to this model.
[Route("api/campus")]
public class BuildingController : Controller
{
[HttpGet]
[Route("{campusId}/building/{buildingId}")]
public IActionResult GetBuilding (BuildingIdInput input)
{
if (ModelState.IsValid)
{...}
}
}
Is it possible to achieve url parameter and attribute based versioning with same Controller methods. To explain that, suppose I have one controller like,
[RoutePrefix("api/{apiVersion:apiVersionConstraint(v2)}/values")]
public class ValuesController : ApiController
{
// GET api/v2/values
[Route("")]
public IEnumerable<string> Get()
{
return new string[] { "v2-value1", "v2-value2" };
}
// GET api/v2/values/5
[Route("{id}")]
public string Get(int id)
{
return "v2-value-" + id;
}
}
Now, I want to access the API endpoint by both of the following URL's:
http://hostname/context/api/v1/values
http://hostname/context/api/values?v=1
Is it possible?
N.B. I'm using the example at WebApiNamespaceVersion in GitHub
So I am familiar with how to write the default Get, Post, Put, Delete
//GET api/customer
public string Get(){}
//GET api/customer/id
public string Get(int id){}
//POST api/customer
public void Post([FromBody]string value){}
//PUT api/customer/id
public void Put(int id, [FromBody]string value){}
//DELETE api/customer/id
public void Delete(int id){}
But how would I write add another Get endpoint w/o having to create a whole new controller? I want to grab the customer's metadata? do I need to make any changes to the routeConfig? if so how would I do that? and then how would I use the new route in javascript?
//GET api/customer/GetMetaData
public string GetMetaData(){
}
You use the Attribute Route. This attribute was added in WebApi 20 and you can use it at Method level to define new route or more routes and the way you use it is like [Route("Url/route1/route1")]:
Using one of your examples above it will be like:
//GET api/customer/GetMetaData
[Route("api/customer/GetMetaData")]
public string Get2(){
//your code goes here
}
If you will be declaring several Routes in your class then you can use RoutePrefix attribute like [RoutePrefix("url")] at class level. This will set a new base URL for all methods your in Controller class.
For example:
[RoutePrefix("api2/some")]
public class SomeController : ApiController
{
// GET api2/some
[Route("")]
public IEnumerable<Some> Get() { ... }
// GET api2/some/5
[Route("{id:int}")]
public Some Get(int id) { ... }
}
Note: In the example above I showed one example where Route allowed us to set type constraints as well.