0

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.

johnny 5
  • 19,893
  • 50
  • 121
  • 195

1 Answers1

1

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.

chris-crush-code
  • 1,114
  • 2
  • 8
  • 17
  • The parameters do not match one array is called `excludedPTORequestIds`, the other is called `ptoRequestIds`, in both routes they are missing 1 parameter. In both Senarios they are missing 1 parameter. But depending on the missing parameter is how the route is resolved – johnny 5 Dec 20 '17 at 20:07
  • the datatypes do though. This is why most best practices say not to have two routes with the same name EDIT: by removing the filter string paramater, you're making the signature match the first route. – chris-crush-code Dec 20 '17 at 20:08
  • I agree, the names should not be duplicate, I'm just trying to understand the Model Binder Implementation, because this code was causing a bug based on how the client serializes the get url. I'm just seeing if there are any other Gotcha's to look out for – johnny 5 Dec 20 '17 at 20:10
  • 1
    I gotcha, if you're using the standard webapiconfig class at startup, I think the modelbinder first verifies controller and action, and then it'll look at the quantity and types of paramters. In your case removing filterOptions from the query string is going to make the unfiltered action match the url more closely than the filtered one, regardless of whatever names you give the parameters – chris-crush-code Dec 20 '17 at 20:13
  • yeah I was thinking the same but in the senario I layed out. if you count the matched properties and then missed properties in each senario, they are equivalent, but they resolve to different routes. I think this has to be because of the way client's normally serialize Get data. If there a list with no values from a form in the get url you are not populated with the query string values. Meaning that for asp.Net to resolve a route it cannot be dependent on Array properties – johnny 5 Dec 20 '17 at 20:15
  • 1
    I did a little digging, the second answer on the question below says that API Model Binding isn't the same as MVC modelbinding and you have to use a custom binder with data annotation attributes to make it work with the FromURI https://stackoverflow.com/questions/26600275/changing-the-parameter-name-web-api-model-binding I'm not sure if any of this is helpful for you at all unfortunately, since I always use data annotations I guess I've never been in exactly the same situation but my hunch is that it doesn't have an exact match so it's going by data types. – chris-crush-code Dec 20 '17 at 20:27