I have the following controller which I wanted to use as an Web API Controller for ajax posts to retrieve data from my user table.
namespace MyProjectName.Controllers.API
{
[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
private readonly myContext _context;
public UsersController(myContext context)
{
_context = context;
}
[HttpGet]
public List<string> GetInstitutionNamesById(int id)
{
// returns desired list
}
}
}
Now I'd expect the routing of this Function to be like this: /api/users/getinstitutionnamesbyid but apparently it seems to be just /api/users which I find really confusing (what if I add additional HttpGet Functions?).
Can anyone explain me what I am doing wrong? Am I using Web Api Controllers not the Intended way? Is my routing wrong?
Thanks in Advance.
[Route("api/[controller]")]
With this template, you're explicitly stating that you only care about the name of the controller. In your example, GetInstitutionNamesById is the name of the action, which isn't being considered by the template.
There are a few options for achieving what you're asking for here:
Change your [Route] template to include the action name:
[Route("api/[controller]/[action]")]
This option applies to all actions within your controller.
Change the HttpGet constraint attribute to specify the action implicitly:
[HttpGet("[action]")]
This option ensures that the name of your action method will always be used as the route
segment.
Change the HttpGet constraint attribute to specify the action explicitly:
[HttpGet("GetInstitutionNamesById")]
This option allows you to use a route segment that differs from the name of the action method itself.
In terms of whether you're using routing in the correct way here - that's somewhat opinion-based. Generally, you'll see that APIs are attempting to be RESTful, using route templates that match resources, etc. With this approach, you might have something more like the following:
/api/Users/{userId}/InstitutionNames
In this case, you might have a separate InstitutionNames controller or you might bundle it up into the Users controller. There really are many ways to do this, but I won't go into any more on that here as it's a little off-topic and opinion-based.
You just need to name it this way
[HttpGet("[action]/{id}")]
public List<string> GetInstitutionNamesById(int id)
{
// returns desired list
}
and from ajax call /api/users/GetInstitutionNamesById/1
Related
I have an ASP.net Core 6 web application where I have two controller actions similar to this:
[Route("auth")]
[ApiController]
public class AuthController: ControllerBase {
[HttpPost]
[Route("login/credentials")]
public async Task<IActionResult> LoginWithCredentials(...)
{
//...
}
[HttpPost]
[Route("login/token")]
public async Task<IActionResult> LoginWithToken(...)
{
//...
}
}
What I would like to do is having any requests that is made towards:
/auth/login
to actually be forwarded to either LoginWithCredentials() or LoginWithToken() depending on a server-side configuration.
In other words, I would like to dynamically chose the controller action for any request that matches a certain route.
Is this possible? If so, how can I do this?
NOTE: I've tried using a DynamicRouteValueTransformer but it doesn't seem to do what I want, it can change the values of route parameters but not change the controller action that is invoked (either that or I'm not using it correctly, please enlighten me if that's the case)
ADDITIONAL NOTE: I see people asking why I don't just use a single action with an if statement inside. The reason is that one of the two actions has a custom TypeFilterAttribute that I've omitted from the example for brevity. Thus, I can't just merge the two into one because then either the one that needs the attribute would not have it or vice-versa the one that doesn't need it would have it. And I can't change the source code of this custom filter either to "neuter" it when it is not needed, because it comes from a library that I'm not authorized to modify.
I have a working API with a bunch of controllers, with a single database specified in config file.
Now I want to make the the API multi database and make the target database a part of the url.
I use attributes on controllers now and default routing.
Startup.cs:
app.UseMVC();
FolderController.cs:
[Route("api/[controller]")]
[ApiController]
public class FoldersController : ControllerBase { ...
and action on controller:
[HttpGet("{Parent:Guid}", Name = "Get")]
public IActionResult Get(Guid Parent) {...
So what that gives me is the standard overall template that looks like this:
https://api.example.com/api/{controller}/{action}
What I'd want is to make the database a part of the url, the intuitive place being in front of the controller. I can also skip the second api bit as I'm not running anything else on that base address.
https://api.example.com/{database}/{controller}/{action}
I've been able to extract the database name by changing the controller attribute to:
[Route("{database}/[controller]")]
But then I'd have to insert code in every action method to check for route etc, with the risk of not implementing it consitently (beside the extra typing).
Ideally I'd like to add this to the default route in startup.cs, and add a service to the middleware that would check the privileges for the authenticated user on the requested database and continue as appropriate. That way I'd have my security in one place and no way to forget it in a controller.
I havent been able to figure out how to mix that with the attributes, they seem to conflict with each other.
Can this be done? Does anyone have some pointers for me get out of this?
By understand I know we can do it. You need to implement IHttpHandler.
You can refer to the following example https://www.c-sharpcorner.com/article/dynamic-and-friendly-url-using-mvc/
When ASP.NET Core encounters ambiguously named routes, it becomes inert. That is, the application will run without exceptions thrown but, it will fail to process any requests, on any controllers. The calling client receives 500 responses.
I'll show how I got into this mess, and I'd like suggestions of how to fix it.
I have a controller that looks like this:
[Route("api/Subscribers/{id}/[controller]")]
[Route("api/Organizations/{id}/[controller]")]
public class AddressesController : Controller
{
[HttpGet("{aid}", Name = "PostalLink")]
public async Task<IActionResult> GetAddress(Guid id, Guid aid)
{
//...implementation is irrelevant for this question.
}
[HttpPost]
[SwaggerResponseRemoveDefaults]
[SwaggerResponse(HttpStatusCode.Created, Type = typeof(PostalRecord))]
public async Task<IActionResult> CreateAddress(Guid id, [FromBody] PostalAddress address)
{
address.ID = Guid.NewGuid();
await createAddress.Handle(address);
return CreatedAtRoute("PostalLink", new { id = id, aid = address.ID });
}
Why the two route prefixes on the controller? Because it fits my microservices (and Swagger documentation) strategy. Nevertheless, in this example ASP.NET Core does not know how to resolve the route name "PostalLink" because it is implicitly bound to the two prefixes:
[Route("api/Subscribers/{id}/[controller]")]
[Route("api/Organizations/{id}/[controller]")]
I can fix the problem simply by changing the HttpGet so that instead of this:
[HttpGet("{aid}", Name = "PostalLink")]
I have this:
[HttpGet("{aid}")] //the route is no longer "named"
Unfortunately, removing the route name is not a real option for me.
What is the prescribed way to fix this?
Below are some of the options I'm considering.
Possibility #1
Theoretically, ASP.NET could simply "figure it out" by itself. For example, if the current request resolved to the route containing the word "Subscribers", then the "PostalLink" name should reference that route. Seen this way, perhaps my code is exposing a bug, defect, or oversight in ASP.NET Core.
Possibility #2
I could collapse my two prefix routes into a single route like this:
[Route("api/{parent}/{id}/[controller]")]
This works, but it undermines my REST documentation strategy. I'm using Swashbuckle to publish endpoint metadata. I want a user of my API to expressly see that my "Addresses" API is serving either "Subscribers" or "Organizations". When I have two explicit route prefixes, the Swagger documentation works correctly (and I properly validate the URI used by the client).
Possibility #3
I could simply override the two prefixes like this:
[HttpGet("~/api/Subscribers/{id}/Addresses/{aid}", Name = "SubscriberLink")]
[HttpGet("~/api/Organizations/{id}/Addresses/{aid}", Name = "OrganizationLink")]
public async Task<IActionResult> GetAddress(Guid id, Guid aid)
{
//...implementation is irrelevant for this question.
}
Now my documentation and route validation works, but my implementation is forced to check which route was used to reach the endpoint. That is very doable, but very annoying.
Possibility #4
Perhaps there is a more expressive way to handle this problem without attribute-based-routing? If yes, please share!
Details
My project.json is configured as follows:
"frameworks": {
"dnx46": { }
},
I am using DNX SDK version 1.0.0-rc1-update1. Also, I posted a related SO question for those who would like more context of what I am trying to do.
If your route names are the same for all your actions, why not specify them directly on the controller ?
[Route("api/Subscribers/{id}/[controller]", Name = "SubscriberLink")]
[Route("api/Organizations/{id}/[controller]", Name = "OrganizationLink")]
public class AddressesController : Controller
{
[HttpGet("{aid}")]
public async Task<IActionResult> GetAddress(Guid id, Guid aid)
{
//...implementation is irrelevant for this question.
}
}
Have you looked into attribute routing?
E.g. Registering routes with ASP.Net 5's MVC 6 Attribute Routing
Sample from the relevant documentation:
In the following example, app.UseMvc(); is used in the Configure method and no route is passed.
public class HomeController : Controller
{
[Route("")]
[Route("Home")]
[Route("Home/Index")]
public IActionResult Index()
{
return View();
}
[Route("Home/About")]
public IActionResult About()
{
return View();
}
[Route("Home/Contact")]
public IActionResult Contact()
{
return View();
}
}
The HomeController.Index() action will be executed for any of the URL paths /, /Home, or /Home/Index.
My controller currently looks like:
[Jsonp filter]
public class ProductController : Controller
{
public Json GetProduct(string id)
{
Product x;
//code
return Json(x, JsonRequestBehavior.AllowGet);
}
}
I am able to get a product doing this:
api/product/getproduct/5
But, I want to be able to access it like this:
api/product/5
What change do I need to make to do this?
EDIT: I am actually using Jsonp because I need to call this API from a different domain and get a json object back. Would this be possible using ApiController? Otherwise is there a way to do this without switching to ApiController?
You will have to edit your webapiconfig (located in the App_Start folder).
You will need to add something like this before any other route (to make sure it is caught first):
// Map Http Route takes in 3 parameters below
// param 1 is the name of the route.. This has nothing to do with class names or method names
// param 2 is the route itself. Route parameters are denoted in curly braces {likethis}
// param 3 sets up defaults
config.Routes.MapHttpRoute("GetProductApi", "api/product/{id}",
new {
controller = "Product", // the name of the controller class (without the Controller suffix)
action = "GetProduct", // the name of your method
id = RouteParameter.Optional
});
Also, your code for your controller looks like it isn't an API controller. Regardless, this is a routing problem. You can add a route configuration in your regular route config if you 100% need to.
Your code above is not a WebApi Controller it is an MVC Controller. Your class needs to inherits from ApiController instead like:
public class ProductController : ApiController{
...
Regarding your method I am not sure why you used Json as returned type since it is part of the MediaFormatter configuration to define the returned format, it should not be defined at method/function level.
It looks like the correct method declaration will be something like:
public Product GetProduct(string id)
{
Product x; //probably you want initialize it like new Product();
return x;
}
Update JsonP
WebApi works based on MediaFormatters as explained earlier. In order to use JsonP you need to use the proper media formatter there are several out there but how about:
http://www.nuget.org/packages/WebApi.JsonP
If you wish to read more about JsonP formatters for WebApi here is a SO Post about this:
.net 4.5 ASP.Net web API JSONP support
Jsonp in WebApi
Your ProductController should derive from ApiController, instead of Controller.
Building on #Dalorzo's answer, if/when you can convert to an APIController, and if you can use WebAPI 2, you can use decorator attributes on your methods that will alter the routes and even the HTTP verbs to use for the method... which is really nice because everything you need to know about that API call is right there at the function signature. It's quite robust and intuitive, and I highly recommend it.
I'm using WebApi and attribute routing.
I have a customer controller that has a method to get all the animals for that customer. This method & route work great. However, I also want a method that just returns a list of all the customers, and I can't get that one to work.
Here is my controller:
[RoutePrefix("api/customer")]
public class CustomerController : ApiController
{
[HttpGet("{customerId}/animals")]
public PagedHorse Get(int customerId)
{
Console.WriteLine("Get");
}
[HttpGet("")]
public PagedCustomer List()
{
Console.WriteLine("List");
}
}
If I change the List route to be "{customerId}/List" and ignore the parameter passed in, it works. What am I doing wrong with these routes? I'd really like to just have api/customer return a list of all customers.
It appears there was some amount of interference between the Hot Towel SPA project type and my project. I didn't start from scratch with Hot Towel, but added it to an existing project, so I believe there were some routing issues.
I deleted the HotTowel specific Configs (e.g. HotTowelRouteConfig) because everything that was done there I was already doing in my ASAX file.
After that everything worked fine.