[ Team LiB ] Previous Section Next Section

3.1 Writing Data

As with XmlReader, I'll start by taking a general look at how data is written in .NET. I've already covered input, and output is very similar in that most operations involve the Stream class. After a general introduction to how the writing process works, I'll show you a quick and simple way of writing text to a file.

3.1.1 Filesystem I/O

I covered the basics of opening and reading a file through the File and FileInfo objects in Chapter 2. In this section, I'll focus on writing to a file using the same objects.

To begin with, File has a Create( ) method. This method takes a filename as a parameter and returns a FileStream, so the most basic creation and writing to a file is fairly intuitive. Stream and its subclasses implement a variety of Write( ) methods, including one that writes an array of bytes to the Stream. The following code snippet creates a file named myfile.txt and writes the text .NET & XML to it:

byte [ ] buffer = new byte [ ] {46,78,69,84,32,38,32,88,77,76};
string filename = "myfile.txt";

FileStream stream;
stream = File.Create(filename);
stream.Write(buffer,0,buffer.Length);

That byte array is an awkward way to write a string to a Stream; ordinarily, you wouldn't hardcode an array of bytes like that. I'll show you a more typical way of encoding a string as a byte array in a moment.


If the file already exists, the previous code overwrites the files's current contents. You may not want to do that in practice; you may prefer to append to the file if it already exists. You can handle this very easily in .NET in several different ways. This snippet shows one way, with the changes highlighted:

byte [ ] buffer = new byte [ ] {46,78,69,84,32,38,32,88,77,76};
string filename = "myfile.txt";

FileStream stream;
if (File.Exists(filename)) {
  // it already exists, let's append to it
  stream = File.OpenWrite(filename);
  stream.Seek(0,SeekOrigin.End);
} else {
  // it does not exist, let's create it
  stream = File.Create(filename);
}

stream.Write(buffer,0,buffer.Length);

SeekOrigin is an enumeration in the System.IO namespace that indicates where the Seek( ) method should seek from. In this code, I'm seeking 0 bytes from the end, but you could also seek from the beginning of the Stream (SeekOrigin.Begin) or from the current position (SeekOrigin.Current).

3.1.1.1 File access and permissions

There are several other ways to open a file for writing. For example, this snippet shows several changes from the previous one. The changes are highlighted:

byte [ ] buffer = new byte [ ] {46,78,69,84,32,38,32,88,77,76};
string filename = "myfile.txt";

FileStream stream;
FileMode fileMode;
if (File.Exists(filename)) {
  // it already exists, let's append to it
  fileMode = FileMode.Append;
} else {
  // it does not exist, let's create it
  fileMode = FileMode.CreateNew;
}

stream = File.Open(filename,fileMode,FileAccess.Write,FileShare.None);
stream.Write(buffer,0,buffer.Length);

The File.Open( ) method has several overloads with additional parameters. The FileMode enumeration specifies what operations the file is to be opened for. Table 3-1 lists the FileMode enumerations.

Table 3-1. FileMode values

Value

Description

CreateNew

A new file will be created. If it already exists, an IOException is thrown.

Create

A file will be created, or, if it already exists, truncated and overwritten.

Open

An existing file will be opened. If it does not exist, a FileNotFoundException is thrown.

OpenOrCreate

If the file exists, it will be opened; if it does not, a new file will be created.

Truncate

An existing file will be opened and truncated to zero bytes long. An attempt to read a truncated file will result in an exception being thrown.

Append

If the file exists, it will be opened and data will be written at the end of the file. If the file does not exist, a new file will be created. Any attempt to Seek( ) to a position before the previous end of the file will result in an exception being thrown. An attempt to read from the file will result in an ArgumentException being thrown.

The FileAccess enumeration restricts the operations a program can exercise on the file, once it has been opened. Table 3-2 details the FileAccess enumerations.

Table 3-2. FileAccess values

Value

Description

Read

Data can be read from the file.

Write

Data can be written to the file.

ReadWrite

Data can be read from or written to the file. Equivalent to FileAccess.Read | FileAccess.Write.

FileShare restricts what operations other applications can exercise. Table 3-3 describes the FileShare enumerations.

Table 3-3. FileShare values

Value

Description

None

No other process may access the file as long as this process has it open.

Read

Subsequent requests to read from the file by other processes will succeed, if they have the other appropriate permissions.

Write

Subsequent requests to write to the file by other processes will succeed, if they have the other appropriate permissions.

ReadWrite

Subsequent requests to read from and write to the file by other processes will succeed, if they have the other appropriate permissions (equivalent to FileShare.Read | FileShare.Write).

Inheritable

The file handle is inheritable by child processes. This is not directly supported by Win32.

Some combinations of FileMode and FileAccess do not make sense and will cause an ArgumentException to be thrown. For example, opening a file with FileMode.Create and FileAccess.Read would mean that you wanted to create a file but then only be allowed to read from it.


3.1.1.2 Encodings and StreamWriter

Having to create an array of bytes to write with the Stream.Write( ) method is a bit tiresome. Luckily, there are at least two ways to work around this. The first is System.Text.Encoding. This class contains methods to convert strings to and from byte arrays, for a given number of standard encodings, including ASCII, UTF-8, and UTF-16. These encodings are provided as static properties of the Encoding class. Strings in .NET are stored in Unicode梬hile ASCII characters are each stored in a single byte, Unicode characters are stored in four bytes. The GetBytes( ) method takes a .NET string and returns an array of bytes in the appropriate encoding, suitable for use by Stream.Write( ):

Unicode is a standard that provides a unique four-byte representation of every character in every alphabet, on any computer operating system and platform. Happily, XML and .NET both use encodings of Unicode by default. For more information about Unicode, visit the Unicode Consortium's web site at http://www.unicode.org/.


string message = "Hello, world.";
byte [ ] buffer = Encoding.ASCII.GetBytes(message);
string filename = "myfile.txt";

FileStream stream;
FileMode fileMode;
if (File.Exists(filename)) {
  // it already exists, let's append to it
  fileMode = FileMode.Append;
} else {
  // it does not exist, let's create it
  fileMode = FileMode.CreateNew;
}

stream = File.Open(filename,fileMode,FileAccess.Write,FileShare.None);
stream.Write(buffer,0,buffer.Length);

Just to belabor the point, remember that a C# byte is the familiar eight-bit byte, but a C# char is a Unicode character. The encodings defined in the .NET Framework are shown in Table 3-4.

Table 3-4. Supported encodings

Class name

Property name

Description

ASCIIEncoding

Encoding.ASCII

7 bit ASCII, used in the snippet above. Support is required for XML processors.

UnicodeEncoding

Encoding.BigEndianUnicode

Encoding.Unicode

Big-endian Unicode.

Little-endian Unicode.

 

Encoding.Default

Represents the default encoding for the current system.

UTF7Encoding

Encoding.UTF7

UTF-7.

UTF8Encoding

Encoding.UTF8.

UTF-8. Support is required for XML processors.

In addition, any other encodings are accessible by calling Encoding.GetEncoding( ) and passing the code page or encoding name.

The other way to write a stream of characters is even simpler. A FileStream is a subclass of Stream, which can be used as a parameter to the StreamWriter's constructor. StreamWriter is analogous to StreamReader, and is a subclass of TextWriter, which is optimized to write textual data to a Stream. The TextWriter's Write( ) and WriteLine( ) methods take care of the encoding of various datatypes when writing to a Stream:

string textToWrite = "This is the text I want to write to the file.";
string filename = "myfile.txt";

FileStream stream;
FileMode fileMode;
if (File.Exists(filename)) {
  // it already exists, let's append to it
  fileMode = FileMode.Append;
} else {
  // it does not exist, let's create it
  fileMode = FileMode.CreateNew;
}

stream = File.Open(filename,fileMode,FileAccess.Write,FileShare.None);
StreamWriter writer = new StreamWriter(stream);
writer.Write(textToWrite);
writer.Flush( );
writer.Close( );

The last two lines of this code snippet cause the output buffer to be flushed, and the file to be closed. Every time you write to the file and you want the file on disk to reflect the changes immediately, it is important to call Flush( ) on the Stream or StreamWriter. You can also indicate that the contents of the file are to be flushed to disk automatically by setting AutoFlush to true:

StreamWriter writer = new StreamWriter(stream);
writer.AutoFlush = true;        
writer.Write(textToWrite);

When you are completely done with a file, you should call Close( ) on the File or the Stream. If you don't call Close( ) yourself, the file will be closed when the garbage collector cleans up the method's local variables. Unfortunately, you don't know when that will happen, so it's always best to close files yourself.

You can close the underlying file by calling Close( ) on the FileStream because, by default, when you instantiate a FileStream, it owns the underlying file. It's important to remember that the FileStream owns the underlying file handle in case you open several streams on the same file; closing any one of them will cause all of them to be closed.


There's an even quicker way to append to an existing file. The StreamWriter class has a constructor that takes a filename as a parameter. Since StreamWriter inherits from TextWriter, it implements the IDisposable interface, which allows you to use the using keyword to automatically close the Stream at the end of the using block.

All the code you wrote above could instead be simplified to five lines, if all you want to do is write a quick chunk of text to a file:

string textToWrite = "This is the text I want to write to the file.";
string filename = "myfile.txt";
using (StreamWriter writer = new StreamWriter(filename,true)) {
  writer.Write(textToWrite);
}

The second parameter to the StreamWriter constructor indicates that the text is to be appended to the file if the file already exists.

3.1.2 Network I/O

Just as with input, network output can use Socket, Stream, or WebRequest objects. The basic unit of network communication is the Socket. For higher-level network output, you can use the WebRequest class. Whether communicating over a Socket or a WebRequest, however, you'll be using a Stream to actually read and write data.

3.1.2.1 Writing data with Sockets

To communicate over a network using a Socket, there must be a server of some sort listening for requests at the other end. The construction of network application servers is beyond the scope of this book, but Example 3-1 shows you how to create a simple network client program.

Example 3-1. A simple network client program
using System;
using System.IO;
using System.Net.Sockets;

public class NetWriter {

  public static void Main(string [ ] args) {

    string address = "example.com";
    int port = 9999;

    TcpClient client = new TcpClient(address,port);
    NetworkStream stream = client.GetStream( );

    StreamWriter writer = new StreamWriter(stream);

    writer.WriteLine("hello\r\n");
    writer.Flush( );

    using (StreamReader reader = new StreamReader(stream)) {
      while (reader.Peek( ) != -1) {
        Console.WriteLine(reader.ReadLine( ));
      }
    }
  }
}

The Main( ) method can be broken down into its major steps. The first step is to initialize some variables:

string address = "example.com";
int port = 9999;

TcpClient is a convenient specialization of a TCP/IP client Socket. The GetStream( ) method makes the connection and returns a Stream to communicate with the remote Socket:

TcpClient client = new TcpClient(address,port);
NetworkStream stream = client.GetStream( );

Next, you use a StreamWriter to write a single line directly to the remote Socket. Since you've connected to port 9999 of the server example.com, you can write the line hello, followed by an end-of-line marker, to the Socket, and receive back the data that the server wants to send. Once again, it's important to call Flush( ), so that the data is actually written to the Stream:

This example assumes that there is some server at the domain address example.com listening for requests on port 9999. In reality, this service does not exist. A similar procedure could be used to connect to any network resource on any port, as long as you know what protocol (such as HTTP) to use to communicate with it.


StreamWriter writer = new StreamWriter(stream);

writer.WriteLine("hello\r\n");
writer.Flush( );

Now that you have written data to the Stream, you can read any data that the server sends back. This while loop checks that there is more data to read, and then echoes it to the console. Finally, the using statement automatically closes the Stream, which closes the underlying network Socket and releases any resources the Socket is holding, much like closing a FileStream:

using (StreamReader reader = new StreamReader(stream)) {
  while (reader.Peek( ) != -1) {
    Console.WriteLine(reader.ReadLine( ));
  }
}

Similar to the way the FileStream class owns its underlying file handle, the StreamWriter owns the underlying network stream. In Socket-based communication, you shouldn't close the StreamWriter until you're done with the entire conversation, because the same underlying Stream will be used to read the response. The Stream represents both sides of the conversation.

3.1.2.2 Writing data with WebRequest

As I mentioned in Chapter 2, the WebRequest class supports the http, https, and file URL schemes, so you could theoretically use a WebRequest to send data to a web server. However, the mechanism for writing files to a web server is not so clear cut; the options for writing data to URLs are limited to the methods that HTTP supports, namely GET, HEAD, POST, PUT, DELETE, TRACE, and OPTIONS. In Chapter 2, I used the default HTTP method, GET. Table 3-5 describes the other HTTP methods.

Table 3-5. HTTP methods

Method

Description

GET

A request for information located at the URI

HEAD

A request for header information about the data located at the URI

POST

A request for information located at the URI, which includes additional request parameters

PUT

A request to store the body of the request at the location specified by the URI

DELETE

A request to delete the information located at the URI

TRACE

A request to have the body sent back to the requester, usually used for debugging

OPTIONS

A request for information about the communication options available for the information located at the URI

There are at least two ways to write data to a web server. The first involves complicated URLs using the POST or GET methods and CGI or ASP.NET code on the server, all of which are outside the scope of this book. The second requires the server to support the HTTP PUT method, which may also require some custom setup on the server. Example 3-2 shows a simple program that constructs a WebRequest using the PUT method.

Example 3-2. Program to send an HTTP PUT request
using System;
using System.IO;
using System.Net;

public class HttpPut {

  public static void Main(string [ ] args) {

    string url = "http://myserver.com/file.txt";
    string username = "niel";
    string password = "secret";
    string data = "This data should be written to the URL.";

    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
    request.Method = "PUT";
    request.Credentials = new NetworkCredential(username, password);
    request.ContentLength = data.Length;
    request.ContentType = "text/plain";

    using (StreamWriter writer = new StreamWriter(request.GetRequestStream( ))) {
      writer.WriteLine(data);
    }

    WebResponse response = request.GetResponse( );

    using (StreamReader reader = new StreamReader(response.GetResponseStream( ))) {
      while (reader.Peek( ) != -1) {
        Console.WriteLine(reader.ReadLine( ));
      }
    }
  }
}

If your web server does not support the PUT method, you will probably receive an HTTP (405) Method not allowed error, wrapped in a System.Net.WebException.


Let's look at this code in small pieces:

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "PUT";
request.ContentLength = data.Length;
request.ContentType = "text/plain";
request.Credentials = new NetworkCredential(username, password);

This code creates a new WebRequest to communicate with the server and sets the method to PUT. With a PUT request, you need to set the ContentLength property before writing to the Stream. If the PUT method is properly implemented, it should also require some sort of authentication to prevent improper access; that's what the NetworkCredential is for:

In an interactive application, you should not hardcode a username and password in your source code; you should prompt the user to enter them. The NetworkCredential class handles basic, digest, NTLM, and Kerberos authentication automatically. If you're concerned about writing secure code梐nd you should be!梒heck out Secure Coding: Principles & Practices by Mark G. Graff and Kenneth R. van Wyk (O'Reilly).


using (StreamWriter writer = new StreamWriter(request.GetRequestStream( ))) {
  writer.WriteLine(data);
}

This code writes the content of the file directly to the WebRequest's Stream. It's important to close the Stream (done here by virtue of the IDisposable interface and the using statement) to release the connection for reuse; otherwise, the application will run out of connections:

The GET method does not allow any data to be written to the request body. If you attempt to write data to a WebRequest's Stream and WebRequest.Method is GET, the default, a ProtocolViolationException will be thrown.


WebResponse response = request.GetResponse( );

using (StreamReader reader = new StreamReader(response.GetResponseStream( ))) {
  while (reader.Peek( ) != -1) {
    Console.WriteLine(reader.ReadLine( ));
  }
}

Finally, data is read from the WebResponse. Typically, the data returned from a PUT request should include the URL of the file you created or updated.

Once you have a WebRequest, you can also set its WebProxy as I demonstrated in Chapter 2.

There is a whole world of detail to be examined when it comes to HTTP PUT. However, I just wanted to give you a taste of writing data across the network, because a Stream is a Stream as far as XmlWriter is concerned, and a local file may just as well be halfway across the planet.

    [ Team LiB ] Previous Section Next Section