How to stream a HTTP response while writing to the same stream? - c#

I have some the following function that formats some data into csv:
public static void WriteToStream(IEnumerable<MyClass> data, Stream stream)
{
var writer = new StreamWriter(stream);
foreach(var record in data)
{
var line = // code that generates the csv line.
writer.WriteLine(line);
}
}
In my REST controller method, I'm trying to send this stream as a response. I'm trying the following:
[HttpGet]
public async Task<IActionResult> GetCsvStream()
{
var data = // code to get raw data.
var stream = new MemoryStream();
WriteToStream(data, stream);
return new FileStreamResult(stream, "text/csv");
}
However, I'm getting Internal Server Error with this code. What I would like to achieve is to continuously stream data as csv.
EDIT:
By the way, I'm currently doing it as below as John suggested. But, does this actually stream data or does it write all the content to response body first and then sends the final response? My understanding is it's the latter.
[HttpGet]
public async Task<IActionResult> GetCsvStream()
{
var data = // code to get raw data.
WriteToStream(data, this.HttpContext.Response.Body);
return this.Ok();
}

Related

In ASP.NET 4.8, how can I deserilize a file containing JSON, uploaded as POST multipart/form-data request to an object in memory (no file on server)?

Example code:
I export an object as JSON in a file
[HttpGet]
public IHttpActionResult ExportObjectToFile()
{
var exampleObject = new ExampleObject();
var response = Request.CreateResponse(HttpStatusCode.OK, exampleObject, JsonMediaTypeFormatter.DefaultMediaType);
response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue(DispositionTypeNames.Attachment)
{
FileName = exampleObject.FileName;
};
return ResponseMessage(response);
}
Now I want to import and create a new object using multipart/form-data on Postman to attach the file with the request
[HttpPost]
public async Task<IHttpActionResult> ImportObjectFromFile()
{
var multipartForm = await Request.Content.ReadAsMultipartAsync().ConfigureAwait(false);
var jsonString = await multipartForm.Contents[0].ReadAsString().ConfigureAwait(false);
var importedExampleObject = JsonConvert.DeserializeObject<exampleObject>(jsonString);
return ResponseMessage(importedExampleObject);
}
I know it's considered bad practice to not store the file first locally but this is more of a learning exercise for me, it helps me understand how file uploads work.
So with the above code importedExampleObject is created but all the fields are null, essentially all the data is lost, and there are no exceptions thrown, so I'm at a loss.

c# Return http content with a stream (bytes) in the HttpResponseMessage

I am trying to return a stream (byte array actually) in a http response.
My first approach is
public async HttpResponseMessage GetBytes() {
// get a memory stream with bytes
using (var result = new HttpResponseMessage(HttpStatusCode.OK))
{
result.Content = new StreamContent(stream);
result.Content.Header.ContentType = new MediaTypeHeaderValue("application/octet-stream");
return result;
}
}
However, on the client side (Postman), I didnt see the binary content in the response.content. it only has a content header content-type application/octet-stream, but the length is not correct, basically the real bytes are not there.
Then I switched to this approach.
public async Task<ActionResult> GetBytes() {
// prepare the stream
return new FileContentResult(stream.ToBytes(), MediaTypeHeaderValue("application/octet-stream"));
}
This time, it works, I can get the bytes on the client side. Why HttpResponseMessage does not work? I think if we can use StreamContent then we should be able to get the bytes from the content. What is the logic behind this?
Thanks
HttpResponseMessage contains status information and request data. You must use the Content property in HttpResponseMessage to return the data
public async Task<HttpContent> GetBytes() {
// get a memory stream with bytes
using (var result = new HttpResponseMessage(HttpStatusCode.OK))
{
result.Content = new StreamContent(stream);
result.Content.Header.ContentType = new MediaTypeHeaderValue("application/octet-stream");
return result.Content;
}
}

request.body is empty even though payload is sent across

I have a PUT endpoint which receives a payload and coverts it to an object using [FromBody].
This works fine when running on localhost (using IIS Express).
But when I run it via the production server (IIS), it fails. The validation error is:
{
"": [
"A non-empty request body is required."
]
}
I can recreate this with both my client code and using Postman.
Thinking that maybe the payload was malformed, I used some middleware (posted elsewhere on Stack Overflow) to inspect the body before it reached the endpoint (see code below), but even at that stage the request.body is empty.
I can't see any other places where the request has already been read at this stage (as I appreciate that can clear it).
I've read countless Stack Overflow posts and other web pages, and nothing else seems to come close to this situation (where request.body is empty, even though you can see in the client the payload is sent).
Any help would be greatly appreciated. I'm happy to provide any further details.
For reference, here is the code.
Payload
{"currentlySaved":false,"type":"album"}
End point
// PUT api/<controller>/toggleSaveState
[HttpPut("toggleSaveState/{id}")]
public async Task<IActionResult> Put(string id, [FromBody] ToggleSaveStateRequest requestDetails)
{
...
}
The object representing the payload:
public class ToggleSaveStateRequest
{
public bool CurrentlySaved { get; set; }
public string Type { get; set; }
}
Middleware used to check the request.body
Note: the problem takes place whether I include this middleware or not. I've included it in a bid to diagnose the problem.
public class RequestResponseLoggingMiddleware
{
private readonly RequestDelegate _next;
public RequestResponseLoggingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
//First, get the incoming request
var request = await FormatRequest(context.Request);
Console.WriteLine($"REQUEST: {request}");
//Copy a pointer to the original response body stream
var originalBodyStream = context.Response.Body;
//Create a new memory stream...
using (var responseBody = new MemoryStream())
{
//...and use that for the temporary response body
context.Response.Body = responseBody;
//Continue down the Middleware pipeline, eventually returning to this class
await _next(context);
//Format the response from the server
var response = await FormatResponse(context.Response);
//TODO: Save log to chosen datastore
Console.WriteLine($"RESPONSE: {response}");
//Copy the contents of the new memory stream (which contains the response) to the original stream, which is then returned to the client.
await responseBody.CopyToAsync(originalBodyStream);
}
}
private async Task<string> FormatRequest(HttpRequest request)
{
var body = request.Body;
//This line allows us to set the reader for the request back at the beginning of its stream.
request.EnableRewind();
//We now need to read the request stream. First, we create a new byte[] with the same length as the request stream...
var buffer = new byte[Convert.ToInt32(request.ContentLength)];
//...Then we copy the entire request stream into the new buffer.
await request.Body.ReadAsync(buffer, 0, buffer.Length);
//We convert the byte[] into a string using UTF8 encoding...
var bodyAsText = Encoding.UTF8.GetString(buffer);
//..and finally, assign the read body back to the request body, which is allowed because of EnableRewind()
request.Body = body;
return $"{request.Scheme} {request.Host}{request.Path} {request.QueryString} {bodyAsText}";
}
private async Task<string> FormatResponse(HttpResponse response)
{
//We need to read the response stream from the beginning...
response.Body.Seek(0, SeekOrigin.Begin);
//...and copy it into a string
string text = await new StreamReader(response.Body).ReadToEndAsync();
//We need to reset the reader for the response so that the client can read it.
response.Body.Seek(0, SeekOrigin.Begin);
//Return the string for the response, including the status code (e.g. 200, 404, 401, etc.)
return $"{response.StatusCode}: {text}";
}
}
Edit:
Version of endpoint without the [FromBody]:
The following is here to try and simplify my explanation.
// PUT api/<controller>/toggleSaveState
[HttpPut("toggleSaveState/{id}")]
public async Task<IActionResult> Put(string id)
{
using (var reader = new StreamReader(Request.Body))
{
var body = reader.ReadToEnd();
Console.WriteLine("body", body);
}
...
return Ok();
}
Given the above endpoint (with no middleware in place), when it's run locally, body has the value of:
{"currentlySaved":false,"type":"album"}
However, when it is run remotely, it is empty.
Problem with your code is here:
//Copy a pointer to the original response body stream
var originalBodyStream = context.Response.Body;
You are assigning a reference to the response stream, and then you are overwriting(that stream) it with an empty memory stream here:
//...and use that for the temporary response body
context.Response.Body = responseBody;
So the reference in originalBodyStream variable, is now pointing to that empty stream as well. Other words - at this point the content of context.Response.Body is lost.
If you want to copy the request body stream to the other stream, use below:
await context.Response.Body.CopyToAsync(originalBodyStream);
Remember that streams are reference type so this:
var bodyVar = request.Body;
assigns only a reference to the stream(not the value!) - other words any modifications on the request.Body will be reflected on the bodyVar as it points to the same place in memory.

How to stream Http response with custom response formatter?

I have a REST controller that streams the response in csv format using a helper method like below:
public static void CsvStreamHelper(IEnumerable<T> data, Stream stream)
{
using (var writer = new StreamWriter(stream))
{
foreach (var line in data)
{
// format csv lines here
writer.WriteLine(lineString);
}
writer.Flush();
}
}
Then, I'm using this in my Controller like:
public Task<IActionResult> MyController()
{
var data = // Get data here.
CsvStreamHelper(data, this.HttpContext.Response.Body);
return new EmptyResult();
}
This is working fine. However, I would like to use content negotiation middleware with custom formatter like here while continue to stream response.
I can override WriteResponseBodyAsync method using my helper method. What I'm unsure about is if I use it in my Rest controller like this.Ok(data), instead of streaming the response, it will just build the response and send it in one chunk. How can I achieve streaming response with content negotiation middleware?

Send image with HttpClient to POST WebApi and consume the data

My HttpClient sends the image with PostAsync.
I am not really sure what to do now since this is my first REST Api and I can't really adapt the things I sorted out in other posts yet.
Hopefully you guys can help me out and can give me a direction.
public async Task SendImage(string fullFileName, string fileName)
{
var client = new HttpClient();
client.BaseAddress = new Uri("http://x.x.x.x");
var content = new StringContent(fullFileName, Encoding.UTF8, "image/jpg");
HttpResponseMessage response = await client.PostAsync($"/values/file/{fileName}", content);
}
I have several questions about the POST function.
First of all I can successfully access it with PostMan and the fileName is correct.
How do I read the image data and write it to a file?
[HttpPost("file/{fileName}")]
public void Upload(string fileName)
{
Debug.Write(fileName);
}
EDIT:
I setup my environment and I can now send a post via the internet to my published Web Api.
On my app just nothing happens.
For now I just tried to get something of a message to work on but I dont getting one.
[HttpPost("file/{fileName}")]
public HttpResponseMessage Upload(UploadedFile fileName)
{
Debug.Write(fileName);
if (!ModelState.IsValid)
{
}
if (fileName == null)
{
}
string destinationPath = Path.Combine(#"C:\", fileName.FileFullName);
System.IO.File.WriteAllBytes(destinationPath, fileName.Data);
HttpResponseMessage rm = new HttpResponseMessage();
rm.StatusCode = HttpStatusCode.OK;
return rm;
}
1.Your controller should look like this:
//For .net core 2.1
[HttpPost]
public IActionResult Index(List<IFormFile> files)
{
//Do something with the files here.
return Ok();
}
//For previous versions
[HttpPost]
public IActionResult Index()
{
var files = Request.Form.Files;
//Do something with the files here.
return Ok();
}
2.To upload a file you can also use Multipart content:
public async Task UploadImageAsync(Stream image, string fileName)
{
HttpContent fileStreamContent = new StreamContent(image);
fileStreamContent.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("form-data") { Name = "file", FileName = fileName };
fileStreamContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");
using (var client = new HttpClient())
using (var formData = new MultipartFormDataContent())
{
formData.Add(fileStreamContent);
var response = await client.PostAsync(url, formData);
return response.IsSuccessStatusCode;
}
}
3.If you are uploading large files you should consider streaming the files instead, you can read about it here
You're going fine until you're trying to retrieve the image data, I'm afraid.
According to your question:
How do I read the image data and write it to a file?
All you want to do is getting the file's data and its file name and sending it to your service.
I would personally create an UploadedFile class on both sides (client and service side), having the file's name and its data, so:
public class UploadedFile
{
public string FileFullName { get; set; }
public byte[] Data { get; set; }
public UploadedFile(string filePath)
{
FileFullName = Path.GetFileName(Normalize(filePath));
Data = File.ReadAllBytes(filePath);
}
private string Normalize(string input)
{
return new string(input
.Normalize(System.Text.NormalizationForm.FormD)
.Replace(" ", string.Empty)
.ToCharArray()
.Where(c => CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark)
.ToArray());
}
}
Then you will need, for example, the NewtonSoft's JsonConvert in order to serialize the object and send it through.
So now you would be able to send your data async:
public async Task SendDataAsync(string fullFilePath)
{
if (!File.Exists(fullFilePath))
throw new FileNotFoundException();
var data = JsonConvert.SerializeObject(new UploadedFile(fullFilePath));
using (var client = new WebClient())
{
client.Headers.Add(HttpRequestHeader.ContentType, "application/json");
await client.UploadStringTaskAsync(new Uri("http://localhost:64204/api/upload/"), "POST", data);
}
}
Now, make sure you correctly handle the request on the server side. If for whatever reason the parameters doesn't match it won't enter into the method (remember having the same model/class - UploadedFile on the service as well).
On the service, just change the arguments of your method and perform something "like this":
[HttpPost]
public HttpResponseMessage Upload(UploadedFile file)
{
if (!ModelState.IsValid)
...
if (file == null)
...
string destinationPath = Path.Combine(_whateverPath, file.FileFullName);
File.WriteAllBytes(destinationPath, file.Data);
}
Hope it helped you having an idea about what to do and what you're actually doing wrong. I've exposed something similar based in my experience.
EDIT: I've actually uploaded an example with both sides working: a simple .NET Core console app which retrieves a file and sends it through POST and a basic WebAPI2 service with a simple controller to retrieve the data. Both ready to go and tested working! Download it here.
Enjoy.

Categories

Resources