4

If I call UrlHelper.Link within an API call which has a parameter matching an optional parameter of the API endpoint I'm attempting to obtain an URL for, UrlHelper.Link returns a URL with values from the current request no matter how I try to exclude the optional parameter from the link.

e.g.

[HttpGet]
[Route("api/Test/Stuff/{id}")]
public async Task<IHttpActionResult> GetStuff(int id) // id = 78
{
    string link = Url.Link("Mary", new
    {
        first = "Hello",
        second = "World"
    });

    string link2 = Url.Link("Mary", new
    {
        first = "Hello",
        second = "World",
        id = (int?)null
    });

    string link3 = Url.Link("Mary", new
    {
        first = "Hello",
        second = "World",
        id = ""
    });

    return Ok(new
    {
        link,
        link2,
        link3
    });
}

[HttpGet]
[Route("api/Test/Another/{first}", Name = "Mary")]
public async Task<IHttpActionResult> AnotherMethod(
[FromUri]string first,
[FromUri]string second = null,
[FromUri]int? id = null)
{
    // Stuff
    return Ok();
}

GET http://localhost:53172/api/Test/Stuff/8

returns

{
  "link": "http://localhost:53172/api/Test/Another/Hello?second=World",
  "link2": "http://localhost:53172/api/Test/Another/Hello?second=World&id=8",
  "link3": "http://localhost:53172/api/Test/Another/Hello?second=World&id=8"
}

How do you get Url.Link to actually use the values you pass it rather than pull it from the current api request when they are not presented or assigned to null or empty string?

I believe the issue is very similar to ....

UrlHelper.Action includes undesired additional parameters

But this is Web API not MVC, not an action and the answers provided do not seem to yield an obvious solution to this issue.

EDIT: I've updated the code as the original code didn't actually replicate the issue. I've also included a request URL and the response returned, which I've tested. Whilst this code demonstrates the issue, the code I'm trying to find a fix for is not passing an anonymous type to UrlHelper, instead its a class which generates a timestamp and hash and has 4 optional parameters. If there's another solution which doesn't require omitting the optional parameters in the structure passed to UrlHelper I'd like to have it, as it'll save me from making a lot of changes to the code.

Mick
  • 6,527
  • 4
  • 52
  • 67
  • Also show the original URL and the link generated URL. – Nkosi Jun 05 '17 at 03:07
  • I'd suggest the answer is more of a work around. It's something I think will catch a lot of people out who mistakenly believe assigning an empty or null value to a property on either any structure (including anonymous or class type) passed into UrlHelper would be the same as omitting the value. Whilst the behavior might actually be by design, I think it's highly ill conceived and not what you would expect – Mick Jun 05 '17 at 08:01

1 Answers1

3

Do not include the id when passing the route values. In fact it should not allow you to compile as

Cannot assign <null> to anonymous type property

public async Task<IHttpActionResult> GetStuff(int id) // id = 78 {
    string myUrl = Url.Link(
        "Mary", 
        new 
        { 
            first = "Hello World"
        });

    //...other code
}

Tested with web api 2 and id was not included when linking to other action.

[RoutePrefix("api/urlhelper")]
public class UrlHeplerController : ApiController {

    [HttpGet]
    [Route("")]
    public IHttpActionResult GetStuff(int id) {
        string myUrl = Url.Link(
            "Mary",
            new {
                first = "Hello World",
           });
        return Ok(myUrl);
    }

    [HttpGet]
    [Route(Name = "Mary")]
    public IHttpActionResult AnotherMethod(
    [FromUri]string first,
    [FromUri]string second = null,
    [FromUri]int? id = null) {
        // Stuff
        return Ok();
    }
}

Calling

client.GetAsync("/api/urlhelper?id=78")

routed to the GetStuff action and the link generated

"http://localhost/api/urlhelper?first=Hello%20World"

Even when tested with

string myUrl = Url.Link(
    "Mary",
    new {
        first = "Hello World",
        id = ""
    });

id was not included in the generated link.

Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • I've updated the question. Technically this was the answer to my question. However, unlike the sample I've provided, my actual code isn't passing an anonymous type to UrlHelper, it's passing a class which generates a timestamp and hash and has 4 optional parameters. It seems going by your answer I would need to create several versions of the class, one for each possible combination required, exposing the required properties for the operation. – Mick Jun 05 '17 at 05:18
  • I believe my original code didn't actually replicate the issue. I think including the ID as part of URL in the request as opposed to a parameter changes the behavior – Mick Jun 05 '17 at 05:22
  • @Mick, Then you need to show show the actual scenario. Showing anonymous types when that is not the case is misleading. – Nkosi Jun 05 '17 at 05:35
  • Sorry, as stated in the edit, after your response I realised the original code didn't exactly replicate the issue. I don't believe passing a class or an anonymous type makes a difference, as demonstrated by the code above its the presence of the property in the structure. The undesiderd behavior in UrlHelper occurs when you pass it a structure containing a property set to null or empty string. Obviously if you're using anonymous types its far easier to include or omit a specific property. – Mick Jun 05 '17 at 05:43
  • @Mick, Whether it is anonymous does matter as the framework uses all the properties of the object provided to make of the route data even if you populate it with null or empty. Because once you omit the property the problem goes away. – Nkosi Jun 05 '17 at 05:46
  • Yes omitting the property is a solution, it just doesn't happen to be an easy one if you're passing a class rather than an anonymous type. – Mick Jun 05 '17 at 05:55
  • @Mick what I am trying to get you to understand is that short of re writing the framework once the property belongs to the object passed into the route values it is going to be used. That is how the framework was written. It is even in the documentation. – Nkosi Jun 05 '17 at 05:59
  • @Mick it is the same logic behind the defaults used when using `MapHttpRoute`. The fact is that you used the api wrong and then expect to find a work around. That is not how the api was designed to be used. Find any examples where they pass in a concrete object when using `UrlHelper`. The only other overload uses a dictionary. – Nkosi Jun 05 '17 at 06:04
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/145844/discussion-between-mick-and-nkosi). – Mick Jun 05 '17 at 06:08