70

I'm using DataContractJsonSerializer, which likes to output to a Stream. I want to top-and-tail the outputs of the serializer so I was using a StreamWriter to alternately write in the extra bits I needed.

var ser = new DataContractJsonSerializer(typeof (TValue));

using (var stream = new MemoryStream())
{   
    using (var sw = new StreamWriter(stream))
    {
        sw.Write("{");

        foreach (var kvp in keysAndValues)
        {
            sw.Write("'{0}':", kvp.Key);
            ser.WriteObject(stream, kvp.Value);
        }

        sw.Write("}");
    }

    using (var streamReader = new StreamReader(stream))
    {
        return streamReader.ReadToEnd();
    }
}

When I do this I get an ArgumentException "Stream was not readable".

I'm probably doing all sorts wrong here so all answers welcome. Thanks.

BanksySan
  • 27,362
  • 33
  • 117
  • 216
Gaz
  • 4,064
  • 3
  • 32
  • 34

4 Answers4

141

Three things:

  • Don't close the StreamWriter. That will close the MemoryStream. You do need to flush the writer though.
  • Reset the position of the stream before reading.
  • If you're going to write directly to the stream, you need to flush the writer first.

So:

using (var stream = new MemoryStream())
{
    var sw = new StreamWriter(stream);
    sw.Write("{");

    foreach (var kvp in keysAndValues)
    {
        sw.Write("'{0}':", kvp.Key);
        sw.Flush();
        ser.WriteObject(stream, kvp.Value);
    }    
    sw.Write("}");            
    sw.Flush();
    stream.Position = 0;

    using (var streamReader = new StreamReader(stream))
    {
        return streamReader.ReadToEnd();
    }
}

There's another simpler alternative though. All you're doing with the stream when reading is converting it into a string. You can do that more simply:

return Encoding.UTF8.GetString(stream.GetBuffer(), 0, (int) stream.Length);

Unfortunately MemoryStream.Length will throw if the stream has been closed, so you'd probably want to call the StreamWriter constructor that doesn't close the underlying stream, or just don't close the StreamWriter.

I'm concerned by you writing directly to the the stream - what is ser? Is it an XML serializer, or a binary one? If it's binary, your model is somewhat flawed - you shouldn't mix binary and text data without being very careful about it. If it's XML, you may find that you end up with byte-order marks in the middle of your string, which could be problematic.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Thanks Jon. It's a DataContractJsonSerializer. I've edited the code now to show that. Does that make what I'm doing OK? – Gaz Aug 05 '09 at 11:03
  • 2
    didn't think of just grabbing the buffer - nice one – ShuggyCoUk Aug 05 '09 at 11:04
  • Gaz: I suspect that'll be okay, although I'm not entirely sure. You might want to consider what will happen if the XML contains a closing brace though... – Jon Skeet Aug 05 '09 at 11:07
  • There is no XML; it's an object. It looks like the whole thing is flawed anyway because everything the DataContractJsonSerializer writes ends up at the start of the Stream and everything the StreamWriter writes at the end. Presumably the Serializer keeps track of what position in the Stream it's writing to and inserts a bunch of bytes? Either way it looks like I need to convert everything to strings and stick them together with StringBuilder. At least that's the simplest way and time is tight! – Gaz Aug 05 '09 at 11:11
  • 1
    that sounds like you aren't getting the flushing right. did you add all the flush calls in Jon's example? – ShuggyCoUk Aug 05 '09 at 11:36
  • Sorry about the XML mixup. Yes, Shuggy's right - check for the call to Flush after you write out the key. – Jon Skeet Aug 05 '09 at 11:48
  • 3
    Amen for `sw.Flush();`. I was going nuts trying to figure out why the MemoryStream had no data inspite of writing to it! – Karthic Raghupathi Apr 15 '13 at 04:48
  • 3
    This is the only way it worked for me. I could not get the Encoding.UTF8.GetString() method to work. It said that it would not work with a closed stream. – wbinford Jun 12 '13 at 21:07
  • 2
    In .net 4.5 there is an overloaded StreamWriter constructor which configures the StreamWriter to leave the stream open when the writer is disposed. This would allow you to keep the using blocks, and avoid the flush call. https://msdn.microsoft.com/EN-US/library/gg712853(v=VS.110,d=hv.2).aspx – Simon Giles Jun 17 '15 at 17:57
  • Encoding.UTF8.GetString gets an int (Int32) as 3rd parameter but Stream.Length is a long (Int64). I should do a simple value check and cast if the length is small enough, I guess? – hsandt Apr 19 '17 at 23:08
  • OK, I've just read Simon Giles's answer that suggests to use Stream.ToArray() instead of Stream.GetBuffer(), so that the array Length corresponds to the actual number of characters (the stream length is the number of *bytes* in the stream). – hsandt Apr 19 '17 at 23:23
  • @hsandt: No, that's incorrect - `stream.ToArray()` will give you a byte array with the same number of bytes as `stream.Length`. They're equivalent, but `ToArray` is less efficient because it creates a copy. (I've commented on Simon Giles's answer, but he doesn't claim what you've suggested he does.) – Jon Skeet Apr 20 '17 at 05:38
  • @JonSkeet OK I got it now. I misread and thought it was a char vs byte conflict but he just meant that GetBuffer() may contain unused bytes at the end (but it avoids a copy and you only extract the used bytes, so no problem). So to come back to my original question, I can keep using stream.Length and just cast to int? – hsandt Apr 21 '17 at 18:06
  • @hsandt: Yes, that's fine. – Jon Skeet Apr 21 '17 at 19:04
  • Calling `stream.Length` caused an `ObjectDisposedException`. The test project is targeting .NET 4.5. Calling just `GetBuffer()` returned an array with the nearest power of 2 size. Only `ToArray()` worked correctly. – AlexDev Jul 27 '17 at 16:29
  • @AlexDev: With the code I've given, the return statement occurs *within* the `using` statement for `stream`, so it shouldn't be disposed yet. It sounds like you've got somewhat different code from the code I've presented here. – Jon Skeet Jul 27 '17 at 16:37
  • Yes my code is different. The stream is closed (in another library). But you wrote "You can do that after closing the stream, so it doesn't matter if the StreamWriter closes it.", so I assumed it would work. Maybe the behavior changed between frameworks? – AlexDev Jul 27 '17 at 17:22
  • @AlexDev: Ah, yes. I'll edit that when I get a chance. – Jon Skeet Jul 27 '17 at 17:23
  • Not true about the flushing part. from the doc: MemoryStream.Flush does nothing. – Clusty Dec 12 '19 at 21:35
  • @Clusty: I said the `StreamWriter` needs to be flushed. I believe that `StreamWriter` can buffer the data, so it needs to be flushed in order to write the data to the stream. – Jon Skeet Dec 12 '19 at 22:12
8

setting the memory streams position to the beginning might help.

 stream.Position = 0; 

But the core problem is that the StreamWriter is closing your memory stream when it is closed.

Simply flushing that stream where you end the using block for it and only disposing of it fter you have read the data out of the memory stream will solve this for you.

You may also want to consider using a StringWriter instead...

using (var writer = new StringWriter())
{
    using (var sw = new StreamWriter(stream))
    {
        sw.Write("{");

        foreach (var kvp in keysAndValues)
        {
            sw.Write("'{0}':", kvp.Key);
            ser.WriteObject(writer, kvp.Value);
        }
        sw.Write("}");
    }

    return writer.ToString();
}

This would require your serialization WriteObject call can accept a TextWriter instead of a Stream.

ShuggyCoUk
  • 36,004
  • 6
  • 77
  • 101
  • Yes sorry I have added the declaration of ser now. It only accepts an XmlWriter, which doesn't really help me as I can't then just add '{'s and whatever. I guess it uses XmlSerializer underneath but I don't want to have to think about that right now. Setting the position has no effect. – Gaz Aug 05 '09 at 10:56
6

To access the content of a MemoryStream after it has been closed use the ToArray() or GetBuffer() methods. The following code demonstrates how to get the content of the memory buffer as a UTF8 encoded string.

byte[] buff = stream.ToArray(); 
return Encoding.UTF8.GetString(buff,0,buff.Length);

Note: ToArray() is simpler to use than GetBuffer() because ToArray() returns the exact length of the stream, rather than the buffer size (which might be larger than the stream content). ToArray() makes a copy of the bytes.

Note: GetBuffer() is more performant than ToArray(), as it doesn't make a copy of the bytes. You do need to take care about possible undefined trailing bytes at the end of the buffer by considering the stream length rather than the buffer size. Using GetBuffer() is strongly advised if stream size is larger than 80000 bytes because the ToArray copy would be allocated on the Large Object Heap where it's lifetime can become problematic.

It is also possible to clone the original MemoryStream as follows, to facilitate accessing it via a StreamReader e.g.

using (MemoryStream readStream = new MemoryStream(stream.ToArray()))
{
...
}

The ideal solution is to access the original MemoryStream before it has been closed, if possible.

Simon Giles
  • 776
  • 9
  • 10
  • 2
    "Note: ToArray() is better than GetBuffer() because ToArray() returns the exact length of the stream, rather than the buffer size (which might be larger than the stream content)." But why do you care? There's no downside in passing an over-sized array to `Encoding.GetString` - we know exactly the number of bytes to tell it to decode. The fact that `GetBuffer` *doesn't* create a copy is precisely why you'd use it - it avoids creating the pointless copy that `ToArray` creates. – Jon Skeet Apr 20 '17 at 05:40
  • Good point @JonSkeet I'll update my answer to reflect the performance considerations. If anyone is interested there is an stack exchange question about the merits of MemoryStream.GetBuffer here https://stackoverflow.com/questions/13053739/when-is-getbuffer-on-memorystream-ever-useful – Simon Giles Jun 21 '17 at 21:17
0

Just a wild guess: maybe you need to flush the streamwriter? Possibly the system sees that there are writes "pending". By flushing you know for sure that the stream contains all written characters and is readable.

Gertjan
  • 850
  • 6
  • 9