PushStreamContent Web API - c#

I am trying to push my data from web API to client, so I use PushStreamContent in Web API.
My data are all student exist in database.
public HttpResponseMessage GetAll()
{
HttpResponseMessage response = new HttpResponseMessage();
var students = _symboleService.GetAll();
response.Content = new PushStreamContent((students, httpContent, context) =>
{
OnStreamAvailable(students, httpContent, context);
}, "text/plain");
return response;
}
private void OnStreamAvailable(Stream stream, HttpContent content,TransportContext context)
{
StreamWriter responseStreamWriter = new StreamWriter(stream);
clients.Add(responseStreamWriter);
}
But an error shows on "students" on this line:
response.Content = new PushStreamContent((students, httpContent, context) =>
A local or parameter named 'students' cannot be declared in this scope
because that name is used in an enclosing local scope to define a
local or parameter
Update My point here is to push the data to the client side,
After some modification, this my code but it shows an error on line,
var students = _symboleService.GetAll();
An object reference is required for the non-static field, method, or
property 'SymbolesController._symboleService'
private static readonly Lazy<Timer> _timer = new Lazy<Timer>(() => new Timer(TimerCallback, null, 0, 1000));
private static readonly ConcurrentDictionary<StreamWriter, StreamWriter> _outputs = new ConcurrentDictionary<StreamWriter, StreamWriter>();
public HttpResponseMessage GetAll()
{
HttpResponseMessage response = new HttpResponseMessage();
response.Content = new PushStreamContent((responseStream, httpContent, context) =>
{
StreamWriter responseStreamWriter = new StreamWriter(responseStream);
_outputs.TryAdd(responseStreamWriter, responseStreamWriter);
}, "text/plain");
Timer t = _timer.Value;
return response;
}
// Runs every second after the first request to this controller and
// writes to the response streams of all currently active requests
private static void TimerCallback(object state)
{
foreach (var kvp in _outputs.ToArray())
{
StreamWriter responseStreamWriter = kvp.Value;
try
{
var students = _symboleService.GetAll();
responseStreamWriter.Write(students);
responseStreamWriter.Flush();
}
catch { }
}
}
By the way, if I change studentsby DateTime.Now here, it works:
responseStreamWriter.Write(students); to
responseStreamWriter.Write(DateTime.Now);

You need to rename the students parameter used in PushStreamContent constructor.
For example, from:
response.Content = new PushStreamContent((students, httpContent, context) =>
{
OnStreamAvailable(students, httpContent, context);
}, "text/plain");
to:
response.Content = new PushStreamContent((stream, httpContent, context) =>
{
OnStreamAvailable(stream, httpContent, context);
}, "text/plain");
The reason why you cannot use students is because it's already declared as a variable in the previous line:
var students = _symboleService.GetAll();

There are a couple of issues here that are going to impede your progress.
To start with, the Action in your PushStreamContent constructor cannot reuse the variable name "students". It appears that you think you're passing the reference to the Action, but that's not how this works.
You could try this:
response.Content = new PushStreamContent(OnStreamAvailable, "text/plain");
But there remains another problem. The stream in the PushStreamContent constructor's action is an output stream.
public PushStreamContent(Action<Stream, HttpContent, TransportContext> onStreamAvailable);
You'll want to write to that stream from within your OnStreamAvailable handler.
Since it's not clear what the _symbolService.GetAll() method, I can only speculate that a Byte[] generated from the method should be written to your output stream within your action itself.
Assuming that your student data is not really a stream or inherently streamable, this may be a misuse of the PushStreamContent class.

Related

Why do I get "invalid_form_data" error when trying to post to Slack-API?

When I use this method:
public async Task<HttpResponseMessage> UploadFileAsync(MultipartFormDataContent requestContent)
{
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, UriMethod);
request.Content = requestContent;
var response = await _httpClient.SendAsync(request);
return response;
}
I allways get the answer:
{"ok":false,"error":"invalid_form_data"}
so I tried to explicitly tell it the 'mediaType', I tried "application/json" and others, but with all of them I get the same error. Here is the full Main-method that calls the upper method:
namespace TestArea
{
class MainArea
{
public static void Main( string[] args)
{
try
{
Task.WaitAll(SendMessage());
}
catch(Exception ex)
{
Console.WriteLine(ex);
Console.ReadKey();
}
}
private static async Task SendMessage()
{
var client = new BpsHttpClient("https://slack.com/api/chat.postMessage");
JsonObject JO = new JsonObject();
JO.channel = "DCW21NBHD";
JO.text = "This is so much fun :D !";
var Json = JsonConvert.SerializeObject(JO, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
var StringJson = new StringContent(Json, Encoding.UTF8, "application/json");
var requestContent = new MultipartFormDataContent();
requestContent.Add(StringJson);
var Response = await client.UploadFileAsync(requestContent);
string AnswerContent = await Response.Content.ReadAsStringAsync();
}
When I use this method:
public async Task<HttpResponseMessage> SendMessageAsync(FormUrlEncodedContent content)
{
var response = await _httpClient.PostAsync(UriMethod, content);
return response;
}
so basically I am passing "FormUrlEncodedContent" instead of "MultipartFormDataContent" in this, and then I get the response I want and can work wiht it. BUT this is of little use to me since I have to use "MultipartFormDataContent" to be able to send files with my requests.
Anyone have an idea what is failing here? Why does it not like the one content-type but the other one? I'd be gratefull for tipps and ideas!
You get the error "invalid_form_data", because the API method chat.postMessage does not support requests with multipart/form-data.
As you can see from the documentation under "Accepted content types" this method only accepts: application/x-www-form-urlencoded, application/json
Note that you can not upload files to chat.postMessage.
If you want to upload files, please use the API method files.upload, which also supports multipart/form-data.
See also my answer here for how to upload files with a comment.

Read a Response in ActionFilter then DelegatingHandlers

Should we always read from HttpContent as a stream and then reset? We were reading twice from the response and it wasn't working [1].
We were trying to log an HTTP response with an ActionFilterAttribute and then update the response in a DelegatingHandler. The changes made in the DelegatingHandler were lost: the reponse that arrived back at the client was unaltered.
public sealed class LoggingFilterAttribute : ActionFilterAttribute
{
public override async void OnActionExecuted(HttpActionExecutedContext context)
{
string logMessage = null;
context.Response?.Content?
.ReadAsStringAsync()
.ContinueWith(task => logMessage = task.Result);
...
}
}
internal class AddVersionsHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var response = await base.SendAsync(request, cancellationToken);
var responseContent =
await response.Content
.ReadAsAsync<IOurModels>(cancellationToken)
.ConfigureAwait(false);
responseContent.Versions = this.Versions;
return response;
}
}
Changing the read in the ActionFilterAttribute to read from the stream and then reset the position works. The HttpContent is assumed to be a stream that must be reset on each read.
public override async void OnActionExecuted(HttpActionExecutedContext context)
{
string logMessage = null;
if (context.ActionContext?.Response?.Content != null)
{
var memoryStream = new MemoryStream();
await context.ActionContext.Response.Content.CopyToAsync(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);
var sr = new StreamReader(memoryStream);
logMessage = sr.ReadToEnd();
}
...
}
Do we need to ensure that everywhere the HttpContent is read we need to read from the stream and then reset?
Chaining these DelegatingHandlers into the pipeline that each alter the reponse works just fine. It seems that ReadAsAsync<T> provides access to the underlying object in the stream without changing the read position. So if you know the object type you intend to read then this is an option.
If you want to read or alter the contents of the response and you know the type of the object you can use ReadAsAsync<T>(). These can be chained in the pipeline if required.
var response = await context.ActionContext.Response.Content.ReadAsAsync<IOurModel>();
If you are logging you can then serialize the above response to XML or JSON .
If you want to read the stream as a string you can only do that once with ReadAsStringAsync(). Later updates in the pipeline will be lost.
string logMessage = null;
context.Response?.Content?
.ReadAsStringAsync()
.ContinueWith(task => logMessage = task.Result);
If you want to read the stream and later do anything else at all to it you must reset the position.
var memoryStream = new MemoryStream();
await context.ActionContext.Response.Content.CopyToAsync(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);
var sr = new StreamReader(memoryStream);
var logMessage = sr.ReadToEnd();

HttpClient did not act as expected

My C# app uploads file to some API, I'm using multipart request, i.e I'm uploading a json string and binary contect of the file, it works fine for most files, but for very few it does nothing, I mean let's try for file named file.pdf:
My code looks roughly as follows:
public async Task<Dictionary<string , string>> Upload(string filePath)
{
FileInfo fi = new FileInfo(FilePath);
string jsonString="some json string";
byte[] fileContents=File.ReadAllBytes(fi.FullName);
Uri webService = new Uri(url);
HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post , webService);
requestMessage.Method = HttpMethod.Post;
requestMessage.Headers.Add("Authorization" , "MyKey1234");
const string boundry = "------------------My-Boundary";
MultipartFormDataContent multiPartContent = new MultipartFormDataContent(boundry);
ByteArrayContent byteArrayContent = new ByteArrayContent(fileContents);
multiPartContent.Add(byteArrayContent);
requestMessage.Content = multiPartContent;
HttpClient httpClient = new HttpClient();
Console.WriteLine("before");
HttpResponseMessage httpResponse = await httpClient.SendAsync(requestMessage , HttpCompletionOption.ResponseContentRead , CancellationToken.None);
Console.WriteLine("after");
}
The caller:
myDictionary = await Upload(filePath);
Output:
before
Press any key to continue . . .
I mean there is no exception, nothing, what is this? a bug?
Edit
The structure of the console app is as follows:
class Program
{
static void Main(string[] args)
{
new MyClass().Start();
}
}
And inside MyClass:
public async void Start()
{
myDictionary = await Upload(filePath);
}
As explained in the comment section, your main method does not await the awaitable call and will not wait for the HttpClient instance to process the response.
If the console app is for testing purposes, you can call the .Result property on the task instance returned by the method, like this:
new MyClass().Start().Result;
However, it would be best to use the async keyword on the main method that has been made available in C# 7.1, like this:
class Program
{
static async Task Main(string[] args)
{
await new MyClass().Start();
}
}
Finally, as recommended, you should add the suffix 'Async' to your async method names. For instance, Start would be named StartAsync.

Inspect DefaultHttpContext body in unit test situation

I'm trying to use the DefaultHttpContext object to unit test my exception handling middleware.
My test method looks like this:
[Fact]
public async Task Invoke_ProductionNonSuredException_ReturnsProductionRequestError()
{
var logger = new Mock<ILogger<ExceptionHandlerMiddleware>>();
var middleWare = new ExceptionHandlerMiddleware(next: async (innerHttpContext) =>
{
await Task.Run(() =>
{
throw new Exception();
});
}, logger: logger.Object);
var mockEnv = new Mock<IHostingEnvironment>();
mockEnv.Setup(u => u.EnvironmentName).Returns("Production");
var context = new DefaultHttpContext();
await middleWare.Invoke(context, mockEnv.Object);
var reader = new StreamReader(context.Response.Body);
var streamText = reader.ReadToEnd();
//TODO: write assert that checks streamtext is the expected production return type and not the verbose development environment version.
}
In my middleware, I am writing to the context like this:
public static Task WriteResponse(HttpContext context, HttpStatusCode statusCode, object responseData, Formatting jsonFormatting)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)statusCode;
return context.Response.WriteAsync(JsonConvert.SerializeObject(responseData, jsonFormatting));
}
To give you more insight into the middleware approach I've taken, I'm taking the approach found in this answer here.
Works fine when the app is running it's normal pipeline. However, using the DefaultHttpContext method in the test, the response body always comes back empty, and ContentLength is null. Thus, my streamText variable in the test is an empty string.
Is it possible to inspect what the middleware is writing to the context in this situation? Is this the appropriate way, or is there a better way.
Consider setting the body yourself so that you have control of the stream
Comment provided by #AndrewStanton-Nurse
The Response.Body stream in DefaultHttpContext is Stream.Null, which is a stream that ignores all reads/writes. You need to set the Stream yourself before calling the method.
Further - This will now allow the body to be set, but in order to read it correctly we must set the pointer to the beginning as per this answer, before using the StreamReader.
//...code removed for brevity
var context = new DefaultHttpContext();
context.Response.Body = new MemoryStream();
await middleWare.Invoke(context, mockEnv.Object);
context.Response.Body.Seek(0, SeekOrigin.Begin);
var reader = new StreamReader(context.Response.Body);
var streamText = reader.ReadToEnd();
//...code removed for brevity

Is it possible to read results of the XmlSerializer being sent by HTTPClient?

I am using an extension method to post xml using the HTTPClient which works great.
My question: Is it possible to read, log, or display the results of the XmlSerializer data that is being sent/posted ?
public static class HttpExtensions {
public static Task<HttpResponseMessage> PostAsXmlWithSerializerAsync<T>(this HttpClient client, string requestUri, T value)
{
return client.PostAsync(requestUri
, value
, new XmlMediaTypeFormatter { UseXmlSerializer = true }
);
}
}
PostAsync hides the actually sent HttpRequestMessage from you, though you can retrieve it from the response, too so you can trace both the request and response contents:
var response = await client.PostAsync(uri, value, formatter);
Log(response);
If you really want to log the request only, create the request manually:
var request = new HttpRequestMessage(HttpMethod.Post, uri);
request.Content = new StreamContent(myXmlStream);
Log(request);
var response = await client.SendAsync(request);
Log(response);
And now you can create one or two Log overloads. I show it for the response, which includes both the request and response log. This is independent from the format, works for both XML and json content.
protected virtual void Log(HttpResponseMessage response)
{
// Use any log/trace engine here, this example uses Debug
Debug.WriteLine($"Response of the API Call [{response.RequestMessage.Method}] {response.RequestMessage.RequestUri}: {response.StatusCode} {FormatResponse(response)}");
}
private static string FormatResponse(HttpResponseMessage response)
{
var result = new StringBuilder();
result.AppendLine();
result.AppendLine("Original request:");
result.AppendLine(FormatHttpMessage(response.RequestMessage.Headers, response.RequestMessage.Content));
result.AppendLine();
result.AppendLine("Obtained response:");
result.AppendLine(FormatHttpMessage(response.Headers, response.Content));
}
private static string FormatHttpMessage(HttpHeaders headers, HttpContent content)
{
var result = new StringBuilder();
var headersString = headers.ToString();
if (!string.IsNullOrWhiteSpace(headersString))
{
result.AppendLine("Headers:");
result.AppendLine(headersString);
result.AppendLine();
}
if (content != null)
{
result.AppendLine("Content:");
result.AppendLine(content.ReadAsStringAsync().Result);
}
return result.ToString();
}
Yes, you can.
Download and install Fiddler , then filter your requestUri , you can monitor all transferring data such you serialized xml.

Categories

Resources