Linas
Linas

Reputation: 602

Write stream directly to HttpClient

Example flow:

  1. Load file from file system
  2. Modify file by embedding custom id
  3. Send modified file to external system with http.

Current implementation:

public async Task EmbedIdAndSendAsync(string filePath, string id, HttpClient httpClient)
{
    using (Stream stream = File.OpenRead(filePath))
    using (PdfDocument pdf = new PdfDocument(stream))
    using (MemoryStream modifiedStream = new MemoryStream())
    {
        pdf.EmbedId(id);

        pdf.WriteTo(modifiedStream);

        modifiedStream.Position = 0;

        await httpClient.PostAsync(
            "files/upload",
            new MultipartFormDataContent {
                { new StringContent(id), "file[id]" },
                { new StreamContent(modifiedStream), "file[stream]", Path.GetFileName(filePath) }
            }
        );
    }
}

How would I write file modifications directly to HttpClient's network stream without having to use an intermediary memory stream?

Upvotes: 3

Views: 89

Answers (2)

Jeremy
Jeremy

Reputation: 46420

I think you can also use the System.Net.Http.PushContentStream class.

An instance of PushContentStream can be added to the httpClient request body or to a multipart data content. Basically what happens is you pass a function to an instance of PushContentStream, and that function will be executed at the time the reqeuest is made, using the outbound http stream.

I think it would be something like (untested - I cobbled this together from old project code):

public async Task EmbedIdAndSendAsync(string filePath, string id, HttpClient httpClient)
{
    var modifiedPdfContent = new PushStreamContent((outputStream, content, context) =>
    {
        using (Stream fileStream = File.OpenRead(filePath))
        using (PdfDocument pdf = new PdfDocument(fileStream))

        pdf.EmbedId(id);

        pdf.WriteTo(outputStream);
        outputStream.Close();
    });
    modifiedPdfContent.Headers.ContentType.MediaType = new MediaTypeHeaderValue("application/pdf");


    await httpClient.PostAsync("files/upload",
        new MultipartFormDataContent {
            { new StringContent(id), "file[id]" },
            { modifiedPdfContent, "file[stream]", Path.GetFileName(filePath) }
        });
    
}

As a basic example:

The following console app works in .net 9, when adding the Microsoft.AspNet.WebApi.Client nuget package:

using System.Net.Http;
using System.Net.Http.Headers;

HttpClient c = new HttpClient(new HttpClientHandler()
{
    UseDefaultCredentials = true
});
HttpRequestMessage msg = new HttpRequestMessage(HttpMethod.Post, "http://test.com");


msg.Content = new PushStreamContent((stream, content, context) =>
{
    using (StreamWriter sw = new StreamWriter(stream))
        sw.WriteLine("This is a test");
    stream.Close();
});
msg.Content.Headers.ContentType = new MediaTypeHeaderValue("text/pain");

HttpResponseMessage response = c.SendAsync(msg).Result;

Upvotes: 0

norekhov
norekhov

Reputation: 4318

The answer is you have to define your own HttpContent.

In this way you kinda invert the task and HttpClient will call your code when needed and only if needed (because maybe there's no network connection or any other error)

sealed class PdfContent : HttpContent
{
    string filePath;
    string id;

    public PdfContent(string filePath, string id)
    {
        this.filePath = filePath;
        this.id = id;
        // Put something here to define content or just do it in the outer code
        Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
    }

    protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken)
    {
        // Here you can write to a stream directly
        using Stream fileStream = File.OpenRead(filePath);
        using PdfDocument pdf = new PdfDocument(fileStream);
        pdf.EmbedId(id);
        // Use async + cancellationToken if there's one
        pdf.WriteTo(stream);
        return Task.CompletedTask;
    }

    // This is just a technical compatibility override to support cancellation
    protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context)
    {
        return SerializeToStreamAsync(stream, context, CancellationToken.None);
    }

    protected override bool TryComputeLength(out long length)
    {
        // Return something if you know length or zero if not
        length = 0;
        return false;
    }
}

Now just post it.

    await httpClient.PostAsync(
        "files/upload",
        new MultipartFormDataContent {
            { new StringContent(id), "file[id]" },
            { new PdfContent(filePath, id), "file[stream]" }
        }
    );

Upvotes: 2

Related Questions