Loose binding of parameter names in WebApi routing - c#

Currently we use the below routing rules to cater for all our controllers; the advantage being that we don't have to define a route for each action in each controller:
routes.MapHttpRoute("3", "{controller}/{action}/{arg1}/{arg2}/{arg3}");
routes.MapHttpRoute("2", "{controller}/{action}/{arg1}/{arg2}");
routes.MapHttpRoute("1", "{controller}/{action}/{arg1}");
routes.MapHttpRoute("0", "{controller}/{action}");
However due to this the parameter names in the methods must match; like so:
// Example method signature
public ResponseDto GetResponse(int arg1, int arg2)
If the parameter names are changed to something more friendly (eg: a name that would show the intention of each parameter instead of an ambiguous "arg1" name) like so:
// Better example method signature
public ResponseDto GetResponse(int userId, int itemId)
The binding would break unless:
The routes and parameter names are explicitly defined
The arguments are passed in using a query string
Is there are way to set up WebApi routing so it will automatically use the correct action based on the number of parameters; rather than the parameter names?

It seems you're trying to do something similar to what is described in this good blog post on custom parameter binding. As you've found out, there is nothing out-of-the-box to support what you're trying to do.
At a conceptual level, HTTP is built around the concept of resources. A URI should identify a specific resource. Using generic positional "parameters" seems to blur the specificity of a resource. It seems that your use case might be better served by using the query string of the URI. That way, your HTTP API expresses a clear intent of what parameters are expected for a given resource.

Related

Web-Api Route resolution Array Model Binding

I'm trying to understand how Web-Api Resolves Routes. I have two routes which use the same base path
[WriteRoute(DivisionAPIRoutes.PAYROLL_IMPORT_PTO)]
[HttpPost]
public void ImportPTOByIds(GlobalEntityKey<IDivision> parentId, GlobalEntityKey<IDivisionPayCalendar> id,
ImportPTORequestDTO importPTORequest, [FromUri] GlobalEntityKey<IPTORequest>[] ptoRequestIds)
{
GlobalFactory<IEmployeePTOListService>.Instance.ImportPTOByIds(parentId, id, ptoRequestIds, importPTORequest);
}
[WriteRoute(DivisionAPIRoutes.PAYROLL_IMPORT_PTO)]
[HttpPost]
public void ImportPTOByFilter(GlobalEntityKey<IDivision> parentId, GlobalEntityKey<IDivisionPayCalendar> id, ImportPTORequestDTO importPTORequest, string filterOptions,
[FromUri] GlobalEntityKey<IPTORequest>[] excludedPTORequestIds)
{
var filterOptionsDTO = JsonConvert.DeserializeObject<FilterOptionsDTO>(filterOptions);
GlobalFactory<IEmployeePTOListService>.Instance.ImportPTOByFilter(parentId, id, filterOptionsDTO, excludedPTORequestIds, importPTORequest);
}
NOTE: By default, builting CLR types string, int are retrieved from the URI
I'm making a post request which work fine but I'm trying to understand the underlying logic for how the route resolves to the method:
(Decoded for Convenience)
https://localhost/api/paystream/v1/divisions/1af4edea-d442-4fda-b29d-02c42951c0d0/payrolls/cd2ed43d-0f3d-48fb-8d00-15294a8fa06e/_actions/import-pto?filterOptions={"query":"","filterParameters":[{"fieldName":"RequestStartDate","parsebleValue":"2016-01-01","filterType":"GreaterThanOrEqual"},{"fieldName":"RequestStartDate","parsebleValue":"2017-12-31","filterType":"LessThanOrEqual"}]}
PostBody: {
AlwaysCreateNewCheck: false,
PayBatchType: 'Checks',
PayBatchId: '1903771'
}
If I Omit the excludedPTORequestIds from the request. This will still resolve to ImportPTOByFilter but if I include the excludedPTORequestIds and Omit the filterOptions the ImportPTOByIds is Selected.
I'm inclined to think that Lists, and Array which are handled by the model binder have different behaviors than other Default CLR Types which model bind (string, int, Guid, etc).
While string are required and will throw a 404 or resolve to other routes, Array's are not required to be explicitly defined in the request.
Is it safe to assume that, What are the other rules for route resolution from WebApi.
It's resolving to ImportPTOByFilter when you include filterOptions because the other route doesn't have a filter option parameter.
If you remove filter option it resolves to ImportPTOByIds because the parameters match the method signature. Route resolution is going to go by route name and parameters. If you have
RouteA(string myString, int myInt) {...}
and
RouteA(string myString) {...}
and
RouteA(int myInt) {...}
It's going to resolve the route based on if you pass a string, int, or both.
Others more knowledgeable than me can probably add more information but I think you want to make an actual model the encapsulates the entire request that way you have one method and in that method you inspect the model to determine if it should be filtered or not.

C# MVC - Routing Issue / Confusion

I am developing one application in the ASP.NET MVC C# on the .NET 4 framework.
I confused in routing and I do the research and developed the one demo version It works as I want but I want to know is which method is best practice for developing application.
First I register the route like this:
routes.MapRoute(
name: "RoutesTesting",
url: "{controller}/{action}/{a}/{b}/{c}/{d}/{e}",
defaults: new { controller = "Home", action = "Test", e = UrlParameter.Optional }
);
I have one class that have the properties and it's name same as the route's parameter.
class MyClass{
public string a{get;set;}
public string b{get;set;}
public string c{get;set;}
public string d{get;set;}
public string e{get;set;}
}
Now I created the tow methods that works find and get the data from the URL successfully.
Method 1:
public ActionResult Test(MyClass objMyClass){
}
Method 2:
public ActionResult Test(string a,string b,string c,string d,string e=String.Empty){
}
My question is:
Is routing doing that conversation in my action method? Like it convert the parameter values in the `MyClass' object's properties?
Which method is best practice to use?
Is method 1 will throw any error or exception when the conversation is not possible ?
Thanks in advance...
The behavior you are seeing is a part of ASP.NET's Model Binding. It's the magic that lets you send across a JSON object of {"firstName":"Jonathon","lastName":"Chase"} and have to automagically be mapped to a model Person that looks like so:
public class Person {
public string FirstName {get;set;}
public string LastName {get;set;}
}
The fact that you can create a route like that is merely a consequence of this. Model Binding is a complex subject, but I can touch on some aspects of how you're forming your route, especially if the action you're creating is going to have a side-effect, such as writing to a database.
Typically if you're going to have a method that will effect state, you should use an Http verb other than Get, and send the model across in the body of the request, rather than in the query/url string. The Model Binding will take care of the mapping for you either way.
You should prefer to use a strong model rather than multiple primitives as parameters, especially in cases where the information will be sent in the body of a request over the query string.
These points are debatable, however, and shouldn't be considered hard or fast rules for the most part.
As to your last point, if the parameters are incorrect enough that the Route can't identifier the action or controller, you should get a 404. However, if you have a valuetype that isn't nullable as an expected routed property that isn't properly sent across, you should expect a 500 with an InvalidOperationException.
Take a look at How model binding works
Is routing doing that conversation in my action method? Like it
convert the parameter values in the `MyClass' object's properties?
The framework model binder is doing the conversion based on the actions parameter.
Which method is best practice to use?
That is an opinionated question. Depends on which one suits your needs. The framework handles both.
Is method 1 will throw any error or exception when the conversation is
not possible ?
Model binder will pass null to the action parameter for the properties that don't match.

WebApi - UrlHelper.Route to GET with [FromUri] object parameter

I have an API action defined as the following:
[Route(Name="GetMembersTest"), HttpGet, ResponseType(typeof(MemberHeadersDto))]
public IHttpActionResult GetMembers[FromUri]MemberFilterDto filter, [FromUri]PagingOptionsDto paging)
This method works as expected, routing and all, requests are flowing through just fine. However, I'd like to supply a "NextUri" for paging so that the caller can just keep following NextUri until it is null to get all the results. I have to send back a uri to the same action, 1 page ahead, if that makes sense.
So I tried using UrlHelper.Route. This route is named "GetMembers" for the purpose of this example.
NextUri = Url.Route("GetMembers", new { filter, paging });
The problem is that instead of getting something like
/v1/members?filter.q=&filter.otherproperty=&paging.count=10&paging.startRow=11
I get
/v1/members?filter=WebApi.Models.MemberFilterDto&paging=WebApi.Models.PagingOptionsDto
It looks like UrlHelper.Route doesn't support complex types in the [FromUri] parameter of a GET Request. Is there anything I can do to get this functionality? My workaround right now is to take in all the Dto properties as individual parameters then build my Dtos from them on the server. This isn't ideal because if I ever add any more options I'd have to add more parameters to the action, which also makes the route value dictionary more fragile as well because it has to match with the method signature in UrlHelper.Route(routeName,routeValues).
Unfortunately, there is no way to pass in complex object to routing. Instead, you will need to pass in the simple properties individually.
I was not able to find a way to extend Url.Route, but that would be/have been your best option.

How does ASP.NET Web.api handle two methods with names starting with GET?

I am looking at the following tutorial from Microsoft. As per this tutorial,
In the first example, "products" matches the controller named
ProductsController. The request is a GET request, so the framework
looks for a method on ProductsController whose name starts with
"Get...". Furthermore, the URI does not contain the optional {id}
segment, so the framework looks for a method with no parameters. The
ProductsController::GetAllProducts method meets all of these
requirements.
What happens if there are two methods like GetAllProducts() and GetSoldProducts()? Both have no parameters.
Your First Web API Tutorial
There are two possible solutions to this specific problem:
Alter MapHttpRoute calls to require specifying the name of the action. (I'm using Self-hosting syntax):
config.Routes.MapHttpRoute(
"API Route 1",
"api/{controller}/{action}");
config.Routes.MapHttpRoute(
"API Route 2",
"api/{action}",
new { controller = "products" });
So your http client would call:
api/products/GetAllProducts OR api/GetAllProducts
api/products/GetSoldProducts OR api/GetSoldProducts
See:
http://www.asp.net/web-api/overview/web-api-routing-and-actions/routing-in-aspnet-web-api
Place each method in a separate controller (ProductsController, SoldProductsController). So you would call api/products and api/soldproducts to get your results.
Related topic... in the situation where you have a multiple Get actions that have a single primitive argument of the same type, ASP.NET Web API will look at the name of the argument to resolve which overloaded action to call.
For example, if you have two actions:
GetProductByName(string name)
GetProductByCategory(string category)
your http client can call
api/products?name=hammer
api/products?category=toys
and the routing engine will call the correct action.
Assuming you're using the default routes the short answer is : the method defined first (at the top) of your class will be called. the other method is inaccessible.
NOTE : the beta behaved as above for 'matching multiple methods' - the RC & Release version is a bit more OCD. It throws an error if there are multiple potential matches. This change removes the confusion of multiple ambiguous matches. At the same time, it reduces our ability to mix REST and RPC style interfaces in the same controller, relying on the order & overlapping routes.
Stealing liberally from another post I wrote on the topic:
WebAPI Matching Semantic
The matching semantic used by WebAPI is fairly simple.
It matches the name of the action with the verb (verb = get? look for method starting with "get")
if a parameter is passed, the api seeks an action with a parameter
So in your code sample a GET request without a parameter matches the Get*( ) function without an parameters. A Get containing and ID looks for a Get***(int id).
Examples
While the matching semantic is simple, it creates some confusion for MVC developers (well at least this developer). Lets look at some examples :
Odd Names - Your get method can be named anything, so long as it starts with "get". So in the case of a widget controller you can name your functions GetStrawberry() and it will still be matched. Think of the matching as something like : methodname.StartsWith("Get")
Multiple Matching Methods - What happens if you have two Get methods with no parameters? GetStrawberry() and GetOrange(). As best I can tell, the function defined first (top of the file) in your code wins ...strange. This has the side effect of making some methods in your controller unreachable (at least with the default routes)....stranger.
UPDATE
#WinFXGuy - This was a bit long to put in a comment, but ...
Don't jump to conclusions! I tried to answer the question you posed, but that's only half the story. There is plenty you can do to change the default behavior.
First, WebAPI supports much of the oData spec. If you bubble an IQueryable up to your controller, oData paramaters are automatically integrated with the query object. It takes parameters like $filter, $top, and $skip. So you in your case you can write one method and pass something like $filter=sale_date neq null.
Additionally, you can apply the [ResultLimit] attribute to prevent people asking for 15 billion records.
Second you can modify the routes. The default routes aim towards a RESTful api, where you generally have 1 controller per entity. You can change the routes and make it RPC style.
If you look at my linked post I explain how I kept the default route binding, added 'sub folders' and also allowed additional method calls for scenarios where i needed GetAllProducts() and GetSoldProducts().
Adding an answer to reflect that the latest version of Web API supports [Route] attribute natively
[Route("api/products")]
public IEnumerable<Product> GetAllProducts(){}
[Route("api/products/sold")]
public IEnumerable<Product> GetSoldProducts(){}

Routing by posted content type in ASP.NET MVC

I have a fixedURL to which I'd like to post different types of xml message, deserialized using DataContracts. Depending on the type of the deserialized message, I'd like to route to:
overloaded methods, e.g.
void Process(ContractType1 request) {}
void Process(ContractType2 request) {}
So at some point I need to deserialize this message and hopefully allow the default routing rules to match the correct method. Which extensibility point should I use for this? Or even better, can I make this work out of the box?!
If it makes any difference, I'm using MVC 3.
ASP NET MVC does not respect the overload if they are not decorated for different HTTP methods - e.g. one for POST, other for GET.
You need to use [ActionName(Name = "Process2")] to change the route name. And you will have to use different routes to access (if the HTTP methods are the same)
Have a look here.
Apart from the technical workaround, passing different contracts to the same URL is against the REST principles. Data could be in different format (XML, JSON, ...) but it must be the same. The URI defines a unique intention. Now it is possible to have a common dumpster where documents are all dumped to the same URI but then ASP NET MVC default model binder would not be able to help you and you need to create your own model binder.
Contrary to the other answer I say this is possible
Asp.net MVC is a great platform that can be easily extended. And so basically I've written a special action method selector that makes it possible to write overloads that can serve the same HTTP method but defer in parameters. By default you'd get runtime error that action method can't be resolved. But when you use this action method selector you get rid of this error.
Basically if your parameter classes have distinct parameter names, you can actually select methods by that.
Action method selector is called RequiresRouteValuesAttribute and a typical usage scenario would be with default route where id is optional in:
{controller}/{action}/{id}
This means that you either have to write
public ActionResult Index(int? id)
{
if (id.HasValue)
{
// display details view
}
else
{
// display master view
}
}
but by using my action method selector you can easily write two action methods:
public ActionResult Index()
{
// display master view
}
[RequiresRouteValues("id")]
public ActionResult Index(int id)
{
// display details view
}
The same could be applied to your action methods as long as your custom types have distinct property names or methods use different parameter names. So in your case it could be something like:
[RequiresRouteValues("first.Id")] // when you provide prefix with your form
// or
[RequiresRouteValues("Some.ContractType1.Distict.Property.Name")]
public ActionResult Process(ContractType1 first)
{
// do what's appropriate
}
[RequiresRouteValues("second.Id")] // when you provide prefix with your form
// or
[RequiresRouteValues("Some.ContractType2.Distict.Property.Name")]
public ActionResult Process(ContractType2 second)
{
// do what's appropriate
}
Read all the details about this action method selector and get the code as well.

Categories