HeyGuys
I'm working on a WebApi project that receives requests from clients and redirects these requests to other services that are not open for direct access.
By default, .Net serializes and deserializes the Json request parameters automatically, so I need to re-serialize them before calling the appropriate service. The same problem occurs when receiving the service response. I need to deserialize it before sending the response to the user; otherwise .Net framework will serialize it one more time, resulting in a "Json of Json" response.
I found this answer but it does not seem to work with .NetCore; so I tried to create my own ModelBinder that just reads the Json object and returns it.
class JsonUnformatterBinderProvider : Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
return new JsonUnformatterBinder(new SimpleTypeModelBinder(context.Metadata.ModelType));
}
}
and
class JsonUnformatterBinder : IModelBinder
{
private readonly IModelBinder _fallbackBinder;
public JsonUnformatterBinder(IModelBinder fallbackBinder)
{
_fallbackBinder = fallbackBinder;
}
public Task BindModelAsync(ModelBindingContext bindingContext)
{
string currMethod = bindingContext.ActionContext.HttpContext.Request.Method;
if ("POST".Equals(currMethod) || "PUT".Equals(currMethod))
{
string strData = new StreamReader(bindingContext.ActionContext.HttpContext.Request.Body).ReadToEnd();
bindingContext.Result = ModelBindingResult.Success(strData);
return Task.CompletedTask;
}
return _fallbackBinder.BindModelAsync(bindingContext);
}
}
This code is very simple, it was my first attempt and it worked well for my purposes. However, I still get the "Json of Json" problem when I take the second service answer and returns back to the user.
I basically have no idea what I can do to overcome this, so any workaround is welcome here.
If you need just redirect a request without modification, you could read it from input stream directly and send it to inner service. You could also use such approach to read responce from inner service.
//1. Set empty parameter list in action then neither serializator nor model binder are not invoked.
public async Task<ContentResult> ProxyAction(/*empty parameter list*/)
{
var newUrl = #"https://stackoverflow.com";
var data = this.Request.Body;
using (var client = new HttpClient())
{
//2. Read request body from input stream.
var reader = new StreamReader(data);
var json = reader.ReadToEnd();
using (var content = new StringContent(json))
{
//3. Set correct content type
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(this.Request.ContentType);
//4. Post request to inner service
var response = await client.PostAsync(newUrl, content);
//5. Read response without deserialization
var innerResponse = await response.Content.ReadAsStringAsync();
var contentType = response.Content.Headers.ContentType.ToString();
var statusCode = response.StatusCode;
//6. Return inner response without serialization
var outerResponse = this.Content(innerResponse, contentType);
outerResponse.StatusCode = (int)statusCode;
return outerResponse;
}
}
}
Related
I have written a C# Web API in VS Studio and have created numerous DTO's that get serialized into JSON entities which any client can consume. One of my API calls is to return a PDF file, so after some online research, I set up one of the DTO's is set up in the following format. I read somewhere you can do this, but not 100% sure:
public class MyCustomResult
{
public bool Success;
public DateTime? LastRunDate;
public string ErrorCode;
public string Message;
public ByteArrayContent ReportBody;
}
I do not get any errors when return the object as an IHttpActionResult:
return Ok(result);
I can see on the server that the ByteSize of the report byte[] is approx 700K. However, when I retrieve the object on the client, the JSON entity is approx 400B and no byte content in the ByteContentStream. When I run the query in Postman, I get an empty Header, so it appears that the ByteContentStream can't be serialized by Newtonsoft JSON.
Are there any options I should consider?
Here is a scenario where you'd use ByteArrayContent:
using(var req = new HttpRequestMessage(HttpMethod.Post, new Uri("https://example.com"))
{
req.Content = new ByteArrayContent(...);
using(var resp = await _client.SendAsync(req))
{
var data = await resp.Content.ReadAsAsync<object>();
}
}
What you'd want to do is this:
public class MyCustomResult
{
public bool Success;
public DateTime? LastRunDate;
public string ErrorCode;
public string Message;
public byte[] ReportBody; // <-- change this to byte[]
}
var dataToSend = new MyCustomResult(); // fill this in
using(var req = new HttpRequestMessage(HttpMethod.Post, new Uri("https://example.com"))
{
req.Content = new StringContent(
JsonConvert.SerializeObject(dataToSend, Encoding.UTF8, "application/json"));
using(var resp = await _client.SendAsync(req))
{
var data = await resp.Content.ReadAsAsync<object>();
}
}
(note: this code is not tested)
So what will happen is SerializeObject will convert that byte array into a Base64 string then send it.
The consumer would then have to decode that Base64 string. If it's another Newtonsoft.Json client and the model definitions match, then it will automatically decode it for you.
I understand you are doing an API endpoint. The above examples are to show the use of ByteArrayContent and why it exists in .NET. How you are returning data is correct: return Ok(response); as long as you fix your model.
So to sum it up:
ByteArrayContent is an implementation of HttpContent which is supposed to be used as a response body only. It's not to be used in conjunction with a JSON response.
I have a problem when try to update a controller-service at nifi instance. I try to make a "put" request to nifi instance and disable the controller-service.
this is my logic:
get a specific controller-service (controller-services/{id})
parse response message to ControllerServiceEntity object
update state of service like --> currentService.Component.State = "DISABLED"; (All part of entity same with first time i just update State poperty)
serialize modified service instance
request nifi-api put for update service (controller-services/{id})
and i get the Badrequest response with "Message body is malformed. Unable to map into expected format." message.
This is my method for put request:
public async Task<T> Put<T>(Uri url,T data) where T:IBaseEntitty
{
T resultEntity = default(T);
using (var client = new HttpClient())
{
var jsonSerializerSettings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
var requestContent = new StringContent(JsonConvert.SerializeObject(data,jsonSerializerSettings), Encoding.UTF8, "application/json");
var response = client.PutAsync(url,requestContent);
var content = response.Result.Content;
using (var reader = new StreamReader(await content.ReadAsStreamAsync()))
{
var result = await reader.ReadToEndAsync();
if (response.Result.StatusCode == HttpStatusCode.OK)
{
var template = Newtonsoft.Json.JsonConvert.DeserializeObject(result, typeof(T));
if (template != null)
{
resultEntity = (T)template;
}
}
}
}
return resultEntity;
}
Any idea please ?
The response body should contain a message indicating why the request failed. Additionally, the <NIFI_HOME>/logs/nifi-user.log and <NIFI_HOME>/logs/nifi-app.log may contain more details.
Also, I would suggest opening the Developer Tools in your web browser to see these requests in action. The UI uses the REST API exclusively for all of it's functionality.
As i understand the request message should be short; my bad was sending whole entity back. I opened the Developer Tools on browser and checked the nifi instance's own requests and compared with mine: than i noticed the request is just include properties that will be update, not whole entity.
The request body must include just state and revision information. This is the request body that sending by nifi-instance when it disables controller-services:
{"revision":{"clientId":"644bf345-015d-1000-e82d-047f6a9f9432","version":15},"component":{"id":"015b1030-a099-13d3-812c-77772afcaeb0","state":"DISABLED"}}
I changed my codes according this informations. This is my sample code that set controll-service for disabling:
var controllerService = new ControllerServiceEntity();
controllerService.Id = existingService.Id;
controllerService.Revision = existingService.Revision;
var component = new ControllerServiceDTO();
component.Id = existingService.Component.Id;
component.State = "DISABLED";
controllerService.Component = component;
After i send the new control-service instance instead of existing one than it worked as i expected.
I'm trying to capture the raw request data for accountability and want to pull the request body content out of the Request object.
I've seen suggestions doing a Request.InputStream, but this method is not available on the Request object.
Any idea of how to get a string representation of the Request.Content body?
In your comment on #Kenneth's answer you're saying that ReadAsStringAsync() is returning empty string.
That's because you (or something - like model binder) already read the content, so position of internal stream in Request.Content is on the end.
What you can do is this:
public static string GetRequestBody()
{
var bodyStream = new StreamReader(HttpContext.Current.Request.InputStream);
bodyStream.BaseStream.Seek(0, SeekOrigin.Begin);
var bodyText = bodyStream.ReadToEnd();
return bodyText;
}
You can get the raw data by calling ReadAsStringAsAsync on the Request.Content property.
string result = await Request.Content.ReadAsStringAsync();
There are various overloads if you want it in a byte or in a stream. Since these are async-methods you need to make sure your controller is async:
public async Task<IHttpActionResult> GetSomething()
{
var rawMessage = await Request.Content.ReadAsStringAsync();
// ...
return Ok();
}
EDIT: if you're receiving an empty string from this method, it means something else has already read it. When it does that, it leaves the pointer at the end. An alternative method of doing this is as follows:
public IHttpActionResult GetSomething()
{
var reader = new StreamReader(Request.Body);
reader.BaseStream.Seek(0, SeekOrigin.Begin);
var rawMessage = reader.ReadToEnd();
return Ok();
}
In this case, your endpoint doesn't need to be async (unless you have other async-methods)
For other future users who do not want to make their controllers asynchronous, or cannot access the HttpContext, or are using dotnet core (this answer is the first I found on Google trying to do this), the following worked for me:
[HttpPut("{pathId}/{subPathId}"),
public IActionResult Put(int pathId, int subPathId, [FromBody] myViewModel viewModel)
{
var body = new StreamReader(Request.Body);
//The modelbinder has already read the stream and need to reset the stream index
body.BaseStream.Seek(0, SeekOrigin.Begin);
var requestBody = body.ReadToEnd();
//etc, we use this for an audit trail
}
If you need to both get the raw content from the request, but also need to use a bound model version of it in the controller, you will likely get this exception.
NotSupportedException: Specified method is not supported.
For example, your controller might look like this, leaving you wondering why the solution above doesn't work for you:
public async Task<IActionResult> Index(WebhookRequest request)
{
using var reader = new StreamReader(HttpContext.Request.Body);
// this won't fix your string empty problems
// because exception will be thrown
reader.BaseStream.Seek(0, SeekOrigin.Begin);
var body = await reader.ReadToEndAsync();
// Do stuff
}
You'll need to take your model binding out of the method parameters, and manually bind yourself:
public async Task<IActionResult> Index()
{
using var reader = new StreamReader(HttpContext.Request.Body);
// You shouldn't need this line anymore.
// reader.BaseStream.Seek(0, SeekOrigin.Begin);
// You now have the body string raw
var body = await reader.ReadToEndAsync();
// As well as a bound model
var request = JsonConvert.DeserializeObject<WebhookRequest>(body);
}
It's easy to forget this, and I've solved this issue before in the past, but just now had to relearn the solution. Hopefully my answer here will be a good reminder for myself...
Here's this answer as an extension method:
using System.IO;
using System.Text;
namespace System.Web.Http
{
public static class ApiControllerExtensions
{
public static string GetRequestBody(this ApiController controller)
{
using (var stream = new MemoryStream())
{
var context = (HttpContextBase)controller.Request.Properties["MS_HttpContext"];
context.Request.InputStream.Seek(0, SeekOrigin.Begin);
context.Request.InputStream.CopyTo(stream);
var requestBody = Encoding.UTF8.GetString(stream.ToArray());
return requestBody;
}
}
}
}
I am trying to send http request of json data to a web service. It successfully get directed to the web service but the data is always null...
Here is my web service:
public bool CheckUserExist ([FromBody] string Email)
{
List<User> all_users = repo.getUsers();
var match = all_users.Find(i => i.Email == Email);
if (match == null)
{
return false;
}
else
{
return true;
}
}
and here is my Http Request:
var webAddr = "http://localhost:59305/api/User/CheckUserExist";
var httpWebRequest = (HttpWebRequest)WebRequest.Create(webAddr);
httpWebRequest.ContentType = "application/json; charset=utf-8";
httpWebRequest.Method = "POST";
using (var streamWriter = new StreamWriter(httpWebRequest.GetRequestStream()))
{
string json = "{\"Email\":\"Email\"}";
streamWriter.Write(json);
streamWriter.Flush();
}
var httpResponse = (HttpWebResponse)httpWebRequest.GetResponse();
using (var streamReader = new StreamReader(httpResponse.GetResponseStream()))
{
var result = streamReader.ReadToEnd();
return RedirectToAction("Index");
}
as i mentioned...Iam using a debugger at the web service function...the request get directed to the service but variable "Email" is always null
The quick fix is to change what you are posting. If you want your API endpoint to work with [FromBody] string Email, then you should change your "json":
string json = "\"a#b.c\"";
streamWriter.Write(json);
streamWriter.Flush();
However, you may want to consider a few other changes for the long term on your approach:
Let your repo return IQueryable instead of List or some other IEnumerable. As written, you will always return every user from the database. With Linq, you can leverage the lazy initialization and let your query only get the matching user instead of get EVERY user and then find the match. This will not scale well.
Use new HttpClient and async functionality in your action instead of HttpWebRequest
Instead of manually building your JSON, let .NET do the work and create a class that gets serialized
With the above solutions in place (except for first since you didn't post your data context, didn't want to make too many assumptions), here are some examples to get you started:
Define a shared user search class (shared either in same project or shared DLL)
public class UserSearch
{
public string Email { get; set; }
}
Let Web API map post against the search class
public bool CheckUserExist([FromBody] UserSearch userSearch)
{
IQueryable<User> all_users = repo.getUsers();
var isMatch = all_users.Any(i => i.Email == userSearch.Email);
return isMatch;
}
Change to HttpClient and use async to send API request with new search class
public async Task<ActionResult> Check()
{
using (var client = new HttpClient())
{
var search = new UserSearch() { Email = "a#b.c" };
var response = await client.PostAsJsonAsync("http://localhost:59305/api/User/CheckUserExist", search);
if (response.IsSuccessStatusCode)
{
bool exists = await response.Content.ReadAsAsync<bool>();
// Handle what to do with exists
return RedirectToAction("Index");
}
else
{
// Handle unsuccessful call
throw new Exception("Application error");
}
}
}
You can't use string to accept the post data, please define a struct class or use dynamic to receive the Json string.
I'm facing a situation where I've to read the form data from incoming request in ASP.NET Web API twice (from model binder and filter). I've tried using LoadIntoBufferAsync but no luck.
// from model binder
Request.Content.LoadIntoBufferAsync().Wait();
var formData = Request.Content.ReadAsFormDataAsync().Result;
// from filter
var formData = Request.Content.ReadAsFormDataAsync().Result;
The problem is that the underlying buffer for content is a forward-only stream that can only be read once.
Why do you need to read it twice? A little more context would help. Is it that you are reading from two separate filters?
EDIT: might try reading directly from MS_HttpContext and using that as your content stream (don't think this works in a self hosted environment).
using (var s = new System.IO.MemoryStream()) {
var ctx = (HttpContextBase)actionContext.Request.Properties["MS_HttpContext"];
ctx.Request.InputStream.Seek(0, System.IO.SeekOrigin.Begin);
ctx.Request.InputStream.CopyTo(s); var body =
System.Text.Encoding.UTF8.GetString(s.ToArray());
}
During the development of a REST API, we had a need to authenticate a request prior to allowing the response to be processed within the controller, and so this created a need to be able to read the header as well as the form (if any) to determine if the credentials were passed into the request within the body of the form rather than through the request header.
A few lines of code reset the stream pointer to the beginning of the stream so that MVC would be able to read the form and populate the view model in the controller
public class WebServiceAuthenticationAttribute : AuthorizationFilterAttribute
{
public override void OnAuthorization(HttpActionContext actionContext)
{
var authenticationHeaderValue = actionContext.Request.Headers.Authorization;
try
{
if (authenticationHeaderValue != null)
{
var webRequestInfo = new WebRequestInfo(actionContext.Request.Method, actionContext.Request.RequestUri);
this.AuthenticationHeaderService.LogOnUsingAuthenticationHeader(authenticationHeaderValue, webRequestInfo);
}
else if (actionContext.Request.Content.IsFormData())
{
Task<NameValueCollection> formVals = actionContext.Request.Content.ReadAsFormDataAsync();
this.AuthenticationFormService.LogOnUsingFormsAuthentication(formVals.Result);
// reset the underlying stream to the beginning so that others may use it in the future...
using (var s = new System.IO.MemoryStream())
{
var ctx = (HttpContextBase) actionContext.Request.Properties["MS_HttpContext"];
ctx.Request.InputStream.Seek(0, System.IO.SeekOrigin.Begin);
}
}
}
catch (Exception)
{
throw;
}
}
}
Initially the data model was not being created by MVC and a null was passed into the controller method. After resetting the stream, MVC was able to read the form, create and populate the data model, and pass it into the controller method.
[WebServiceAuthentication]
public HttpResponseMessage Get(DocumentRequestModel requestForm)
{
var response = CreateResponse(HttpStatusCode.OK);
response.Content = new ByteArrayContent(this.documentService.GetDocument(requestForm.DocumentId.ToString()));
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
return response;
}
You really should not need to do that. At the end of the day, HttpContext Stream points to the same stream Web API reads from.
You can try to put LoadIntoBufferAsync in both places as one could trigger before the other and it was already in the buffer, calling LoadIntoBufferAsync has no side effect.
// from model binder
Request.Content.LoadIntoBufferAsync().Wait();
var formData = Request.Content.ReadAsFormDataAsync().Result;
// from filter
Request.Content.LoadIntoBufferAsync().Wait();
var formData = Request.Content.ReadAsFormDataAsync().Result;