2017-01-13 7 views
1

これをバグとして送信する前に、これをバグとみなすべきかどうかを確認するコミュニティのフィードバックを得たいと思っていました。 AttachmentBaseから派生LinkedResourceまたは他のクラスを作成する場合.Net Frameworkバグ:System.Net.Mail.AttachmentBaseは作成しなかったストリームを破棄します

file nameまたはStreamのいずれかを取るコンストラクタのオーバーロードがあります。ファイル名を指定すると、AttachmentBaseは内部でFileStreamを作成し、MimePart.SetContent(Stream)を使用してプライベートMimePart partのコンテンツを割り当てます。 Streamを取るコンストラクタを使用してAttachmentBaseを作成するときは、基になるコンテンツを直接Streamに割り当てます。

AttachmentBasedisposedの場合は、MimePartが内部処理され、Streamの内容が内部処理されます。 Streamを取っているコンストラクタを使用した場合、このMimePartクラスはちょうどStreamを処理しませんでした。このバグは副作用です。クラス/メソッドが所有していないオブジェクトを破棄すると、そのオブジェクトを後で再利用することは不可能になります。使い捨てオブジェクトを作成する場合、そのオブジェクトはガベージコレクションの前に破棄できる必要があります。これにより、FileStreamsによってキャプチャされたファイルハンドルのようなリソースを解放することができます。

多くのメールを送信する必要があり、各メールに1つの画像があるとします。イメージは非常に大きく、すべての電子メールはこの同じ正確なイメージを使用します。このファイルを読み取ってMemoryStreamに追加し、を各メールのLinkedResourceに再利用できるはずです。これにより、ディスクの読み込みやメモリの使用量が少なくて済みます。 Dispose()は、メッセージが送信されると、各メッセージとそのリソースで呼び出され、ハンドルと管理されていないメモリを解放します。 (Dispose()というコードは、常にオブジェクトを作成したのと同じコードである必要があります)。Dispose()メソッドが正しく実装されていないため、このメソッドは正しく実装されていません。下にあるStreamを作成していないイメージに置いただけです。後続のメッセージがこのStreamを使用しようとすると、ObjectDisposedExceptionがスローされます。

このバグの一時的な回避策は、メッセージごとにMemoryStreamをコピーし、コピーしたStreamsを使用することです。これにより、ディスクの読み込みはほとんど行われませんが、大きな画像がメモリに一度にコピーされます。

正しいパターンと慣行に従い、Streamを作成したコードを廃棄して一度だけ使用すると仮定しないようにすることで、これはすべて回避できました。ここにこのバグの修正があります。コードは、同様のメソッドを置き換える必要があります。これにより、MimePartの下位互換性が確保されます。 AttachmentBaseへの変更は、Streamに渡されるコードの変更点である可能性があります。この問題を解決するには、自分自身を適切に廃棄して渡すStreamを処理することです。

public abstract class AttachmentBase : IDisposable 
{ 
    MimePart part; 

    protected AttachmentBase(Stream contentStream) 
    { 
     part.SetContent(contentStream, true); 
    } 

    protected AttachmentBase(Stream contentStream, string mediaType) 
    { 
     part.SetContent(contentStream, null, mediaType, true); 
    } 

    internal AttachmentBase(Stream contentStream, string name, string mediaType) 
    { 
     part.SetContent(contentStream, name, mediaType, true); 
    } 

    protected AttachmentBase(Stream contentStream, ContentType contentType) 
    { 
     part.SetContent(contentStream, contentType, true); 
    } 
} 

internal class MimePart : MimeBasePart, IDisposable 
{ 
    private bool _keepOpen; 

    internal void SetContent(Stream stream, bool keepOpen = false) 
    { 
     if (stream == null) 
     { 
      throw new ArgumentNullException("stream"); 
     } 

     if (streamSet && !_keepOpen) 
     { 
      this.stream.Dispose(); 
     } 

     this.stream = stream; 
     streamSet = true; 
     streamUsedOnce = false; 
     TransferEncoding = TransferEncoding.Base64; 
     _keepOpen = keepOpen; 
    } 

    internal void SetContent(Stream stream, string name, string mimeType, bool keepOpen = false) 
    { 
     if (stream == null) 
     { 
      throw new ArgumentNullException("stream"); 
     } 

     if (!string.IsNullOrEmpty(mimeType)) 
     { 
      contentType = new ContentType(mimeType); 
     } 

     if (!string.IsNullOrEmpty(name)) 
     { 
      ContentType.Name = name; 
     } 

     SetContent(stream, keepOpen); 
    } 

    internal void SetContent(Stream stream, ContentType contentType, bool keepOpen = false) 
    { 
     if (stream == null) 
     { 
      throw new ArgumentNullException("stream"); 
     } 

     this.contentType = contentType; 
     SetContent(stream, keepOpen); 
    } 

    public void Dispose() 
    { 
     if (stream != null && !_keepOpen) 
     { 
      stream.Dispose(); 
     } 
    } 
} 

答えて

2

私はKeepOpenオプションは、StreamReaderをが何に似て、このAPIでサポートされている何か、でなければならないことに同意だと思います。しかし、提案された実装では、コンシューマがオブジェクトを作成する場所に戻って、変化が起こらないようにする必要があります。

この問題を回避するもう1つの方法は、終了する呼び出しを無視し、終了時にストリームを閉じる別の方法を持つラッピングストリームを作成することです。例えば

private class UncloseableStreamWrapper : Stream 
{ 
    private readonly Stream _baseStream; 
    public UncloseableStreamWrapper(Stream baseStream) 
    { 
     _baseStream = baseStream; 
    } 

    public override void Close() 
    { 
     // DO NOTHING HERE 
    } 

    public void CloseWrappedStream() 
    { 
     _baseStream.Close(); 
    } 
    public override long Position 
    { 
     get 
     { 
      return _baseStream.Position; 
     } 
     set 
     { 
      _baseStream.Position = value; 
     } 
    } 

    public override bool CanRead => _baseStream.CanRead; 
    public override bool CanSeek => _baseStream.CanSeek; 
    public override bool CanWrite => _baseStream.CanWrite; 
    public override long Length => _baseStream.Length; 
    public override void Flush() => _baseStream.Flush(); 
    public override int Read(byte[] buffer, int offset, int count)=> _baseStream.Read(buffer, offset, count); 
    public override long Seek(long offset, SeekOrigin origin)=>_baseStream.Seek(offset, origin); 
    public override void SetLength(long value) => _baseStream.SetLength(value); 
    public override void Write(byte[] buffer, int offset, int count) => _baseStream.Write(buffer, offset, count); 
} 

は、その後、あなたのループで、あなただけの位置をリセットして行くことができます。ここ

var stream = new UncloseableStreamWrapper(System.IO.File.OpenRead(@"Z:\temp\temp.png")); 
var lr = new LinkedResource(stream); 
lr.Dispose(); 

stream.Position = 0; 

lr = new LinkedResource(stream); 
lr.Dispose(); 

stream.CloseWrappedStream(); 
Console.ReadLine(); 

一つの大きな注意点は、あなたが完全にあなたのコードを壊す将来のアップデートで変更される可能性が基礎となるMimePartの実装にあなたのコードを抱き合わせているということです。

+0

1つのストリームを再利用できるため、回避策が大好きです。あなたが言ったように、それは 'Dispose()'の代わりに 'Close()'を呼び出すための基礎となるコードの実装に依存しています。ストリームを破棄する場合は、何もしないようにラッパーで 'Dispose()'をオーバーライドしなければなりません。これは 'using'ステートメントを中断させます。ストリームソースコードのかなりの部分を見た後、MSは他のdispose呼び出しからのみ 'Dispose()'を使い、他のものは 'Close()'を使います。私は、将来の変更があなたの回避策を破る可能性はほとんどないと主張するのは大体は安全だと思います。 – kkirkfield

関連する問題