Reputation: 6588
UPDATE 9/12/2012:
I shared my code with a co-worker and everything worked fine for him the first time without any changes. So, there must be something environmental on my box, Anyone have any thoughts?
See Update Below
Set Up:
.Net 4.5
Self Hosted (console app) .Net 4.5 Web API application
Test harness using MSTest
My Web API app is mostly full of REST ApiControllers which all work properly as I expect with standard CRUD type stuff. Now I have a requirement (to add some objects to an internal queue) which doesn't seem to fit well into the REST CRUD model. I found this article which seems to say that you can do RPC style non-REST operations in Web API just fine.
I've written a new controller which looks like this:
public class TaskInstanceQueueController : ApiController
{
public void Queue(TaskInstance taskInstance)
{
// Do something with my taskInstance
Console.WriteLine("Method entered!");
}
}
In my proxy class which calls this, I have code which looks like this:
public class TaskInstanceQueueProxy : ITaskInstanceQueueProxy
{
readonly HttpClient _client = new HttpClient();
public TaskInstanceQueueProxy()
{
var apiBaseUrl = System.Configuration.ConfigurationManager.AppSettings["APIBaseUrl"];
_client.BaseAddress = new Uri(apiBaseUrl);
_client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
public void QueueTaskInstances(TaskInstance taskInstance)
{
QueueTaskInstanceViaAPI(taskInstance);
}
private async void QueueTaskInstanceViaAPI(TaskInstance taskInstance)
{
var response = await _client.PostAsJsonAsync("api/TaskInstanceQueue/Queue", taskInstance);
var msg = response.EnsureSuccessStatusCode();
}
}
Here are my routes:
config.Routes.MapHttpRoute("API Default", "api/{controller}/{id}", new {id = RouteParameter.Optional});
config.Routes.MapHttpRoute("API RPC Style", "api/{controller}/{action}", new { id = RouteParameter.Optional });
When I run a test against my proxy, I don't get any errors, but no break point ever hits inside my controller method, nor does the Method entered! message appear in the console. The break line on the var msg line never hits either. For whatever reason, it doesn't look like I'm properly using the HttpClient object to do this.
Again, this web api app is working just fine with a bunch of other apicontrollers, but they're all doing standard REST stuff.
Anyone have any clues?
UPDATE
If I put a try/catch around the PostAsJsonAsync call, I get the following:
A first chance exception of type 'System.Threading.ThreadAbortException' occurred in mscorlib.dll
System.Threading.ThreadAbortException: Thread was being aborted.
at System.Threading.Tasks.TaskHelpers.RunSynchronously(Action action, CancellationToken token)
at System.Net.Http.Formatting.JsonMediaTypeFormatter.WriteToStreamAsync(Type type, Object value, Stream writeStream, HttpContent content, TransportContext transportContext)
at System.Net.Http.ObjectContent.SerializeToStreamAsync(Stream stream, TransportContext context)
at System.Net.Http.HttpContent.LoadIntoBufferAsync(Int64 maxBufferSize)
at System.Net.Http.HttpClientHandler.PrepareAndStartContentUpload(RequestState state)
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at TaskManagerProxy.TaskInstanceQueueProxy.<QueueTaskInstanceViaAPI>d__0.MoveNext() in c:\Moso\MOSO\MOSO.Infrastructure\tm\TaskManagerProxy\TaskManagerProxy\TaskInstanceQueueProxy.cs:line 30
Line 30 is the line with the call.
Upvotes: 0
Views: 9508
Reputation: 12194
This answer does kind of depends on how many other methods you have defined in TaskInstanceQueueController
. Assuming Queue is your only one then I believe your routes would already work (albeit they are a bit untidy).
I have just built an example version of your code and managed to successfully Post to the Queue method and hit a break point by using Fiddler and Curl. I have elaborated on your example a little and showed how the RPC actions could be mixed with normal REST methods.
The example code is located on GitHub here.
Basically the issue is not specifically to do with the WebApi element (routes, config etc, although you should probably remove the Optional id and add the HttpPost attribute to the queue method) instead as your inital question suggested it is how you are calling the server and this should probably be another question.
It is unclear whether you have two projects and how the MS Test code is hosted etc?... but there is a good example of a WebApi integration test here that you can follow and when debugging the API using tools like Fiddler can quickly help eliminate and debug the routing config issues.
Working console program:
static void Main(string[] args)
{
// Set up server configuration
HttpSelfHostConfiguration config = new HttpSelfHostConfiguration("http://localhost:8080");
//Route Catches the GET PUT DELETE typical REST based interactions (add more if needed)
config.Routes.MapHttpRoute("API Default", "api/{controller}/{id}",
new { id = RouteParameter.Optional },
new { httpMethod = new HttpMethodConstraint(HttpMethod.Get, HttpMethod.Put, HttpMethod.Delete) });
//This allows POSTs to the RPC Style methods http://api/controller/action
config.Routes.MapHttpRoute("API RPC Style", "api/{controller}/{action}",
new { httpMethod = new HttpMethodConstraint(HttpMethod.Post) });
//Finally this allows POST to typeical REST post address http://api/controller/
config.Routes.MapHttpRoute("API Default 2", "api/{controller}/{action}",
new { action = "Post" },
new { httpMethod = new HttpMethodConstraint(HttpMethod.Post) });
using (HttpSelfHostServer server = new HttpSelfHostServer(config))
{
server.OpenAsync().Wait();
Console.WriteLine("Press Enter to quit.");
Console.ReadLine();
}
}
Working Controller
public class TaskInstanceQueueController : ApiController
{
public void Get(string id)
{
// Do something with my taskInstance
Console.WriteLine("Method entered!" + id);
}
[ActionName("Post")]
[HttpPost]
public void Post(TaskInstance taskInstance)
{
// Do something with my taskInstance
Console.WriteLine("REST Post Method entered!");
}
[ActionName("Queue")]
[HttpPost]
public void Queue(TaskInstance taskInstance)
{
// Do something with my taskInstance
Console.WriteLine("Queue Method entered!");
}
[ActionName("Another")]
[HttpPost]
public void Another(TaskInstance taskInstance)
{
Console.WriteLine("Another Method entered!");
}
}
Upvotes: 6
Reputation: 70696
You have ambiguous routes.
config.Routes.MapHttpRoute("API Default", "api/{controller}/{id}", new {id = RouteParameter.Optional});
config.Routes.MapHttpRoute("API RPC Style", "api/{controller}/{action}", new { id = RouteParameter.Optional });
When the request comes in for /api/TaskInstanceQueue/Queue, it matches the first route, and so the route data contains { controller = "TaskInstanceQueue", id = "Queue" }. The system then tries to discover the Post method, and cannot do so, because you have no Post (or PostXxx) method, so your HTTP call is failing.
You have a couple choices. One is you can put an explicit route (first) for this one controller:
config.Routes.MapHttpRoute("API RPC Style", "api/TaskInstanceQueue/{action}", new { controller = "TaskInstanceQueue" });
config.Routes.MapHttpRoute("API Default", "api/{controller}/{id}", new {id = RouteParameter.Optional});
The other is, if you know your IDs will always be numbers only, you can add a constraint to the ID-based route, which will cause it to not match "Queue" as an ID, and therefore fall down into the correct action-based route.
Upvotes: 4