Web API 2 Attribute Routing Controller Selection - c#

I use Web API 2 Attribute Routing in my project to provide JSON interface over my data. I am facing weird behaviour of controller selection, not decided yet whether it's a bug or a feature :)
Let me describe my approach.
I would like to simulate OData syntax with help of attribute routing (direct OData usage has been refused due to design principles). For example, to get entity with id=5 I use HTTP GET request to URI http://mydomain.com/api/Entity(5) . I expect to use the same URI with HTTP PUT verb to update the entity. This is where the journey begins...
I would like to have separate controller for getting entities (FirstController in the example provided below) and another one for modifying entities (SecondController). Both controllers handles the same URI (e.g. http://mydomain.com/api/Entity(5)) the only difference is HTTP verb used with the URI - GET should be handled by FirstController, PUT should be handled by SecondController. But the URI is handled by none of them; instead HTTP 404 error is returned.
When I "merge" GET and PUT actions to only one controller (commented out in FirstController), both verbs are handled correctly.
I am using IIS Express and all conventional routes are disabled, only attribute routing is in charge.
It looks like the controller selection process does not work with HTTP verb. In another words, HttpGet and HttpPut attributes just limit action usage but they do not serve as criteria during controller selection. I am not so familiar with MVC / Web API fundamentals, so let me ask you my big question:
Is the behaviour, described herein before, a feature intentionally implemented by MVC / Web API 2 or a bug to be fixed?
If it is considered as a feature, it prevents me to follow design principles. I can live with "merged" controllers but still considering it as a bad practice...
Or am I missing something in my train of thought?
My environment setup:
Windows 7 (virtual machine using Oracle VirtualBox)
Visual Studio 2013
.NET 4.5.1
Web API 2
The following is implementation of FirstController class:
public class FirstController : ApiController
{
[HttpGet]
[Route("api/Entity({id:int})")]
public Output GetEntity(int id)
{
Output output = new Output() { Id = id, Name = "foo" };
return output;
}
//[HttpPut]
//[Route("api/Entity({id:int})")]
//public Output UpdateEntity(int id, UpdateEntity command)
//{
// Output output = new Output() { Id = id, Name = command.Name };
// return output;
//}
}
The following is implementation of SecondController class:
public class SecondController : ApiController
{
[HttpPut]
[Route("api/Entity({id:int})")]
public Output UpdateEntity(int id, UpdateEntity command)
{
Output output = new Output() { Id = id, Name = command.Name };
return output;
}
}
The following is implementation of a console application to test the described behaviour:
class Program
{
static void Main(string[] args)
{
// HTTP client initialization
HttpClient httpClient = new HttpClient();
httpClient.BaseAddress = new Uri("http://localhost:1567");
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// HTTP GET - FirstController.GetEntity
HttpResponseMessage getEntityResponse = httpClient.GetAsync("/api/Entity(5)").Result;
Output getOutput = getEntityResponse.Content.ReadAsAsync<Output>().Result;
// HTTP PUT - SecondController.UpdateEntity
UpdateEntity updateCommand = new UpdateEntity() { Name = "newEntityname" };
HttpResponseMessage updateEntityResponse = httpClient.PutAsJsonAsync("/api/Entity(10)", updateCommand).Result;
Output updateOutput = updateEntityResponse.Content.ReadAsAsync<Output>().Result;
}
}
For completion, the following are used DTOs:
public class UpdateEntity
{
public string Name { get; set; }
}
public class Output
{
public int Id { get; set; }
public string Name { get; set; }
}
Thanks in advance for your responses,
Jan Kacina

This design was intentional as we thought it to be an error case where a user would be having same route template on different controllers which can cause ambiguity in the selection process.
Also if we keep aside attribute routing, how would this work with regular routing? Let's imagine we have 2 regular routes where first one is targeted for FirstController and the second to SecondController. Now if a request url is like api/Entity(5), then Web API would always match the 1st route in the route table which would always hit the FirstController and would never reach SecondController. Remember that once Web API matches a route it tries to go till the action selection process and if the action selection process doesn't result in an action being selected, then an error response is sent to the client. You probably are assuming that if an action is not selected in one controller then Web API would route it to the next one in the route configuration. This is incorrect.
Route probing occurs only once and if it results in a match, then the next steps take place...that is controller and action selection. Hope this helps.

Related

.NET CORE Web API Routing

I am new to .NET Core Web API and i'm trying to create Web API with 3 POST methods.
AddUser
UpdateUser
DeleteUser
I was able to create a .NET core web api project with AddUser POST method and its working fine but they way I want it be uri is
https://localhost:1234/api/Project/AddUser
https://localhost:1234/api/Project/UpdateUser
https://localhost:1234/api/Project/DeleteUser
When I run the application in default swagger uri shows POST /api/Project i.e. https://localhost:1234/api/Project
I am using .NET core web api 5.0
Here code from my controller
namespace ProjectAPI.Controllers
{
[Route("api/[controller]")]
[ApiController]
[ApiKeyAuth]
public class ProjectController : ControllerBase
{
[HttpPost]
public async Task<ActionResult<Response>> AddUser([FromBody] Request request)
{
var _message = await DoSomething(request);
Response response = new Response
{
Message = _message
};
return response;
}
private async Task<string> DoSomething(Request request)
{
string msg = string.Format("Add user {0} to {2} is successful", request.User, request.FromRole, request.ToRole);
return msg;
}
}
}
#joshykautz is right, you can add routing to each action
Another way is just to change controller routing and not touching actions:
[Route("api/[controller]/[action]")]
public class ProjectController : ControllerBase
....
but after this, if you need, you can still can assign a very special route for some action, for example
[Route("~/api/Project/AddNewUser")]
public async Task<ActionResult<Response>> AddUser( Request request)
Don't miss "~/". It will work for url
https://localhost:1234/api/Project/AddNewUser
Adding the [action] token to your controller route will yield your desired route format:
[Route("api/[controller]/[action]")]
However, I would discourage using verbs in your route naming. The HTTP method already sufficiently describes the action taken when calling a given endpoint.
POST /api/user creates a new user.
GET /api/user gets users.
PUT /api/user/{id} updates an existing user.
DELETE /api/user/{id} deletes a user.
In a RESTful approach, the route describes the resource that you're interacting with on the server, and the HTTP method used describes the action. Mixing actions/verbs into your routes goes against this mindset.
What I would do in your situation is create a new UserController, which will contain the endpoints for your user resource. Having them in a ProjectController, which to me sounds like something that should handle projects, mixes responsibilities and makes your code difficult to understand.
I kindly recommend not to use ../adduser ../updateuser ../deleteuser in your routing. It also causes security weakness.
You can build your API as /user and add;
yourrouter.route('/user/:id?')
.get(user.get)
.post(user.post)
.put(user.put)
.delete(user.delete);
for the same /user route.
It means, the client calls the same ./user with a specific request (GET, POST, PUT etc.) and with ID and also other parameters if required.
You can test your API and routing via POSTMAN, by selecting the method when you call the API (e.g. https://yourdomain/api/user/{parameters})
Give a routing attribute to each action.
[HttpPost]
[Route("api/[controller]/AddUser")]
public async Task<ActionResult<Response>> AddUser([FromBody] Request request)
{
var _message = await DoSomething(request);
Response response = new Response
{
Message = _message
};
return response;
}
And remember to remove the routing attribute that you've defined for the class.
You could also use [Route("api/[controller]/[action]")] since your method is already named AddUser.
[HttpPost]
[Route("api/[controller]/[action]")]
public async Task<ActionResult<Response>> AddUser([FromBody] Request request)
{
var _message = await DoSomething(request);
Response response = new Response
{
Message = _message
};
return response;
}
You can read more here on the Microsoft Docs.

The request matched multiple endpoints on .NET Core

I use OpenAPI (Swagger) in a .NET Core project and when using multiple methods that have similar get requests, I encounter "Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints." error during runtime. I look at several pages on the web and SO and tried to apply the workarounds like The request matched multiple endpoints but why?, but it does not fix the problem. Here are the API methods and route definitions that I use.
[Route("get", Name="get")]
public IEnumerable<DemoDto> Get()
{
//
}
[Route("get/{id}", Name="getById")]
public DemoDto GetById(int id)
{
//
}
[Route("get/{query}", Name="getWithPagination")]
public IEnumerable<DemoDto> GetWithPagination(DemoQuery query)
{
//
}
I use Name property in order to fix the problem but not solved. Any idea to make changes on the routes to differentiate Get() and GetWithPagination()?
You have two endpoints with equals routes:
get/{id} and get/{query}.
If you write in browser line: get/123, the system can't understand what route to use, because they have the same pattern.
You need to distinguish them and I suggest you use restful style for routes, like:
item/{id},
items?{your query}
[Route("get/{query}", Name="getWithPagination")]
This doesn't make sense. DemoQuery is an object, it can't be represented by a single part of a url. You can tell the ModelBinder to build your object from multiple query parameters, though.
The routing engine is getting this route confused with the [Route("get/{id}", Name="getById")] route. They both appear to match get/blah.
In addition to fixing your DemoQuery route, try adding a route constraint on the id route -
[Route("get/{id:int}", Name="getById")]
to better help the engine.
To get DemoQuery to work, assume it looks something like:
public class DemoQuery
{
public string Name { get; set; }
public int Value { get; set; }
}
Then change your action to
[Route("getPaged/{query}", Name="getWithPagination")]
public IEnumerable<DemoDto> GetWithPagination([FromQuery] DemoQuery query)
and call then endpoint like /getPaged?name=test&value=123. And the ModelBinder should build your object for you.
ASP.NET Web API 2 supports a new type of routing. Offical Doc
Route constraints let you restrict your parameters type and matched with these types (int, string, even date etc). The general syntax is "{parameter:constraint}"
[Route("users/{id:int}")]
public User GetUserById(int id) { ... }
[Route("users/{name}")]
public User GetUserByName(string name) { ... }
I tested at API;
//match : api/users/1
[HttpGet("{id:int}")]
public IActionResult GetUserById(int id){ ... }
//match : api/users/gokhan
[HttpGet("{name}")]
public IActionResult GetUserByName(string name){ ... }

POST request won't execute from localhost

I got the following address:
api/users/AddNew/<object here>
And so, I did something like this to test it out:
api/users/AddNew/{"Id":0,"NameFirst":"NewUser","NameLast":"NewUserLast","DateOfBirth":"2018/07/27"}
But all I get is a "This localhost page can't be found". Debugging in Visual Studio I set a breakpoint right when the code is supposed to trigger, but it never hits the debug point. Code below:
[HttpPost(Name = "AddNew")]
[Route("AddNew/{jsonUser}")]
public ActionResult<User> Post([FromBody] User newUser)
{
if (newUser != null) // Debug Point here never triggers
{
return Facade.AddNewUser(newUser);
}
else
{
return new User() { Id = 0, NameFirst = ErrorCodeUtility.GetEnumName(ErrorCodes.API_INVALID_POST_OBJECT) };
}
}
The User Model:
public class User
{
public uint Id;
public String NameFirst;
public String NameLast;
public DateTime DateOfBirth;
}
So, I'm assuming I'm doing something wrong either with my request or my C# code, but I can't quite find out which one of them it is. Sorry I'm a bit new at this :)
There are at least three problems here:
You cannot call a POST method by navigating to an URL, since that executes a GET. You need a tool such as Postman for this testing.
[FromBody] means that the data will come as part of the body of the request, so the data would be ignored even if the request could be processed (which cannot be, as explained in point 1). Note, however, that since you are using ASP.NET Core 2.1, you don't actually need to use [FromBody] as that is the default, as explained here.
You say you want to create a REST API. /AddNew doesn't follow REST.
Your method should be:
[HttpPost]
public ActionResult<User> Post([FromBody] User newUser)
And your URL would then be:
localhost:somePort/api/users

Accessing the query string in MVC 6 Web Api?

I am trying to add a Get() function in a MVC 6 (Asp .Net 5) Web Api to pass a configuration option as a query string. Here are the two functions that I already have:
[HttpGet]
public IEnumerable<Project> GetAll()
{
//This is called by http://localhost:53700/api/Project
}
[HttpGet("{id}")]
public Project Get(int id)
{
//This is called by http://localhost:53700/api/Project/4
}
[HttpGet()]
public dynamic Get([FromQuery] string withUser)
{
//This doesn't work with http://localhost:53700/api/Project?withUser=true
//Call routes to first function 'public IEnumerable<Project> GetAll()
}
I've tried several different ways to configure the routing, but MVC 6 is light on documentation. What I really need is a way to pass some configuration options to the list of Projects for sorting, custom filtering etc.
You can't have two [HttpGet]s with the same template in a single controller. I'm using asp.net5-beta7 and in my case it even throws the following exception:
Microsoft.AspNet.Mvc.AmbiguousActionException
Multiple actions matched. The following actions matched route data and had all constraints satisfied:
The reason for this is that [From*] attributes are meant for binding, not routing.
The following code should work for you:
[HttpGet]
public dynamic Get([FromQuery] string withUser)
{
if (string.IsNullOrEmpty(withUser))
{
return new string[] { "project1", "project2" };
}
else
{
return "hello " + withUser;
}
}
Also consider using Microsoft.AspNet.Routing.IRouteBuilder.MapRoute() instead of attribute routing. It may give you more freedom defining the routes.
Accessing the query string is very doable by using either the RESTful routing conventions (enforced by ASP.NET 5 / MVC 6 by default) or by defining custom routes, as explained in this answer.
Here's quick example using custom, attribute-based routes:
[HttpGet("GetLatestItems/{num}")]
public IEnumerable<string> GetLatestItems(int num)
{
return new string[] { "test", "test2" };
}
For more info about custom routing, read the following article on my blog.

Mapping to an ASMX service using routing in ASP.NET MVC

I wonder if there's any way to map a URL to ASMX service much like it is done with pages (using routes.MapPageRoute() method).
When I tried to do it simply by pointing the MapPageRoute to my service I get the error
Type 'MvcApplication1.Services.EchoService' does not inherit from 'System.Web.UI.Page'.
Matthias.
I stumbled upon this question trying to find the answer myself, and since I did figure out a way to do it, I figured I'd answer it.
The reason I needed this is because I'm converting an old ASP.NET website to ASP.NET MVC, and for compatibility purposes I need a web service available at a specific URL. However, the path of that URL is now handled by a Controller in the new site, so I cannot have a physical directory with the same name (since that will prevent the controller from being invoked for other URLs with that path other than the web service).
The PageRouteHandler, which is used by RouteCollection.MapPageRoute, indeed requires that the handler for the target path derives from System.Web.Page, which isn't the case for web services. So instead, it is necessary to create a custom page handler:
using System;
using System.Web;
using System.Web.Routing;
using System.Web.Services.Protocols;
public class ServiceRouteHandler : IRouteHandler
{
private readonly string _virtualPath;
private readonly WebServiceHandlerFactory _handlerFactory = new WebServiceHandlerFactory();
public ServiceRouteHandler(string virtualPath)
{
if( virtualPath == null )
throw new ArgumentNullException("virtualPath");
if( !virtualPath.StartsWith("~/") )
throw new ArgumentException("Virtual path must start with ~/", "virtualPath");
_virtualPath = virtualPath;
}
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
// Note: can't pass requestContext.HttpContext as the first parameter because that's
// type HttpContextBase, while GetHandler wants HttpContext.
return _handlerFactory.GetHandler(HttpContext.Current, requestContext.HttpContext.Request.HttpMethod, _virtualPath, requestContext.HttpContext.Server.MapPath(_virtualPath));
}
}
This route handler will create an appropriate handler for the web service based on the request and mapped virtual path of the service.
You can add a route for a web service now as follows:
routes.Add("RouteName", new Route("path/to/your/service", new RouteValueDictionary() { { "controller", null }, { "action", null } }, new ServiceRouteHandler("~/actualservice.asmx")));
Note: you must specify the controller and action values in the route value dictionary (even though they're set to null), otherwise the Html.ActionLink helper will always use this route for every single link (unless a match was found in the list before this route). Since you probably want to add this route before the default MVC route, it's important that it doesn't get matched that way.
Of course, you can create your own extension method to alleviate this task:
public static Route MapServiceRoute(this RouteCollection routes, string routeName, string url, string virtualPath)
{
if( routes == null )
throw new ArgumentNullException("routes");
Route route = new Route(url, new RouteValueDictionary() { { "controller", null }, { "action", null } }, new ServiceRouteHandler(virtualPath));
routes.Add(routeName, route);
return route;
}
After which you can simply do:
routes.MapServiceRoute("RouteName", "path/to/your/service", "~/actualservice.asmx");
I hope this helps someone, despite the age of this question. :)
Now that we waited two years with an anwer, how about using Web API instead? :)
EDIT: Kidding aside if that doesn't work for you and you still need an answer, leave a comment and I will see if I can't come up with a better one.
I attempted the original post's solution (also posted here), but I encountered a serious problem. I couldn't target the web method within the web service. When attempting to do so I got an exception stating the file didn't exist.
If you truly want to map an MVC route to a .ASMX web service the solution is explained here.
I believe that solution to be a hack by abusing the built-in types, because it uses reflection to bypass the intentional restrictive access members on the built-in .NET types.
Here is the method I'm taking which I believe to be much more straightforward.
First off, you should design your web services in the .ASMX file so that all the web service does is act as a published interface. At that point we don't need to target the .ASMX web service's methods directly. The important code has been made re-useable in core classes that are agnostic to the application's entry-point. We need this anyway so we can run automated tests!
Replace the MVC's web service method with a new route that has a custom route handler and http handler.
Old Route:
routes.MapRoute(
"Lead",
"lead/{action}.mvc",
new { controller = "Lead" });
New Route:
var dict = new RouteValueDictionary
{
{ "controller", null },
{ "action", null }
};
var handler = new LeadRouteHandler();
var route = new Route("lead/MVC_General.mvc", dict, handler);
routes.Add("Lead", route);
Note that the new route has the action hard-coded "MVC_General". This is because I wish to improve upon the giant controller class and create a handler for each action that way I can have small class with a single responsibility for each web method.
Implement the route's handlers.
IRouteHandler:
public class LeadRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new LeadHttpHandler();
}
}
IHttpHandler:
public class LeadHttpHandler : IHttpHandler
{
public bool IsReusable
{
get { return false; }
}
public void ProcessRequest(HttpContext context)
{
// Just enough code to preserve the route's json interface for tests
var typedResult = new PsaLeadSubmissionResult();
typedResult.Registered = false;
typedResult.Message = new List<string>
{
"Not Implemented"
};
var jsonResult = JsonConvert.SerializeObject(typedResult);
context.Response.Write(jsonResult);
}
}
From within IHttpHandler's ProcessRequest method we gain full control over that route's action. With a well designed web service architecture all we need to do is call the class's that support the .ASMX web method you are trying to map the route to.
The result is a very clean Global.asax file. We could have done all of this without the URL routing just by manually inspecting the URL, but that's too important of a file to bloat.

Categories

Resources