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.
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 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
This is an interesting problem I'm having. My controller is like
public HomeController()
{
TaskRepo = new ArchiveTasksRepository();
}
// GET: /<controller>/
public IActionResult Index()
{
return View();
}
[Route("tasks/all/count")]
[HttpGet]
[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
public ActionResult NumberArchiveTasks()
{
return Json(TaskRepo.GetTaskCount());
}
and I have a bunch of other endpoints like the NumberArchiveTasks one. This is for an ASP.NET Core app that I wrote following a tutorial. All the endpoints work when I run the app locally. But when I deploy to IIS on a remote server, the Index one works while the others don't; they 404. I'm not sure if it's the HttpGet attribute, the fact that I'm using ActionResult instead of IActionResult, or what exactly it could be. Maybe something else. Any idea how I could figure this out, or should I just guess-and-check until it works?
Your default route will match /home/numberarchivetasks, I believe. Your route attribute doesn't start with a slash so it will be relative, which means it likely is something like /home/tasks/all/count - have you tried that?
I recommend putting your action in a TasksController (with attribute [Route("[controller]")]) and then the action can have a relative route off of that.
I have created a .Net Core Web API program. I want to add a single view to it. Under the same project I add a "Views" folder. In the HomeController, where I am routing all my API requests, I created the following:
[HttpGet("view")]
public IActionResult Index()
{
return View();
}
In my Views folder I created a folder "Home" and added "Index.cshtml" to it.
When I launch the API, and navigate to "../view" I get to the return View(); line, but then it returns a 500 Internal Server Error.
This is what I don't like about the "automagical" approach of MVC convention. I have no idea where to link a view to a controller if the convention didn't work.
Update, this probably should have been my first course of action.
I added a new class to Controllers folder, and used the MVC Controller template in VS2015. I then added a view to match, and it still doesn't work automagically.
So for clarity, my project is: ASP.NET Core Web Application(.NET Core) with a Web API template. I have a "Jobs" controller class that was added at the start as 'Values' and I renamed. Then I added an MVC Controller Class named "HomeController" with the only method being "Index". I added a folder named "Views" and a subfolder named "Home", and added an MVC View Page named "Index.cshtml".
I tried to use "return View();" in the Index method, didn't work. I then tried to add [Route("Home/Index")] above the Index method. Either way, the URL will get me to my break point at "return View();" but it will never return the view.
Note : It's a little strange that you want to return a view in a Web API project, a Web API project is supposed to return some data structure, like json using return new JsonResult(your_json_here) for example.
Note 2 : you need the Microsoft.AspNetCore.Mvc framework (which is installed with the Web API template)
Anyway, you have different ways to configure routing in a asp.net core application :
Creating and extending default routes
Example of routing configuration in the Configure method :
app.UseMvc(routes =>
{
// You can add all the routes you need here
// And the default route :
routes.MapRoute(
name: "default_route",
template: "{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" }
);
});
Using attributes
If you configure routing with attributes, don't forget the controller's one :
Example for the route /index :
[Route("")]
public class HomeController : Controller
{
[HttpGet]
[Route("[action]")]
public IActionResult Index()
{
return View();
}
}
Example for the route /home/index :
[Route("[controller]")]
public class HomeController : Controller
{
[HttpGet]
[Route("[action]")]
public IActionResult Index()
{
return View();
}
}
Example for the route /iputwhatiwant/actionnameiwant :
[Route("iputwhatiwant")]
public class HomeController : Controller
{
[HttpGet]
[Route("actionnameiwant")]
public IActionResult Index()
{
return View();
}
}
My screen of a .NET Core Web API project returning a view :
For more information, the official documentation is well-documented : https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing
How are you running this webapp, from the Windows commandline?... can you give us the detailed HTTP500 error. It will probably reveal something different than routing errors because that usually gives 404.
[Route("[controller]")]
public class HomeController : Controller
Note the automagical "[controller]" in the Route definition, I think its necessary now
It took me a frustratingly long while to learn the routing convention as it was being developed, but it seems to have normalized out for a few versions. Check out this tutorial documentation on the subject in MVC: Attribute Routing in ASP.NET MVC 5, which is MVC not WebCoreAPI where it is likely based from. If you have a better documentation specific to Web Core API, use that.
This ASP.NET Web Core Build a web API tutorial documentation has some good points about what you seem to be trying to do. Specifically, the section title "Getting to-do items" has this code:
[HttpGet("{id}", Name = "GetTodo")]
public IActionResult GetById(long id)
{
var item = _todoRepository.Find(id);
if (item == null)
{
return NotFound();
}
return new ObjectResult(item);
}
Looking at that with benefit of some measure of MVC routing experience, it looks particularly different from your approach in that the HTTP verb annotation member property value used is a query parameter.
Seeing I am guessing using known inexpertise, still, I think you need to get the attribute routing fixed, and maybe return an ObjectResult instead of a view, as NightOwl888 suggests. The server error might also have much more useful information along with the exception message.
EDIT: Sorry, I may have misunderstood your question. If you are trying to build an app that serves dynamic web pages instead of a WebAPI that serves data object results, this Build an MVC Web App tutorial, similar to the "Build a web API" tutorial I mentioned before might have your app structure problem answer. If you are trying to do both, you should probably start with the MVC Web App structure or use two separate projects.
The (only) way I have got this working is to declare the path as an attribute on the action - in the same way you have done but with the format as below (Controller/Action):
[HttpGet("Home/Index")]
public IActionResult Index()
{
return View();
}
I was missing:
"preserveCompilationContext": true
in the build options of my project.json
I am using WebAPI 2 with EF and scaffolding webapi controllers from visual studio.
Each controller is created with 4 default verbs (GET,PUT,DELETE,POST) and 5 actions. while there are two versions of GET action.
IQueryable<entity> GetEntities ()
Task<IHttpActionResult> GetEntity(GUID key) // default is int id but I changed to guid.
I am using attribute routing and route prefix for the controller. just some fancy keywords for better management of url. [RoutePrefix("api/v3/Company")]
Problem :
Ideally when a wrong parameter is sent in url, it should return error, but it is not raising error, instead it fall back to the action without parameter.while if I send a wrong GUID, it shows error.
Like if I call :
http://localhost:8080/api/v3/Company/1f7dc74f-af14-428d-aa31-147628e965b2
it shows the right result.
when I call :
http://localhost:8080/api/v3/Company/1f7dc74f-af14-428d-aa31-147628e96500 (wrong key)
it set back to GetEntity() function and shows all records
when I call:
http://localhost:8080/api/v3/Company/1 (not a GUID length parameter)
it do the same and shows all records.
I am using attribute [Route("{id:guid}")]
Really appreciate if I can get some guidance on this!
It is most likely that the route is defaulting back to the convention-based mapping.
You need to explicitly make apply the route attribute on actions to let the routing know that it is the default route got GET
[RoutePrefix("api/v3/Company")]
public class CompanyController : ApiController {
//GET api/v3/Company
[HttpGet]
[Route("")] //Default Get
public IQueryable GetEntities() { ... }
//GET api/v3/Company/1f7dc74f-af14-428d-aa31-147628e965b2
[HttpGet]
[Route("{id:guid}")] // ALSO NOTE THAT THE PARAMETER NAMES HAVE TO MATCH
public Task<IHttpActionResult> GetEntity(Guid id) { ... }
//...other code removed for brevity
}
Make sure that attribute routing is enabled in the web api config
config.MapHttpAttributeRoutes();