116

I want to handle POST of the following API-Call:

/v1/location/deviceid/appid

Additional Parameter are coming from the Post-Body.

This all works fine for me. Now I wnat to extend my code by allowing "deviceid" and/or "appid" and/or BodyData to be null:

/v1/location/deviceid
/v1/location/appid
/v1/location/

These 3 URLs should responded by the same route.

My first approach (BodyData required):

[Route("v1/location/{deviceid}/{appid}", Name = "AddNewLocation")]
public location_fromuser Post(string deviceid = null, string appid = null, [FromBody] location_fromuser BodyData)
{
    return repository.AddNewLocation(deviceid, appid, BodyData);
}

This does not work and returns a compile error:

"optional Parameters must be at the end"

Next try:

[Route("v1/location/{deviceid}/{appid}", Name = "AddNewLocation")]
public location_fromuser Post([FromBody] location_fromuser BodyData, string deviceid = null, string appid = null)

Now my function AddNewLocation() get always an BodyData=null - even if the call send the Body.

Finally I set all 3 Parameter optional:

[Route("v1/location/{deviceid}/{appid}", Name = "AddNewLocation")]
public location_fromuser Post(string deviceid = null, string appid = null, [FromBody location_fromuser BodyData = null)

Don´t work:

Optional parameter BodyData is not supported by FormatterParameterBinding.

Why do I want a solution with optional Parameters? My Controller handles just the "adding of a new Location" via a POST.

I want to send on wrong data my own exceptions or error messages. Even if the call has missing values. In this case I want to be able to decide to throw an exception or Setting Defaults by my code.

jasonscript
  • 6,039
  • 3
  • 28
  • 43
Oliver Apel
  • 1,808
  • 3
  • 19
  • 31

4 Answers4

211

For an incoming request like /v1/location/1234, as you can imagine it would be difficult for Web API to automatically figure out if the value of the segment corresponding to '1234' is related to appid and not to deviceid.

I think you should change your route template to be like [Route("v1/location/{deviceOrAppid?}", Name = "AddNewLocation")] and then parse the deiveOrAppid to figure out the type of id.

Also you need to make the segments in the route template itself optional otherwise the segments are considered as required. Note the ? character in this case. For example: [Route("v1/location/{deviceOrAppid?}", Name = "AddNewLocation")]

Thomas Eyde
  • 3,820
  • 2
  • 25
  • 32
Kiran
  • 56,921
  • 15
  • 176
  • 161
  • 75
    `?` inside the route template is what I was looking for. +1 – Kal_Torak Oct 19 '15 at 19:19
  • 5
    I wouldn't say that "deviceOrAppId" is the best design choice. I think the API should always know by definition what it will be receiving if at all possible. – Niels Brinch Aug 06 '16 at 10:13
  • 19
    Just for information - When we mark a parameter as optional in the action uri using `?` character then we must provide default values to the parameters in the method signature e.g. MyMethod(string name = "someDefaultValue", int? Id = null). – RBT Feb 02 '17 at 10:58
  • @RBT you da real MVP, I got stumped there for a minute. Thank you! – s.m. Jun 23 '17 at 10:00
  • 1
    Cool. Glad that it helped you @s.m. I've converted my comment into an answer for better visibility as it seems helpful. It will be an add-on to Kiran's post. – RBT Jun 28 '17 at 00:44
53

Another info: If you want use a Route Constraint, imagine that you want force that parameter has int datatype, then you need use this syntax:

[Route("v1/location/**{deviceOrAppid:int?}**", Name = "AddNewLocation")]

The ? character is put always before the last } character

For more information see: Optional URI Parameters and Default Values

abatishchev
  • 98,240
  • 88
  • 296
  • 433
Tiago Ferreira
  • 540
  • 4
  • 6
27

An additional fact to complement @Kiran Chala's answer -

When we mark any parameter (appid) as optional in the action URI using ? character(for nullable value types) then we must provide default value to the parameter in the method signature as shown below:

[Route("v1/location/{deviceid}/{appid}", Name = "AddNewLocation")]
public location_fromuser Post(string deviceid, int? appid = null)
RBT
  • 24,161
  • 21
  • 159
  • 240
  • I was about to comment the same. – Jnr Sep 27 '17 at 12:54
  • Exactly my problem. I remembered the question mark in the route but forgot the default value in the method parameter. – Dan Apr 28 '22 at 11:40
  • why are the parameter names not matching the route token names? – Choco Aug 12 '22 at 09:37
  • 1
    Very keen observation @Choco. I've fixed the parameter names and the related portions of the answer. Thank you for your kind feedback. – RBT Aug 13 '22 at 06:53
7

Ok, I fallen here with my internet research and I continue my way, because the accepted solution not working with dotnet core 3.1. So here is my solution, following this doc

[HttpPost]
[Route("{name}")]
[Route("{name}/parent/{parentId}")]
public async Task<IActionResult> PostSomething(string name, Guid? parentId = null)
{
    return Ok(await Task.FromResult(new List<string>()));
}

By this way many routes go to this single API function

Shadam
  • 1,041
  • 13
  • 25
  • I thought you were on to something but for me this is generating two endpoints in swagger and both are still required. :/ – johnw182 Jul 15 '21 at 22:20
  • Ok. maybe its swagger. I now have just [HttpPost("~/Path/Action/{paramName?}")] and a nullable int in my method, WITHOUT a default value and from postman it works fine. Swagger has just been marking all of my attempts as required this whole time. Now how to fix swagger? – johnw182 Jul 15 '21 at 22:24
  • @johnw182 if you want the parameter as not required you need to set a default value, bacause if you don't swagger want you to give the value but with a default it's ok. – Shadam Jul 17 '21 at 07:48
  • And for the two generated endpoints it's normal, you set two routes so two endpoints are available :) – Shadam Jul 17 '21 at 07:48
  • I did the default. Swagger still marked it as required and would not let it submit without a value – johnw182 Jul 17 '21 at 15:14
  • seems like swagger is brok – sensei Jan 20 '22 at 12:11
  • No it's totally logic, first route no need of `parentId`, second route swagger set it as required. The default value setting is for the case where you came from the first route. Be careful to not confuse between c# and swagger, swagger only describe your API's route – Shadam Jan 21 '22 at 09:35