21

I need to manipulate the contents of a file:

 FileStream fs = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);               
 StreamReader sr = new StreamReader(fs);
 StreamWriter sw = new StreamWriter(fs);
 newString = someStringTransformation(sr.ReadToEnd());
 sw.Write(newString);
 fs.flush();
 fs.Close();

However the above appends the newString instead of overwriting the file with the new changes. It needs to be done so that no other application can access the file in between reading a writing which is why I'm creating the reader and writer from a FileStream object.

I know that you can create a StreanWriter with the second parameter set to false as described here. However when creating the StreamWriter as above that does not seem to be one of the parameters.

Dmitry Bychenko
  • 180,369
  • 20
  • 160
  • 215
EJS
  • 1,011
  • 5
  • 14
  • 28
  • 1
    Try flushing the stream and repositioning it back at the start of the file, then writing, then flushing the writer, then truncating the file. – Lasse V. Karlsen Nov 11 '15 at 08:09
  • Another way is create another temporary file, save all the new contents to that file then delete the old one and rename the new one. – Callum Linington Nov 11 '15 at 08:12
  • 1
    What you are trying to do makes no sense, you have no protection against another process reading the file a microsecond before you open the file. – Hans Passant Nov 11 '15 at 08:16
  • side note: when working with `IDisposable` (e.g. `FileStream`) wrapping it into `using(FileStream fs = new FileStream..) {...}` is a better design than calling `Close` (you may have resource leakage on exceptions) – Dmitry Bychenko Nov 11 '15 at 08:38

6 Answers6

33

The problem you are having is that reading from the stream advances to the end of the file. Further writes will then append.

This will achieve a full overwrite.

using(FileStream fs = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None))
{
    StreamReader sr = new StreamReader(fs);
    using (StreamWriter sw = new StreamWriter(fs))
    {
        newString = someStringTransformation(sr.ReadToEnd());

        // discard the contents of the file by setting the length to 0
        fs.SetLength(0); 

        // write the new content
        sw.Write(newString);
    }
}

Why use SetLength? Your new content might be shorter than the existing string! The last thing you want is the old content at the end of your file.

Dorus
  • 7,276
  • 1
  • 30
  • 36
Gusdor
  • 14,001
  • 2
  • 52
  • 64
  • 2
    Don't forget to `.Close()` the `StreamWriter` and `StreamReader`. Alternatively they can be encapsulated in a `using() { }` block. – Stefan May 20 '19 at 15:09
4

There are several steps you need to take here but let me make my assumptions clear:

You need to keep the file open and locked during the entire operation to prevent others from accessing the file during this time.

With that said, here's what you need to do:

  1. You need to read the contents using the StreamReader, as you've done
  2. You need to reposition the underlying stream back to the start, its position have been changed by reading through the reader
  3. You need to write out the transformed contents through the StreamWriter, as you've done
  4. You need to flush the writer because of the next step
  5. You need to truncate the underlying stream/file to its current position, to handle a transformation that shortens the contents.

The code for all of this could look like this LINQPad program:

void Main()
{
    const string filePath = @"d:\temp\test.txt";
    var encoding = Encoding.UTF8;
    using (var stream = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None))
    using (var reader = new StreamReader(stream, encoding))
    using (var writer = new StreamWriter(stream, encoding))
    {
        // Read
        var contents = reader.ReadToEnd();

        // Transform
        var transformedContents = contents.Substring(0, Math.Max(0, contents.Length - 1));

        // Write out transformed contents from the start of the file
        stream.Position = 0;
        writer.Write(transformedContents);
        writer.Flush();

        // Truncate
        stream.SetLength(stream.Position);
    }
}
Lasse V. Karlsen
  • 380,855
  • 102
  • 628
  • 825
3

Just use:

FileStream fs = System.IO.File.Create(filePath);

File.Create will create or overwrite a file and return the filestream for it.

  • The only thing that is concerning me is that this method raises an `UnauthorizedAccessException` when the [target file is hidden](https://learn.microsoft.com/en-us/dotnet/api/system.io.file.create). Does anyone know whether that is the same with all variations of opening a `FileStream`? – Amir Mahdi Nassiri Nov 15 '22 at 15:29
2

You can avoid these low-level Stream's and their Reader/Writers by using Linq:

  File.WriteAllText(filePath, someStringTransformation(File.ReadAllText(filePath)));
Dmitry Bychenko
  • 180,369
  • 20
  • 160
  • 215
2

Maybe it will help one.

Just use FileMode.Open or FileMode.Truncate to overwrite the file:

namespace System.IO
{
    //
    // Summary:
    //     Specifies how the operating system should open a file.
    [ComVisible(true)]
    public enum FileMode
    {
        ...
        //
        // Summary:
        //     Specifies that the operating system should create a new file. If the file already
        //     exists, it will be overwritten. This requires System.Security.Permissions.FileIOPermissionAccess.Write
        //     permission. FileMode.Create is equivalent to requesting that if the file does
        //     not exist, use System.IO.FileMode.CreateNew; otherwise, use System.IO.FileMode.Truncate.
        //     If the file already exists but is a hidden file, an System.UnauthorizedAccessException
        //     exception is thrown.
        Create = 2,
        //
        ...
    }

or

namespace System.IO
{
    //
    // Summary:
    //     Specifies how the operating system should open a file.
    [ComVisible(true)]
    public enum FileMode
    {
        ...
        //
        // Summary:
        //     Specifies that the operating system should open an existing file. When the file
        //     is opened, it should be truncated so that its size is zero bytes. This requires
        //     System.Security.Permissions.FileIOPermissionAccess.Write permission. Attempts
        //     to read from a file opened with FileMode.Truncate cause an System.ArgumentException
        //     exception.
        Truncate = 5,
        ...
    }
Vitaliy Prushak
  • 1,057
  • 8
  • 13
1

What you could do is repositioning the streams and also removing buffered data to be sure that nothing gets in the way. Taking your example:

FileStream fs = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);               
 StreamReader sr = new StreamReader(fs);
 StreamWriter sw = new StreamWriter(fs);
 newString = someStringTransformation(sr.ReadToEnd());

    sr.Position = 0;
    sr.DiscardBufferedData(); 

    sw.Position = 0;

 sw.Write(newString);
 fs.flush();
 fs.Close();

if the new data is less than the old data you would need to truncate the remaining data. By using sw.SetLength(newString.Length);.

Thomas
  • 2,886
  • 3
  • 34
  • 78