Why is my Web API routing being re-routed / falsel

2019-09-18 15:46发布

问题:

I have two GET methods for a particular Controller / Repository:

public IEnumerable<InventoryItem> GetAllInventoryItems()
{
    return inventoryItemsRepository.GetAll();
}

[Route("api/{controller}/{ID}/{CountToFetch}")]
public IEnumerable<InventoryItem> GetBatchOfInventoryItemsByStartingID(string ID, int CountToFetch)
{
    return inventoryItemsRepository.Get(ID, CountToFetch); 
}

Even though I've tried calling it all these ways from the client, with two arguments:

0)
formatargready_uri = string.Format("http://localhost:28642/api/inventoryItems/{0}/{1}", lastIDFetched, RECORDS_TO_FETCH);
var webRequest = (HttpWebRequest)WebRequest.Create(formatargready_uri); 

1)
formatargready_uri = string.Format("http://localhost:28642/api/inventoryItems/?ID={0}&CountToFetch={1}", lastIDFetched, RECORDS_TO_FETCH);
var webRequest = (HttpWebRequest)WebRequest.Create(formatargready_uri); 

2)
formatargready_uri = string.Format("http://localhost:28642/api/inventoryItems/ID={0}&CountToFetch={1}", lastIDFetched, RECORDS_TO_FETCH);
var webRequest = (HttpWebRequest)WebRequest.Create(formatargready_uri); 

...in each case it is still the first method (GetAll) that is being called. Why?

Here is my Repository code:

public IEnumerable<InventoryItem> GetAll()
{
    return inventoryItems;
}

public IEnumerable<InventoryItem> Get(string ID, int CountToFetch)
{
    return inventoryItems.Where(i => 0 < String.Compare(i.Id, ID)).Take(CountToFetch);
}

...and here is what's in WebApiConfig.cs:

public static void Register(HttpConfiguration config)
{
    config.MapHttpAttributeRoutes();

    config.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = RouteParameter.Optional }
    );

    config.Routes.MapHttpRoute(
        name: "DefaultApiWithParameters",
        routeTemplate: "api/{controller}/{ID}/{CountToFetch}",
        defaults: new { ID = RouteParameter.Optional, CountToFetch = RouteParameter.Optional }
    );

}

UPDATE

This will show what I've tried as to routing calls to the Controller method I'm trying to have run:

//[HttpGet]
//[Route("api/{controller}/{ID:string}/{CountToFetch:int}")] <-- throws exception - won't run
//[Route("{controller}/{ID:string}/{CountToFetch:int}")] <-- throws exception - won't run
//[Route("inventoryItems/{ID:string}/{CountToFetch:int}")] <-- throws exception - won't run
//[Route("api/inventoryItems/{ID:string}/{CountToFetch:int}")] <-- throws exception - won't run
//[Route("api/{controller}/{ID}/{CountToFetch}")] // <-- runs, but is not called
[Route("api/InventoryItemsController/{ID}/{CountToFetch}")] // <-- runs, but is not called
//[Route("api/{controller}/{ID:string}/{CountToFetch:int}")] <-- throws exception - won't run

So the method that I don't want to be called is extremely robust: no matter how I decorate the other method, or how I call it, the undesired one runs.

UPDATE 2

Wouldn't it have been easier to just allow the calling of Controller methods by name? e.g, given this Controller method:

public IEnumerable<InventoryItem> GetBatchOfInventoryItemsByStartingID(string ID, int CountToFetch)
{
    return inventoryItemsRepository.Get(ID, CountToFetch); //.Where(i => string.Equals(p.Category, category, StringComparison.OrdinalIgnoreCase));
}

...why [c,w]ouldn't it be called from the client like so:

formatargready_uri = string.Format("http://localhost:28642/api/InventoryItemsController.GetBatchOfInventoryItemsByStartingID/{0}/{1}", lastIDFetched, RECORDS_TO_FETCH);

???

ISTM that would be something that like that would have been a lot more intuitive.

UPDATE 3

So what it boils down to is: Why is my GetAll() method getting called, when it takes no args?

Possibly the routing mechanism is getting confused because of lastIDFetched being set to an empty string:

string lastIDFetched = string.Empty;

...and so then formatargready_uri. which is assigned to this way:

formatargready_uri = string.Format("http://locohost:28642/api/InventoryItems/{0}/{1}", lastIDFetched, RECORDS_TO_FETCH);

...is at first:

"http://locohost:28642/api/InventoryItems//100"

(when I would expect it to be:

"http://locohost:28642/api/InventoryItems/""/100"

)

Could it be that the "missing" first arg is what's throwing the routing mechanism off, so that when it sees:

"http://locohost:28642/api/InventoryItems//100" 

...it doesn't know whether to call this:

public IEnumerable<InventoryItem> GetBatchOfInventoryItemsByStartingID(string ID, int CountToFetch)
{
    return inventoryItemsRepository.Get(ID, CountToFetch); //.Where(i => string.Equals(p.Category, category, 

StringComparison.OrdinalIgnoreCase)); }

...or this:

public IEnumerable<InventoryItem> GetAllInventoryItems()
{
    return inventoryItemsRepository.GetAll();
}

???

UPDATE 4

When I comment out the other method, so that the client has no choice but to see the only existing method in the Controller/Repository, it does nothing on this line:

var webRequest = (HttpWebRequest)WebRequest.Create(formatargready_uri);

(the two-arg method in the Controller still isn't called)

This is all that's in the Controller now:

public class InventoryItemsController : ApiController
{
    static readonly IInventoryItemRepository inventoryItemsRepository = new InventoryItemRepository();

    [Route("api/InventoryItems/{ID}/{CountToFetch:int}")] // <-- with this route decoration commented out or not, makes no difference
    public IEnumerable<InventoryItem> GetBatchOfInventoryItemsByStartingID(string ID, int CountToFetch)
    {
        return inventoryItemsRepository.Get(ID, CountToFetch); //.Where(i => string.Equals(p.Category, category, StringComparison.OrdinalIgnoreCase));
    }    
}

Here is the corresponding Repository interface:

interface IInventoryItemRepository
{
    IEnumerable<InventoryItem> Get(string ID, int CountToFetch); 
    InventoryItem Add(InventoryItem item);
}

...Repository implementation:

public class InventoryItemRepository : IInventoryItemRepository
{
    private readonly List<InventoryItem> inventoryItems = new List<InventoryItem>();

    public InventoryItemRepository()
    {
// code that populates inventoryItems by calling Add() not shown - it works, though
    } 

    public IEnumerable<InventoryItem> Get(string ID, int CountToFetch)
    {
        return inventoryItems.Where(i => 0 < String.Compare(i.Id, ID)).Take(CountToFetch);
    }

    public InventoryItem Add(InventoryItem item)
    {
        if (item == null)
        {
            throw new ArgumentNullException("item");
        }
        inventoryItems.Add(item);
        return item;
    }        
}

...and the client code that calls it:

formatargready_uri = string.Format("http://localhost:28642/api/InventoryItems/{0}/{1}", lastIDFetched, RECORDS_TO_FETCH);
var webRequest = (HttpWebRequest)WebRequest.Create(formatargready_uri);

UPDATE 5

Well, I'll be darned like a holey sock. It seems to have been a problem with starting out with the empty string, after all. When I changed the initial value of lastIDFetched from string.Empty to "billy" it worked... Is this a bug? Is there a workaround? If I want to start from "scratch," what would I use rather than string.Empty? A blank space (" ") also doesn't work.

回答1:

First, why are you having {controller} in the following attribute route? By decorating with an attribute route here you already are indicating the controller and action which should be hit, so remove {controller} here and replace it with the controller name.

[Route("api/{controller}/{ID}/{CountToFetch}")]
public IEnumerable<InventoryItem> GetBatchOfInventoryItemsByStartingID(string ID, int  CountToFetch)
{
   return inventoryItemsRepository.Get(ID, CountToFetch); 
}

Requests 1) and 2) where you are using query string would not work because ID and CountToFetch are required route parameters.