3

I have a method that take Stream parameter and pass it to server

public async Task<string> Execute(Stream archive)
    {
        archive.Seek(0, SeekOrigin.Begin);
        using var content = new MultipartFormDataContent();
        content.Add(new StreamContent(archive), "file1", "file1");
        var result = "";
        using (var response = await _client.PostAsync(_uri, content))
        {
            if (response.IsSuccessStatusCode)
            {
                var stringResult = await response.Content.ReadAsStringAsync();
                result = stringResult;
            }
        }
        // here archive is already disposed
        return result;
    }

Now I implement the retry policy of this method. If outside code calling this method gets "" as result, then it tries to call this method againg. But the archive is disposed to that moment. I see that archive stream is disposed immediately after disposing of response. Why? What should I do if I need stream parameter outside after this method?

vitm
  • 473
  • 1
  • 3
  • 12

3 Answers3

5

It's the StreamContent that will dispose the Stream, as states in it's source. And that will be disposed by the MultipartContent. And that will be disposed in PostAsync... all though the chain.

A solution is to subclass the Stream and remove the dispose method, like proposed here. but you'll have to make sure the original stream gets disposed yourself!. Edit: update. Stream is an abstract class, so it would be easier if you know the specific stream type, e.g.

public sealed class NoDisposeMemoryStream : MemoryStream
{
    protected override void Dispose(bool disposing) { }
}

Else you will need to write your own complete Stream wrapper, see bottom.

Another solution is to implement the retry mechanism in the innermost using block, resetting the archive seek origin every fail. That's likely safer.


    public sealed class NoDisposeStream : Stream
    {
        private Stream _original;
        public NoDisposeStream(Stream original) => _original = original
            ?? throw new ArgumentNullException(nameof(original));
        public override bool CanRead => _original.CanRead;
        public override bool CanSeek => _original.CanSeek;
        public override bool CanWrite => _original.CanWrite;
        public override long Length => _original.Length;
        public override long Position { get => _original.Position; set { _original.Position = value; } }
        public override void Flush() => _original.Flush();
        public override int Read(byte[] buffer, int offset, int count) => _original.Read(buffer, offset, count);
        public override long Seek(long offset, SeekOrigin origin) => _original.Seek(offset, origin);
        public override void SetLength(long value) => _original.SetLength(value);
        public override void Write(byte[] buffer, int offset, int count) => _original.Write(buffer, offset, count);
        protected override void Dispose(bool disposing) { }
    }
JHBonarius
  • 10,824
  • 3
  • 22
  • 41
  • You need a _wrapper_ around original stream (and wrapper has dispose method empty), not just subclass `Stream` class. – Evk Oct 18 '21 at 11:54
  • @Evk the `StreamContent` constructor used expects a `Stream`. So the wrapped would have to be derived from `Stream` anyway, not? I'm not fully understanding what you mean. – JHBonarius Oct 18 '21 at 11:57
  • Yes but I mean the code as you wrote it won't compile (Stream is abstract class) and in general it does not describe the idea correctly, I think. You need to pass original stream in the constructor of `NoDisposeStream` and then implement all methods of `Stream` class, delegating to that original stream (`public override bool CanRead => _original.CanRead` etc). Linked answer subclasses specific `MemoryStream` and it's not the same here. – Evk Oct 18 '21 at 11:59
  • @Evk oh dear. yeah. I didn't actually try and used the answer I linked as reference. That makes it quite ugly. But I'll make one. one moment. – JHBonarius Oct 18 '21 at 12:02
1

This happens because HttpClient PostAsync Disposes of Content that you pass. https://github.com/microsoft/referencesource/blob/master/System/net/System/Net/Http/HttpClient.cs

The relevant code:

public Task<HttpResponseMessage> PostAsync(Uri requestUri, HttpContent content,
                CancellationToken cancellationToken)
{
    HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri);
    request.Content = content;
    return SendAsync(request, cancellationToken);
}

And then SendAsync calls DisposeRequestContent which is implemented like:

private void DisposeRequestContent(HttpRequestMessage request)
{
   Contract.Requires(request != null);
    
   // When a request completes, HttpClient disposes the request content so the user  doesn't have to. This also
   // ensures that a HttpContent object is only sent once using HttpClient (similar to   HttpRequestMessages
   // that can also be sent only once).
   HttpContent content = request.Content;
   if (content != null)
   {
     content.Dispose();
   }
}

The comments say why

Serve Laurijssen
  • 9,266
  • 5
  • 45
  • 98
1

Please, take a look to my answer here. I think it's closely related to this scenario.

https://stackoverflow.com/a/75668203/1025407

SuperJMN
  • 13,110
  • 16
  • 86
  • 185