I have a LineReader class in my MiscUtil project. It's slightly more general than the solutions given here, mostly in terms of the way you can construct it:
- From a function returning a stream, in which case it will use UTF-8
 
- From a function returning a stream, and an encoding
 
- From a function which returns a text reader
 
- From just a filename, in which case it will use UTF-8
 
- From a filename and an encoding
 
The class "owns" whatever resources it uses, and closes them appropriately. However, it does this without implementing IDisposable itself. This is why it takes Func<Stream> and Func<TextReader> instead of the stream or the reader directly - it needs to be able to defer the opening until it needs it. It's the iterator itself (which is automatically disposed by a foreach loop) which closes the resource.
As Marc pointed out, this works really well in LINQ. One example I like to give is:
var errors = from file in Directory.GetFiles(logDirectory, "*.log")
             from line in new LineReader(file)
             select new LogEntry(line) into entry
             where entry.Severity == Severity.Error
             select entry;
This will stream all the errors from a whole bunch of log files, opening and closing as it goes. Combined with Push LINQ, you can do all kinds of nice stuff :)
It's not a particularly "tricky" class, but it's really handy. Here's the full source, for convenience if you don't want to download MiscUtil. The licence for the source code is here.
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace MiscUtil.IO
{
    /// <summary>
    /// Reads a data source line by line. The source can be a file, a stream,
    /// or a text reader. In any case, the source is only opened when the
    /// enumerator is fetched, and is closed when the iterator is disposed.
    /// </summary>
    public sealed class LineReader : IEnumerable<string>
    {
        /// <summary>
        /// Means of creating a TextReader to read from.
        /// </summary>
        readonly Func<TextReader> dataSource;
        /// <summary>
        /// Creates a LineReader from a stream source. The delegate is only
        /// called when the enumerator is fetched. UTF-8 is used to decode
        /// the stream into text.
        /// </summary>
        /// <param name="streamSource">Data source</param>
        public LineReader(Func<Stream> streamSource)
            : this(streamSource, Encoding.UTF8)
        {
        }
        /// <summary>
        /// Creates a LineReader from a stream source. The delegate is only
        /// called when the enumerator is fetched.
        /// </summary>
        /// <param name="streamSource">Data source</param>
        /// <param name="encoding">Encoding to use to decode the stream
        /// into text</param>
        public LineReader(Func<Stream> streamSource, Encoding encoding)
            : this(() => new StreamReader(streamSource(), encoding))
        {
        }
        /// <summary>
        /// Creates a LineReader from a filename. The file is only opened
        /// (or even checked for existence) when the enumerator is fetched.
        /// UTF8 is used to decode the file into text.
        /// </summary>
        /// <param name="filename">File to read from</param>
        public LineReader(string filename)
            : this(filename, Encoding.UTF8)
        {
        }
        /// <summary>
        /// Creates a LineReader from a filename. The file is only opened
        /// (or even checked for existence) when the enumerator is fetched.
        /// </summary>
        /// <param name="filename">File to read from</param>
        /// <param name="encoding">Encoding to use to decode the file
        /// into text</param>
        public LineReader(string filename, Encoding encoding)
            : this(() => new StreamReader(filename, encoding))
        {
        }
        /// <summary>
        /// Creates a LineReader from a TextReader source. The delegate
        /// is only called when the enumerator is fetched
        /// </summary>
        /// <param name="dataSource">Data source</param>
        public LineReader(Func<TextReader> dataSource)
        {
            this.dataSource = dataSource;
        }
        /// <summary>
        /// Enumerates the data source line by line.
        /// </summary>
        public IEnumerator<string> GetEnumerator()
        {
            using (TextReader reader = dataSource())
            {
                string line;
                while ((line = reader.ReadLine()) != null)
                {
                    yield return line;
                }
            }
        }
        /// <summary>
        /// Enumerates the data source line by line.
        /// </summary>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
}