I'm new to MVC Web Api but I have some experience working with ServiceStack framework. Some of the web api examples look a lot like RPC with more action methods and less parameters. In fact most examples seem to limit the request parameter to an id. I would like to create some reporting services using request/response because the request gets quite complex with all the reporting criteria.
Here's my simplified request / response types:
public class PendingRequest
{
public string Id { get; set; }
public int AccountId { get; set; }
public DateTime? FromDate { get; set; }
public DateTime? ToDate { get; set; }
}
public class PendingResponse
{
public string Id { get; set; }
public IEnumerable<Pending> Data { get; set; }
}
And my outline reports controller:
public class ReportsController : ApiController
{
public async Task<PendingResponse> GetPending(PendingRequest request)
{
return new PendingResponse
{
Id = request.Id,
// Data = await Repo.GetPending()
};
}
public async Task<ShippedResponse> GetShipped(ShippedRequest request)
{
return new ShippedResponse
{
Id = request.Id,
// Data = await Repo.GetShipped()
};
}
public async Task<ProductsResponse> GetProducts(ProductsRequest request)
{
return new ProductsResponse
{
Id = request.Id,
// Data = await Repo.GetProducts()
};
}
}
And my routing and config for a self-hosting app:
class Program
{
static void Main()
{
var config = new HttpSelfHostConfiguration("http://localhost:8080");
config.Routes.MapHttpRoute(
name: "Reports",
routeTemplate: "api/reports/{action}",
defaults: new
{
controller = "Reports"
});
using (var server = new HttpSelfHostServer(config))
{
server.OpenAsync().Wait();
Console.WriteLine("Press Enter to quit.");
Console.ReadLine();
}
}
}
So with this routing I intend the action to be the name of the report. Note i've not included the request parameter here. Not sure if I should or not.
The problem is actually on the client side. With the HttpClient the GetAsync method does not allow me to include the JSON request object. I see an extension method PostAsJsonAsync but surely this shouldn't be a POST? Without a GetAsJsonAsync or similar then I can't see a way of including a request with a GET?
To be honest I prefer ServiceStack but there appears to be no async support yet. I need to develop some high performance services which are I/O intensive and I want to reduce the blocking as much as possible.
UPDATE:
It seems Web Api will perform the model binding if, rather than using the request body, I include the model's parameters as part of the query string. In order for this to work, the Get method needs to prefix the model type with the [FromUri] attribute, which informs the model binder that the model should be constructed from the request query string. Messy, but works.
[ActionName("Pending")]
public async Task<PendingResponse> GetPending([FromUri] PendingRequest request)
{
return new PendingResponse
{
Id = request.Id,
// query = ...build from request params
// Data = await Repo.GetPending(query)
};
}
And now on the client side I perform the following:
var result = await _httpClient.GetAsync("api/reports/pending?Id=123&AccountId=456");
result.EnsureSuccessStatusCode();
var response = await result.Content.ReadAsAsync<PendingResponse>();
A call to the service results in the GetPending method being called with a copy of the PendingRequest object materialised from the supplied query string parameters.
The fundamental problem here is you are trying to provide a request body to HTTP GET, which is not allowed. Well, you can still go ahead and format a GET with request body and submit it but that is not according to the spec. Strictly speaking, HTTP spec does not forbid GET requests from having a body but the response for a GET request must not change based on the request body, which basically means you cannot use search criteria in the GET request body. You can use the URI path and query string to specify the search criteria and if you want to bind them into a complex type parameter, you will need to use [FromUri] like this: public async Task<PendingResponse> GetPending([FromUri]PendingRequest request).
Bit of a guess, but what about just URL encoding the JSON request object and putting it in the query string segment of the GET URI?
Also, you might need [FromUri] attribute on your request method parameter in the action as it is a complex type. I may have missed something, but this article on parameter binding doesn't mention anything about GET being a special case for complex types, hence I think they will always try and read from the POST body.
Related
I have a controller with the following setup for API requests:
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" }
);
This works great for GET requests, but for POST requests my parameters do not seem to have any values. On the frontend, in JavaScript, I can see my parameters in the payload so I know they are there. However, my controller must not be set up correctly to take the POST requests.
Here is my GET request which works, and my POST which doesn't. I believe my issue is that I set the controller route to require the ? and take parameters. However, I still need to do post requests!
GET request:
public ActionResult Contacts([FromQuery] String DD_INPUT)
{
//We have parameters here just in case we want to use them
IEnumerable queryResult;
String query = "Exec dbo.storedprocedure2 #DD_INPUT";
using (var connection = new SqlConnection(connectionString))
{
queryResult = connection.Query(query, new { DD_INPUT = DD_INPUT });
}
return Ok(queryResult);
}
POST request:
[HttpPost]
public ActionResult AddFirm([FromBody] String FIRM_NAME)
{
String query = "exec dbo.storeprocedername #FIRM_NAME";
System.Diagnostics.Debug.WriteLine("value:" + FIRM_NAME);
using (var connection = new SqlConnection(connectionString))
{
var json = connection.QuerySingle<string>(query, new { FIRM_NAME = FIRM_NAME});
return Content(json, "application/json");
}
}
POST Request JavaScript
axios.post(window.location.origin + '/API/AddFirm', {
FIRM_NAME: this.FirmName
}).then(response => {
this.FirmsArray = response.data;
}).catch(error => {
console.log(error.response.data.error);
});
if you want to use [frombody] in your action, you will have to stringify your data
const json = JSON.stringify(this.FirmName);
await axios.post(window.location.origin + '/API/AddFirm', json, {
headers: {
'Content-Type': 'application/json'
}
}).then(response => {
this.FirmsArray = response.data;
}).catch(error => {
console.log(error.response.data.error);
});
I am only not sure about url that you are offering. Usually Api has a different url than application it is called from. And it usually looks like "/MyController/MyAction". Unfortunately you didn't post your controller header.
UPDATE
if you need post several properties using [frombody] action you need to change your action too
Create view model
public class ViewModel
{
public string FirmName {get; set;}
public string Email {get; set;}
}
action
[HttpPost]
public ActionResult AddFirm([FromBody] ViewModel viewModel)
ajax
data: JSON.stringify({FirmName: this.FirmName, Email: this.CurrentEmail}),
Your configuration up top is your service configuration where you configure that all controllers and all endpoints in your controllers have the format of "{controller}/{action}/{id?}".
You can configure how your route is built not only on API level like you did in your example, but also on Controller and Endpoint level:
i.e.
[ApiController]
[Route("{controller}s"}
public class FirmController : ControllerBase
{
[HttpGet]
[Route("/{firmId}/contacts/{contactId}"]
public ActionResult GetContacts([FromRoute] int firmId, [FromRoute] int contactId)
{
...
}
[HttpPost]
public ActionResult AddFirm([FromBody] string firmName)
{
...
}
}
or even better, add a FirmModel for adding a new firm.
Also give https://learn.microsoft.com/en-us/azure/architecture/best-practices/api-design a read for properly designing an API.
So, the optimal solution would be this (Changes are explained in code comments):
[HttpPost]
public ActionResult AddFirm(string FIRM_NAME) // Can accept value from body AND query string. If you have more firm names then simply use List<string>/IEnumable<string> to represent it. If you have more parameters you want to pass, then simply write them like this: string FIRM_NAME, int NUM_OF_EMPLYEES and so on.
{
// Why are you using class String, when you can use the string keyword that does exactlly the same thing?
String query = "exec dbo.storeprocedername #FIRM_NAME";
System.Diagnostics.Debug.WriteLine("value:" + FIRM_NAME);
using (var connection = new SqlConnection(connectionString))
{
var json = connection.QuerySingle<string>(query, new { FIRM_NAME = FIRM_NAME});
return Content(json, "application/json"); // When sending JSON answear, be aware that every parameter from object will start with lowercased letter.
}
}
If you incist on using the [FromBody] tag, then you sadlly have to use models.
I'm trying to pass some JSON on the querystring of a GET request to an MVC controller, but can't seem to get it to come through as anything other than null.
Ajax (through TypeScript)
$.ajax(url, {
method: 'GET',
data: { 'request': JSON.stringify(this.request) },
dataType: 'json'
})
MVC Controller
[Route("stuffAndThings/{request?}")]
public async Task<HttpResponseMessage> GetStuff(requestType request)
{
}
Because this is TypeScript, the object being passed is the TypeScript representation of the C# model, including several custom objects
TS class
class requestType {
pageData: PageData;
}
C# class
public class requestType
{
public PageData pageData { get; set; } = new PageData();
}
Looking at the request in devtools, it appears to be being passed correctly on the querystring, but always comes through on the controller as null.
What am I missing?
EDIT
To address a couple of comments, the controller method is purely for data retrieval, and does have the potential in the future to be turned into a WebAPI method, so I would like to keep it as a GET request if possible.
In MVC controller you will get the parameter as string because you have passed parameters as string through GET request
[Route("stuffAndThings/{request?}")]
public async Task<HttpResponseMessage> GetStuff(string request)
{
}
Make requestType class serializable,
now in your method you have to deserialize the json string into your object
using (var ms = new MemoryStream(Encoding.Unicode.GetBytes(request)))
{
DataContractJsonSerializer deserializer = new DataContractJsonSerializer(typeof(requestType));
requestType requestObj = (requestType)deserializer.ReadObject(ms);
//your code here
}
Json.Stringfy convert your request in string form, and in controller your are fetching with particular type. So, to get the proper result update with strin g instead of RequestType.
if still face issue you reach out to me.
I am here
Please response or like if its helpful.
I try to make server-side processing for DataTables using Web API. There are two actions in my Web API controller with same list of parameters:
public class CampaignController : ApiController
{
// GET request handler
public dtResponse Get(int draw, int start, int length)
{
// request handling
}
// POST request handler
public void Post(int draw, int start, int length)
{
// request handling
}
}
If I use GET method to send AJAX request to the server, the Get action is activated. However, if I use POST method, then neither action are activated.
I tried to change POST handler signature to
public void Post([FromBody]object value)
{
// request handling
}
In this case the value is null. Note, the HttpContext.Current.Request.Form collection isn't empty. The draw, start, length variables are exist in this collection. Thus, I think the trouble is in model binding, but I cannot fix it. Help me, please.
Not knowing exactly what's going on, but appears there are a few missing elements. I've written a Post endpoint this morning, So hopefully will help pushing you in the right direction.
Also to note if you want "Data" use Get, if your inserting data then POST
[HttpPost]
[Route("orders")]
public async Task<IHttpActionResult> Post([FromBody]List<Models.Model.Order> orders)
{
if (orders == null)
return BadRequest("Unusable resources.");
if (validatedOrders.Count <= 0)
return BadRequest("Unusable resources.");
try
{
//Create abstracted Identity model to pass around layers
var identity = User.Identity as ClaimsIdentity;
var identityModel = IdentityModel.Create(identity);
if (identityModel == null)
return StatusCode(HttpStatusCode.Forbidden);
var response = await _orderService.AddAsync(validatedOrders, identityModel);
return Ok(response);
}
catch (System.Exception ex)
{
return InternalServerError();
}
finally
{
_orderService.Dispose();
}
}
Utilizing IHttpActionResult will expose the returning of a response
Verb attributes helps with building/designing Restful API's and same
name signatures
Attribute Routing saves writing in the config and change routes in
the class
To wrap it all up, replace the order collection with:
public sealed class Diagram
{
public int Draw { get; set; }
public int Start { get; set; }
public int Length { get; set; }
}
Rewrite the validation, remove the Identity creation, remove the insert and remove/replace the attribute routing.
With HttpContext.Current.Request.Form Try building a Diagram object from that and passing it up.
OR Alternatively passing in a form collection
[HttpPost]
[Route("something")]
// POST api/<controller>
public async Task<HttpResponseMessage> Post(FormDataCollection form)
{
string tid = form.Get("tid");
string sid = form.Get("sid");
string userid = form.Get("userid");
string udid = form.Get("udid");
}
Additional resource from the DataTable Docs
Git Hub Repo
What about this:
[HttpPost]
public void Post([FromBody]int draw, [FromBody]int start, [FromBody]int length)
{
// request handling
}
Below is WebAPI action. On googling about the below error:-
The requested resource does not support http method 'POST'
I got number of links & updated my api accordingly but still I am getting the same error.
Web api not supporting POST method
ASP.NET Web Api: The requested resource does not support http method 'GET'
[AcceptVerbs("POST")]
[HttpPost]
[Route("rename/{userId}/{type}/{title}/")]
public IHttpActionResult Rename([FromBody] int userId, [FromBody] string type, [FromBody] string title)
{
//my api stuff
}
But still when calling the above via post man throws the error.
How do I get rid of this error??
Also is it possible to fix this without using [FromBody] attribute in the method parameters list?
Any help/suggestion highly appreciated.
Thanks.
You have declared route which requires url parameters
[Route("rename/{userId}/{type}/{title}/")]
So when you send request to api/customer/rename it does not match this method. You should remove parameters which you are passing in request body from route parameters
[Route("rename")]
Make sure that you have appropriate RoutePrefix("api/customer") attribute on your controller.
Second problem is multiple [FromBody] parameters. You will get can't bind multiple parameters error. There is limitation - you can mark only one parameter as FromBody. See Sending Simple Types notes:
Web API reads the request body at most once, so only one parameter of
an action can come from the request body. If you need to get multiple
values from the request body, define a complex type.
You should create complex type which will hold all parameters
public class RenameModel
{
public int UserId { get; set; }
public string Type { get; set; }
public string Title { get; set; }
}
And change method signature to
[HttpPost]
[Route("rename")]
public IHttpActionResult Rename(RenameModel model)
And send request data as application/x-www-form-urlencoded
[Route("rename/{userId}/{type}/{title}/")]
public IHttpActionResult Rename([FromBody] int userId, [FromBody] string type, [FromBody] string title)
The last answer is correct, you're asking for these parameters in the route, but saying that you expect them in the post body. Also, usually the route would begin with a noun rather than a verb. What is it you're renaming? (i.e. [Route("users/rename/{userId}/{type}/{title}")]
Based on your initial post, try this instead:
[HttpPost]
[Route("rename/{userId}/{type}/{title}" Name = "RenameUser"]
public IHttpActionResult Rename(int userId, string type, string title)
{
_myServiceMethod.Rename(userId, type, title);
return new StatusCodeResult(HttpStatusCode.Created, this);
}
Or, if you wanted to do a post with the info in the body:
Declare your data contract:
public class User
{
public string Type { get; set; }
public string Title { get; set; }
}
Then on the endpoint:
[HttpPost]
[Route("rename/{userId}", Name = "RenameUserPost")]
public IHttpActionResult RenameUserPost(int userId, [FromBody] User userData)
{
return new StatusCodeResult(HttpStatusCode.Created, this);
}
Note that in both returns 'this' refers to your controller class that inherits from ApiController. Verified both of these in swagger, and they accept POSTs and return status codes.
Hope this helps.
I had this error for wrong string in Route string on top of my action.
[Route("api/TestReaderPercentStudyHomework/AddOrUpdate")]
I use Web API 2 Attribute Routing in my project to provide JSON interface over my data. I am facing weird behaviour of controller selection, not decided yet whether it's a bug or a feature :)
Let me describe my approach.
I would like to simulate OData syntax with help of attribute routing (direct OData usage has been refused due to design principles). For example, to get entity with id=5 I use HTTP GET request to URI http://mydomain.com/api/Entity(5) . I expect to use the same URI with HTTP PUT verb to update the entity. This is where the journey begins...
I would like to have separate controller for getting entities (FirstController in the example provided below) and another one for modifying entities (SecondController). Both controllers handles the same URI (e.g. http://mydomain.com/api/Entity(5)) the only difference is HTTP verb used with the URI - GET should be handled by FirstController, PUT should be handled by SecondController. But the URI is handled by none of them; instead HTTP 404 error is returned.
When I "merge" GET and PUT actions to only one controller (commented out in FirstController), both verbs are handled correctly.
I am using IIS Express and all conventional routes are disabled, only attribute routing is in charge.
It looks like the controller selection process does not work with HTTP verb. In another words, HttpGet and HttpPut attributes just limit action usage but they do not serve as criteria during controller selection. I am not so familiar with MVC / Web API fundamentals, so let me ask you my big question:
Is the behaviour, described herein before, a feature intentionally implemented by MVC / Web API 2 or a bug to be fixed?
If it is considered as a feature, it prevents me to follow design principles. I can live with "merged" controllers but still considering it as a bad practice...
Or am I missing something in my train of thought?
My environment setup:
Windows 7 (virtual machine using Oracle VirtualBox)
Visual Studio 2013
.NET 4.5.1
Web API 2
The following is implementation of FirstController class:
public class FirstController : ApiController
{
[HttpGet]
[Route("api/Entity({id:int})")]
public Output GetEntity(int id)
{
Output output = new Output() { Id = id, Name = "foo" };
return output;
}
//[HttpPut]
//[Route("api/Entity({id:int})")]
//public Output UpdateEntity(int id, UpdateEntity command)
//{
// Output output = new Output() { Id = id, Name = command.Name };
// return output;
//}
}
The following is implementation of SecondController class:
public class SecondController : ApiController
{
[HttpPut]
[Route("api/Entity({id:int})")]
public Output UpdateEntity(int id, UpdateEntity command)
{
Output output = new Output() { Id = id, Name = command.Name };
return output;
}
}
The following is implementation of a console application to test the described behaviour:
class Program
{
static void Main(string[] args)
{
// HTTP client initialization
HttpClient httpClient = new HttpClient();
httpClient.BaseAddress = new Uri("http://localhost:1567");
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// HTTP GET - FirstController.GetEntity
HttpResponseMessage getEntityResponse = httpClient.GetAsync("/api/Entity(5)").Result;
Output getOutput = getEntityResponse.Content.ReadAsAsync<Output>().Result;
// HTTP PUT - SecondController.UpdateEntity
UpdateEntity updateCommand = new UpdateEntity() { Name = "newEntityname" };
HttpResponseMessage updateEntityResponse = httpClient.PutAsJsonAsync("/api/Entity(10)", updateCommand).Result;
Output updateOutput = updateEntityResponse.Content.ReadAsAsync<Output>().Result;
}
}
For completion, the following are used DTOs:
public class UpdateEntity
{
public string Name { get; set; }
}
public class Output
{
public int Id { get; set; }
public string Name { get; set; }
}
Thanks in advance for your responses,
Jan Kacina
This design was intentional as we thought it to be an error case where a user would be having same route template on different controllers which can cause ambiguity in the selection process.
Also if we keep aside attribute routing, how would this work with regular routing? Let's imagine we have 2 regular routes where first one is targeted for FirstController and the second to SecondController. Now if a request url is like api/Entity(5), then Web API would always match the 1st route in the route table which would always hit the FirstController and would never reach SecondController. Remember that once Web API matches a route it tries to go till the action selection process and if the action selection process doesn't result in an action being selected, then an error response is sent to the client. You probably are assuming that if an action is not selected in one controller then Web API would route it to the next one in the route configuration. This is incorrect.
Route probing occurs only once and if it results in a match, then the next steps take place...that is controller and action selection. Hope this helps.