I recently discovered a method which has been around since .NET 4 (shame on me for not knowing about it before now). It is in the File
class: File.ReadAllLines(string path)
It returns a string array, each string consisting of a line in the text file. So basically, where we previously would have written something like:
FileInfo templateFile = new FileInfo(pathToFile);
TextReader reader = templateFile.OpenText();
List parsedLines = new List();
while ((lineOfText = reader.ReadLine()) != null)
{
parsedLines.Add(lineOfText);
}
We could now get it done with:
string [] parsedLines = File.ReadAllLines(pathToFile);
I thought I would road-test this method with a little real world situation. The task at hand is to populate a template at run-time with some data and configuration options for an external hardware unit; a high-volume industrial DVD writer. The template in our example is a text file:
# Job file for External Component
Stacker : {0}
Publisher : {0}
Label Details : Date-{0}, Title-{1}, Artist-{2}
Verify Write: {0}
Data: {0}
We will populate those placeholders (the numbers in curly braces) with an array of configuration values which we will store in the App.config file. This enables us to change those values from run to run without having to recompile:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="CurrentTemplate" value="sampleTemplate.job.txt" />
<add key="Placeholders" value="1,CD_Pub1,12 July 2012,The Southern Harmony and Musical Companion,The Black Crowes,Do not Verify,D:\BurnDataBucket" />
</appSettings>
</configuration>
The following code will complete the task (note the use of the File.ReadAllLines
method at the beginning of the static method ProcessTemplateWithValues
):
private const string PlaceHolderPattern = @"{\d}";
static void Main()
{
var pathToCurrentTemplate = GetCurrentTemplate();
var configOptionsForTemplate = GetConfigForTemplate().ToArray();
var populatedLines = ProcessTemplateWithValues(pathToCurrentTemplate, configOptionsForTemplate);
// ... do something with populated template
}
private static IList ProcessTemplateWithValues(string pathToCurrentTemplate, params object[] args)
{
string [] linesOfText = File.ReadAllLines(pathToCurrentTemplate);
string populatedLine = string.Empty;
var listOfLines = new List();
int startingArgsIndex = 0;
foreach (var lineOfText in linesOfText)
{
int numberOfPlaceholders = lineOfText.GetNumberOfPatternPlaceholders(PlaceHolderPattern);
if (numberOfPlaceholders > 0)
{
if (TryParseValues(lineOfText, numberOfPlaceholders, ref populatedLine, ref startingArgsIndex, args))
{
listOfLines.Add(populatedLine);
}
else
{
throw new FormatException(string.Format(
"The line beginning with the word {0} failed to parse.",
new String(lineOfText.TakeWhile(c => c != ':').ToArray())));
}
}
else
{
listOfLines.Add(lineOfText);
}
}
return listOfLines;
}
public static bool TryParseValues(string lineOfText, int numberOfPlaceholders, ref string populatedLine, ref int startingArgsIndex, params object[] args)
{
var argsForThisLine = new object[numberOfPlaceholders];
if (startingArgsIndex > args.Length - 1 || startingArgsIndex < 0)
return false;
Array.Copy(args, startingArgsIndex, argsForThisLine, 0, numberOfPlaceholders);
startingArgsIndex += numberOfPlaceholders;
populatedLine = string.Format(lineOfText, argsForThisLine);
return true;
}
static string GetCurrentTemplate()
{
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Templates", ConfigurationManager.AppSettings["CurrentTemplate"]);
}
static IEnumerable GetConfigForTemplate()
{
return ConfigurationManager.AppSettings["Placeholders"].Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
}
We can now take those configuration options and use them, as the elipses in the code samples is meant to indicate.
Now, compare with the same task being implemented using the pre-.NET4 style of code:
private const string PlaceHolderPattern = @"{\d}";
static void Main()
{
var pathToCurrentTemplate = GetCurrentTemplate();
var templateFile = new FileInfo(pathToCurrentTemplate);
TextReader templateReader = templateFile.OpenText();
var configOptionsForTemplate = GetConfigForTemplate().ToArray();
var populatedLines = ProcessTemplateWithValues(templateReader, configOptionsForTemplate);
// ... do something with populated template
}
private static IList ProcessTemplateWithValues(TextReader reader, params object[] args)
{
string lineOfText = string.Empty;
string populatedLine = string.Empty;
var listOfLines = new List();
int startingArgsIndex = 0;
while ((lineOfText = reader.ReadLine()) != null)
{
int numberOfPlaceholders = lineOfText.GetNumberOfPatternPlaceholders(PlaceHolderPattern);
if (numberOfPlaceholders > 0)
{
if (TryParseValues(lineOfText, numberOfPlaceholders, ref populatedLine, ref startingArgsIndex, args))
{
listOfLines.Add(populatedLine);
}
else
{
throw new FormatException(string.Format(
"The line beginning with the word {0} failed to parse.", new String(lineOfText.TakeWhile(c => c != ':').ToArray())));
}
}
else
{
listOfLines.Add(lineOfText);
}
}
return listOfLines;
}
public static bool TryParseValues(string lineOfText, int numberOfPlaceholders, ref string populatedLine, ref int startingArgsIndex, params object[] args)
{
var argsForThisLine = new object[numberOfPlaceholders];
if (startingArgsIndex > args.Length - 1 || startingArgsIndex < 0)
return false;
Array.Copy(args, startingArgsIndex, argsForThisLine, 0, numberOfPlaceholders);
startingArgsIndex += numberOfPlaceholders;
populatedLine = string.Format(lineOfText, argsForThisLine);
return true;
}
static string GetCurrentTemplate()
{
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Templates", ConfigurationManager.AppSettings["CurrentTemplate"]);
}
static IEnumerable GetConfigForTemplate()
{
return ConfigurationManager.AppSettings["Placeholders"].Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
}
There we see that a few extra calories are burned in extra code with the explicit creation and use of a TextReader
. So a bit of a saving is made with less code. But note that there is only 1 iteration of the lines in the old way. Using File.ReadLines
, we iterated once using that method in and of itself (inside the framework) and a second time in the foreach
loop which we wrote. There’s always a tradeoff.
I should also point out a handy little extension method which I wrote and used in that code. GetNumberOfPatternPlaceholders
returns the number of times a placeholder appears in a particular line:
public static int GetNumberOfPatternPlaceholders(this string source, string pattern)
{
var regex = new Regex(pattern);
var matches = regex.Matches(source);
return matches.Count;
}
Whilst File.ReadAllLines
does not buy us much in this scenario (in terms of briefer code), I still think it’s a welcome addition to the framework and I’m sure I will find good use for it from time to time.
Get the code: