OK, there is lots going on here so I will try and keep my question and examples as simple as I can. With that in mind, please ask if you need any additional information or clarification on anything.
The code
I have a Web API 2 project which has a number of controllers and actions. The particular action I am having problems with is defined in the ContactController as follows:
[HttpPost]
public MyModel GetSomething(System.Nullable<System.Guid> uid)
{
return GetMyModel(uid);
}
In case it matters, my routing is setup as follows:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = RouteParameter.Optional }
);
Now I have another project that is required to call the above action. For calling the Web API I am using HttpClient. Note that I have lots of other actions calls which are working correctly, so this isn't a connectivity issue.
The code I am using to call the Web API method is as follows:
using (HttpClient client = GetClient())
{
var obj = new List<KeyValuePair<string, string>> { new KeyValuePair<string, string>("uid", someGuid.ToString()) };
var response = client.PostAsync(path, new FormUrlEncodedContent(obj)).Result;
return response.Content.ReadAsAsync<T>().Result;
}
In this instance, path is basically:
localhost:12345/api/contact/getsomething
The problem
The PostAsync call Result (i.e. response in the above code) gives this message:
{StatusCode: 404, ReasonPhrase: 'Not Found', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
Pragma: no-cache
X-SourceFiles: =?UTF-8?B?QzpcRGV2ZWxvcG1lbnRcUHJvamVjdHNcTGltYVxMaW1hIHYzXERFVlxMaW1hRGF0YVNlcnZpY2VcYXBpXHVzZXJhY2Nlc3NcZ2V0bW9kdWxlc2FjY2Vzcw==?=
Cache-Control: no-cache
Date: Fri, 18 May 2018 10:25:49 GMT
Server: Microsoft-IIS/10.0
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Content-Length: 222
Content-Type: application/json; charset=utf-8
Expires: -1
}}
If I put a breakpoint inside the aciton then it doesn't fire. However, what I find strange is that when I call it, Visual Studio (2018) tells me that the specific action has a "failed request" on that specific action. So clearly it must know which method I am trying to call?
At this point I am running out of ideas on how to debug further. What am I doing wrong here?
in this case you can use the same endpoint as for getting and posting.
so you probably need:
[HttpGet]
public IActionResult Get(System.Nullable<System.Guid> uid)
{
return GetMyModel(uid); //make sure you got it, oterhwise return a NotFound()
}
[HttpPost]
public IActionResult Post(InputModel model)
{
_service.doMagicStuff();
return Ok();
}
cheers!
not sure but error may be because of you are passing keyvalue pair
var obj = new List<KeyValuePair<string, string>> { new KeyValuePair<string, string>("uid", someGuid.ToString()) };
var response = client.PostAsync(path, new FormUrlEncodedContent(obj)).Result;
instead of guild only i.e. only string value expected by function , so it will be
var response = client.PostAsync(path, new FormUrlEncodedContent(someGuid.ToString())).Result;
method should be
[HttpPost]
public MyModel GetSomething(string uid)
{
return GetMyModel(Guid.Parse( uid));
}
You are sending the guid with the FormUrlEncodedContent but the requests content type is application/json.
I recommend you to send it as a json like this
using (HttpClient client = GetClient())
{
var obj = new { uid = someGuid.ToString()) };
var json = JsonConvert.SerializeObject(obj);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var result = client.PostAsync(path, content).Result;
return response.Content.ReadAsAsync<T>().Result;
}
Then in the api controller, use the FromBody attribute to declare that the parameter is read from the request body
[HttpPost]
public MyModel GetSomething([FromBody]RequestModel model)
{
return GetMyModel(model.uid);
}
public class RequestModel
{
public System.Nullable<System.Guid> uid { get; set; }
}
Also, if you only have one Post method in the contact controller the url localhost:12345/api/contact will be enough
Related
I started out with a .NET 6 minimal api which was working fine, however I needed some extra functionality, so I then decided to switch over to MVC style api. Since then, when my WPF app tries to call the endpoint using POST I get a 400 bad request.
If I use swagger to make the call, it works with no problem. But using HttpClient in my WPF app gives the error. I've looked at about a dozen other questions on this error but none of the solutions seemed to fix my problem.
When I run both the API and WPF app with debugging and debug the beginning of both the UserService call to InsertUser and the API's InsertUser methods, the API's debug point never gets hit, which to me sounds like when MVC is processing the request it sees something it doesn't like and returns the 400 error, but I'm not sure why the PUT request is fine and the POST request isn't.
In my ASP.net core program.cs I added the following to enable MVC api usage
builder.Services.AddControllers();
app.MapControllers();
In my WPF app the httpClient is handled through DI and injected in my UserService class used to make the call to the endpoint.
InsertUser POST method
public async Task<bool> InsertUser(UserModel user)
{
var result = await _httpClient.PostAsync("/api/Users",
user.AsJsonContent());
return result.IsSuccessStatusCode;
}
This is the UpdateUser method that uses PUT and works fine
public async Task<bool> UpdateUser(UserModel user)
{
var result = await _httpClient.PutAsync("/api/Users",
user.AsJsonContent());
return result.IsSuccessStatusCode;
}
Extension that turns the UserModel into a StringContent
public static StringContent AsJsonContent(this object obj)
{
string json = JsonConvert.SerializeObject(obj);
var content = new StringContent(json, Encoding.UTF8, "application/json");
//content.Headers.Remove("Content-Type");
//content.Headers.Add("Content-Type", "application/json");
//content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
return content;
}
This is a trimmed down version of my UsersController
[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
[HttpPost]
public async Task<IActionResult> InsertUser(UserModel model)
{
_appDbContext.Users.Add(model);
var result = await _appDbContext.SaveChangesAsync();
return result > 0 ? Ok() : Problem("User was not inserted");
}
[HttpPut]
public async Task<IActionResult> UpdateUser(UserModel model)
{
if(await _appDbContext.Users.FindAsync(model.Id) is UserModel efModel)
{
_mapper.Map(model, efModel);
_appDbContext.Users.Update(efModel);
var result = await _appDbContext.SaveChangesAsync();
return result > 0 ? Ok() : Problem("User was not updated");
}
return Problem("User not found.");
}
}
This is result from PostAsync
{StatusCode: 400, ReasonPhrase: 'Bad Request', Version: 1.1, Content: System.Net.Http.HttpConnectionResponseContent, Headers:
{
Date: Thu, 07 Jul 2022 14:35:53 GMT
Server: Kestrel
Transfer-Encoding: chunked
Content-Type: application/problem+json; charset=utf-8
}}
This is my response message
{Method: POST, RequestUri: 'https://localhost:7159/api/Users', Version: 1.1, Content: System.Net.Http.StringContent, Headers:
{
Content-Type: application/json
Content-Length: 253
}}
Edit: Adding reqested info
Here is the json responce for the bad responce
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-ac9fc01b742547b2525eef9de28ea6a0-d123c5b603ff637c-00",
"errors": {
"Token": [
"The Token field is required."
]
}
}
This was the minimal api version of InsertUser
//Registered in another class
app.MapPost("/Users", InsertUser);
public async Task<bool> InsertUser(UserModel model)
{
await _appDbContext.Users.AddAsync(model);
var result = await _appDbContext.SaveChangesAsync();
return result > 0;
}
since you are posting the data as a json content, you have to add a frombody attribute to both controller actions
public async Task<IActionResult> UpdateUser([FromBody]UserModel model)
Edit 1: Other Controller
public class identityController : ApiController
{
[HttpGet]
public async Task<IHttpActionResult> getfullname(string firstName)
{
string name = firstName;
return Ok(name);
}
}
I have created a controller which uses an API from another solution.
Method that i use in the controller looks like below:
public class GetNameController : ApiController
{
[HttpGet]
public async Task<IHttpActionResult> CalculatePrice(string firstName)
{
string _apiUrl = String.Format("api/identity/getfullname?firstName={0}", firstName);
string _baseAddress = "http://testApp.azurewebsites.net/";
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(_baseAddress);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
HttpResponseMessage response = await client.GetAsync(_apiUrl);
if (response.IsSuccessStatusCode)
{
return Ok(response);
}
}
return NotFound();
}
The result of response.IsSuccessStatusCode is always false. When i check the response values i see this result:
{
StatusCode: 400, ReasonPhrase: 'Bad Request', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
Connection: close
Date: Thu, 21 Jul 2016 12:28:21 GMT
Server: Microsoft-HTTPAPI/2.0
Content-Length: 334
Content-Type: text/html; charset=us-ascii
}
}
What could i be missing?
string _apiUrl = String.Format("api/identity/{0}", firstName);
This is assuming that your url is correct, and your testapp is up and running. Even though when I hit it azure tells me your app is stopped. You will need to get your app started first, then change the string _apiUrl to the suggestion above.
http://testapp.azurewebsites.net/api/identity/getfullname?firstName=steve
Gives me this message
Error 403 - This web app is stopped.
The web app you have attempted to reach is currently stopped and does
not accept any requests. Please try to reload the page or visit it
again soon.
If you are the web app administrator, please find the common 403 error
scenarios and resolution here. For further troubleshooting tools and
recommendations, please visit Azure Portal.
So there are several things in your identity controller that are going on.
the functions name is getFullName. Since the word get is in the name of the function. Any httpget request will be routed to the function automagically. Thus making the [HttpGet] redundant. This only works if there is 1 and only 1 httpget request in your controller. If there are multiple you will need to fully qualify the url like you have done
Since youa re using the [httpget] method attribute I can assume you are using webapi2. That being the case and you are using a
primitive in your controller argument you can do notneed to fully
qualify the parameter name on your call. ?firstname={0} changes to
/{0}
So, i'm trying to pass multiple parameters from fiddler to my web api, using FormDataCollection.ReadAsNameValueCollection().
Problem is everytime is send my data, formData comes back as null. I'm not sure what I'm doing wrong here. I've tried decorating formData with a [FromBody] attribute. Also registered JsonMediaTypeFormatter() in the global.asax class.
Any help would be much appreciated.
Please see code below:
[HttpPost]
public HttpResponseMessage PostAccount([FromBody]FormDataCollection formData)
{
if (formData != null)
{
var nValueCol = formData.ReadAsNameValueCollection();
var account = new Account()
{
Email = nValueCol["email"],
Password = nValueCol["password"],
AgreedToTerms = Convert.ToBoolean(nValueCol["agreesToTerms"]),
//LocationAccountCreated = DbGeography.FromText(nValueCol["userLocation"])
};
var userProfile = new UserProfile()
{
FirstName = nValueCol["firstName"],
LastName = nValueCol["lastName"],
DateOfBirth = Convert.ToDateTime(nValueCol["dateOfBirth"])
};
var newAcc = _accountService.CreateAccount(account.Email, userProfile.FirstName, userProfile.LastName,
userProfile.DateOfBirth, account.Email, account.AgreedToTerms,
account.LocationAccountCreated);
var response = Request.CreateResponse(HttpStatusCode.Created);
return response;
}
else
return Request.CreateResponse(HttpStatusCode.NotAcceptable);
}
Sample request:
FormDataCollection is normally associated with application/x-www-form-urlencoded media type.
Your screen shot shows you are trying to send json data. If you don't have a concrete data type for your data and you want to send it as Json you can use an IDictionary<string,string> which will be mapped by the model binder successfully.
You action will look something like...
[HttpPost]
public HttpResponseMessage PostAccount([FromBody]IDictionary<string, string> formData) {
if (formData != null) {
var nValueCol = formData;
//...other code removed for brevity, but can basically stay the same
var response = Request.CreateResponse(HttpStatusCode.Created);
return response;
} else
return Request.CreateResponse(HttpStatusCode.NotAcceptable);
}
Based on your code and the information from your fiddler screen shot, a TestController was created, a request was tested with fiddler like...
POST http://localhost:35979/api/account/create HTTP/1.1
User-Agent: Fiddler
Host: localhost:35979
Content-Type: application/json
Content-Length: 76
{"email":"myemail#email.com",
"firstname":"myFName",
"lastName":"myLName"}
...and the formData was populate with the 3 fields and their data.
Is it possible to send [FromBody] POST data to a controller using client.GetAsync() (or PostAsync/SendAsync?
I had to set up a base controller that all api calls will go through.
My ajax calls all go to this SecureApi controller, and they send the original path as a parameter to that they can be re-routed to the correct controller. Something like:
$.ajax({
url: "./api/SecureApi/?path=/api/OtherApi/SomeRoute",
data: {
param1: 1,
param2: 2
}
});
So my base controller looks something like:
public class SecurityApiController : ApiController
{
[HttpPost]
public HttpResponseMessage SecureApi([FromBody]object data, string path)
{
// do some stuff
// get uri
var applicationPath = Request.RequestUri.Scheme + "://" + Request.GetRequestContext().VirtualPathRoot.Replace("/", String.Empty);
Uri routeUri = new Uri(applicationPath + path);
// then redirect to correct controller
var config = new HttpConfiguration();
var server = new HttpServer(config);
var client = new HttpClient(server);
// how can I send [FromBody]object data here?
var response = client.GetAsync(routeUri).Result; // or PostAsync/SendAsync?
return response;
}
}
The other controller looks like:
public class OtherApiController : ApiController
{
[HttpPost]
public HttpResponseMessage OtherApi([FromBody]OtherData data)
{
// do stuff
}
}
Unfortunately I can't change OtherApi, so I HAVE to send the [FromBody] POST data in the same way (in the POST body).
Is that possible?
EDIT:
Per #Philippe's response below, I'm using PostAsJsonAsync and it seems to want to work, but I'm getting a 401 Unauthorized result. More info:
I went with the correct(?) ASYNC/AWAIT route...
public async Task<HttpResponseMessage> SecureApi([FromBody]Dictionary<string, dynamic> data, string path)
{
...
var client = new HttpClient();
var response = await client.PostAsJsonAsync(routePath, data);
return response;
}
And the Other controller has:
[Authorize(Roles = "Admin")] // I do have the "Admin" role
[Route("Save")]
[HttpPost]
public SaveResultBase Save([FromBody]Dictionary<string, dynamic> data)
{
...
}
But this controller is never hit (no breakpoints are hit there) and it returns a 401 Unauthorized response.
I guess that I have to add my user credentials to the client headers before calling PostAsJsonAsync. Can't find any way to do that though.
The method GetAsync of HttpClient will send a HTTP GET request so it would only be possible to have [FromUri] arguments. Because [FromBody] argument are by definition POST data, you will want to use PostAsJsonAsync/ PostAsXmlAsync/PostAsync. The difference between all of them is how the data is serialized.
var response = client.PostAsJsonAsync(routeUri, data).Result;
That being said, if you have security in mind, it would be rather easy for anyone to call the "right api" directly. Moreover you will increase latency by generating two HTTP requests.
You should take a look at this guide on MSDN. I believe that an authentication filter is probably what you are looking for.
I've got the following code running in a Windows Store application that is supposed to call one of my WebApis:
using (var client = new HttpClient())
{
var parms = new Dictionary<string, string>
{
{"vinNumber", vinNumber},
{"pictureType", pictureType},
{"blobUri", blobUri}
};
HttpResponseMessage response;
using (HttpContent content = new FormUrlEncodedContent(parms))
{
const string url = "http://URL/api/vinbloburis";
response = await client.PostAsync(url, content);
}
return response.StatusCode.ToString();
}
The WebApi code looks like this:
[HttpPost]
public HttpResponseMessage Post(string vinNumber, string pictureType, string blobUri)
{
var vinEntity = new VinEntity
{
PartitionKey = vinNumber,
RowKey = pictureType,
BlobUri = blobUri
};
_vinRepository.InsertOrUpdate(vinEntity);
return new HttpResponseMessage { Content = new StringContent("Success"), StatusCode = HttpStatusCode.OK };
}
Using Fiddler, I've observed the following ... here's what the request looks like:
POST http://URL/api/vinbloburis HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Host: URL
Content-Length: 113
Expect: 100-continue
vinNumber=vinNumber&pictureType=top&blobUri=https%3A%2F%2Fmystorage.blob.core.windows.net%2Fimages%2Fimage.png
But the response is an error with little/no information:
{"Message":"An error has occurred."}
I've even tried locally with the debugger attached to the WebApis and my Post method never catches.
Does anyone see something I've missed here? I feel like I'm looking right at it but not seeing it. I should add that I am able to call an HttpGet method while passing a parameter through the querystring. Right now the problem is only with this HttpPost.
Thanks!
UPDATE: Based on some good comments from folks I'm adding some more details.
I have the default routes configured for WebApis ...
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
Consequently, I think /URL/api/vinbloburis should work. Additionally, as alluded above, I currently have this working with an HttpGet. Here's what's working in the Windows Store app to call the HttpGet WebApi ...
using (var client = new HttpClient())
{
using (var response = await client.GetAsync(uri))
{
if (response.IsSuccessStatusCode)
{
var sasUrl = await response.Content.ReadAsStringAsync();
sasUrl = sasUrl.Trim('"');
return sasUrl;
}
}
}
... and it's calling the following WebApi ...
[HttpGet]
public HttpResponseMessage Get(string blobName)
{
const string containerName = "images";
const string containerPolicyName = "policyname";
_helper.SetContainerPolicy(containerName, containerPolicyName);
string sas = _helper.GenerateSharedAccessSignature(blobName, containerName, containerPolicyName);
return new HttpResponseMessage
{
Content = new StringContent(sas),
StatusCode = HttpStatusCode.OK
};
}
I hope the additional information helps!
I copied your code, as is and I was getting a 404 error.
I changed the signature to
public HttpResponseMessage Post(FormDataCollection data)
and it worked.
You can also just do this,
public HttpResponseMessage Post(VinEntity vinEntity)
and the model binder will do the mapping work for you.
Rick Strahl has a post on the issue here http://www.west-wind.com/weblog/posts/2012/Sep/11/Passing-multiple-simple-POST-Values-to-ASPNET-Web-API
Have you turned on Internet Networking in your manifest?
It may be a Cross Domain Ajax Security issue. See JSONP info here. => http://json-p.org/