Context
I'm working on a new Enterprise Library at my company and I am implementing some code that will be used to read files into memory. Since this is an enterprise library to be shared across the organization, the size of the file to be read in is unknown. I chose to manually read the contents of the files into memory (using a buffer) instead of File.ReadAllText
in order to have more control over the read process. For now, the buffer size is fixed at 512 bytes, but may be configurable in the future to allow a given use-case to have a larger or smaller buffer size. I have come up with the following code (which appears to work well):
Code
/// <summary>
/// Class that provides access to the file system by wrapping system level file IO interactions.
/// </summary>
public class FileSystemProvider : IFileSystemProvider {
/// <summary>
/// Initializes a new instance of the <see cref="FileSystemProvider"/> class.
/// </summary>
public FileSystemProvider() {
} // end default constructor
/// <summary>
/// Returns a value indicating whether or not the specified file exists.
/// </summary>
/// <param name="filePath">string: The fully qualified path to the file.</param>
/// <returns>true if the specified file is found; false otherwise</returns>
public virtual bool FileExists(string filePath) {
bool returnValue = false;
if (this.IsValidPath(filePath)) {
returnValue = File.Exists(filePath);
}
return returnValue;
} // end function FileExists
/// <summary>
/// Returns a value indicating whether or not the given path is valid. This method is provided as a means to keep the code
/// DRY by providing a common place to verify the contents of a string prior to performing any file IO operations. This
/// method can / should be overridden by a derived type to perform any additional checks (regex, length, etc...). The
/// default implementation just checks for a null or whitespace-trimmed string.
/// </summary>
/// <param name="path">string: The path to check.</param>
/// <returns>true if the path is not null or an empty, whitespace-trimmed string; false otherwise</returns>
protected virtual bool IsValidPath(string path) {
bool returnValue = false;
if (!string.IsNullOrWhiteSpace(path)) {
returnValue = true;
}
return returnValue;
} // end function IsValidPath
/// <summary>
/// Reads the contents of the file using the specified encoding and returns the string-based content.
/// </summary>
/// <param name="filePath">string: The fully qualified path to the file.</param>
/// <param name="fileEncoding"><see cref="System.Text.Encoding"/>: The encoding used to convert the file binary to a readable string.</param>
/// <returns>The contents of the specified file as a string.</returns>
public virtual string ReadFileContents(string filePath, Encoding fileEncoding) {
string returnValue = string.Empty;
if (this.FileExists(filePath)) {
// Stream the contents of the file to reduce the memory load for large files
StringBuilder output = new StringBuilder();
byte[] bufferBlock = new byte[512];
using (FileStream fs = new FileStream(filePath, FileMode.Open)) {
if (fs.Length > bufferBlock.Length) {
long remainingBytes = fs.Length;
while (remainingBytes > 0) {
if (remainingBytes >= bufferBlock.Length) {
fs.Read(bufferBlock, 0, bufferBlock.Length);
output.Append(fileEncoding.GetString(bufferBlock).Trim());
} else {
Array.Clear(bufferBlock, 0, bufferBlock.Length);
fs.Read(bufferBlock, 0, (int)remainingBytes); // We can safely cast the long here because we know it's value is less than 512
output.Append(fileEncoding.GetString(bufferBlock, 0, (int)remainingBytes).Trim());
}
remainingBytes -= bufferBlock.Length;
}
} else {
// The total file length is smaller than our buffer size, so read it all in at once, trimming the excess buffer space
fs.Read(bufferBlock, 0, (int)fs.Length);
output.Append(fileEncoding.GetString(bufferBlock, 0, (int)fs.Length).Trim());
}
}
returnValue = output.ToString();
}
return returnValue;
} // end function ReadFileContents
/// <summary>
/// Reads the raw contents of the file.
/// </summary>
/// <param name="filePath">string: The fully qualified path to the file.</param>
/// <returns>An array of the raw byte information stored in the file.</returns>
public virtual byte[] ReadFileContents(string filePath) {
byte[] returnValue = null;
if (this.FileExists(filePath)) {
byte[] buffer = new byte[512];
returnValue = new byte[0];
using (FileStream fs = new FileStream(filePath, FileMode.Open)) {
if (fs.Length > buffer.Length) {
long remainingBytes = fs.Length;
while (remainingBytes > 0) {
if (remainingBytes >= buffer.Length) {
fs.Read(buffer, 0, buffer.Length);
int oldLength = returnValue.Length;
int newLength = returnValue.Length + buffer.Length;
Array.Resize<byte>(ref returnValue, newLength);
Array.Copy(buffer, 0, returnValue, oldLength, buffer.Length);
} else {
Array.Clear(buffer, 0, buffer.Length);
fs.Read(buffer, 0, (int)remainingBytes);
int oldLength = returnValue.Length;
int newLength = returnValue.Length + (int)remainingBytes;
Array.Resize<byte>(ref returnValue, newLength);
Array.Copy(buffer, 0, returnValue, oldLength, (int)remainingBytes);
}
remainingBytes -= buffer.Length;
}
} else {
fs.Read(buffer, 0, (int)fs.Length);
Array.Resize<byte>(ref returnValue, (int)fs.Length);
Array.Copy(buffer, 0, returnValue, 0, (int)fs.Length);
}
}
}
return returnValue;
} // end function ReadFileContents
Review Points
I'm interested in a general code review, but I'm specifically looking for input regarding:
- How will this code handle very large files (currently tested up to a file length of 5098 kB)?
- Is using the
Array.Resize<>
method in the byte[] return overload efficient, or is there a better way? - Is there a way to refactor to improve the readability and / or simplify the code?
File.ReadAllBytes
and only replace the implementation when there is clear performance evidence that this is an improvement. \$\endgroup\$