Skip to content

Instantly share code, notes, and snippets.

@cnjimbo
Last active March 21, 2017 09:15
Show Gist options
  • Select an option

  • Save cnjimbo/4f7b386349a2d04b2272460faaf5ed07 to your computer and use it in GitHub Desktop.

Select an option

Save cnjimbo/4f7b386349a2d04b2272460faaf5ed07 to your computer and use it in GitHub Desktop.
Simple File Logger Not Simple Function,Entirely Independent. This is an open source from Microsoft patterns & practices Enterprise Library Logging Application Block //We have done lots of enhancement
//===============================================================================
#region Using
// Simple File Logger Not Simple Function,Entirely Independent
//===============================================================================
//This is an open source from Microsoft patterns & practices Enterprise Library Logging Application Block
//We have done lots of enhancement,
//===============================================================================
#endregion
#region USING BLOCK
using System;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
#endregion
namespace Tsharp
{
public static class SimpleLogger
{
public static LogSource Default = new LogSource();
/// <summary>
/// write entry to the default logger
/// </summary>
/// <param name="entry"></param>
public static void WriteLine(object entry)
{
Default.WriteLine(entry);
}
/// <summary>
/// write the category and entry to the default logger, such as 'category+entry
/// </summary>
/// <param name="entry"></param>
/// <param name="category"></param>
public static void WriteLine(object entry, string category)
{
Default.WriteLine(entry, category);
}
/// <summary>
/// Defines the behavior when the roll file is created.
/// </summary>
public enum RollFileExistsBehavior
{
/// <summary>
/// Overwrites the file if it already exists.
/// </summary>
Overwrite,
/// <summary>
/// Use a secuence number at the end of the generated file if it already exists. If it fails again then increment the
/// secuence until a non existent filename is found.
/// </summary>
Increment
}
/// <summary>
/// Defines the frequency when the file need to be rolled.
/// </summary>
public enum RollInterval
{
/// <summary>
/// None Interval
/// </summary>
None,
/// <summary>
/// Minute Interval
/// </summary>
Minute,
/// <summary>
/// Hour interval
/// </summary>
Hour,
/// <summary>
/// Day Interval
/// </summary>
Day,
/// <summary>
/// Week Interval
/// </summary>
Week,
/// <summary>
/// Month Interval
/// </summary>
Month,
/// <summary>
/// Year Interval
/// </summary>
Year,
/// <summary>
/// At Midnight
/// </summary>
Midnight
}
/// <summary>
/// Purges archive files generated by the <see cref="RollingFlatFileTraceListener" />.
/// </summary>
public class RollingFlatFilePurger
{
private readonly string baseFileName;
private readonly int cap;
private readonly string directory;
/// <summary>
/// Initializes a new instance of the <see cref="RollingFlatFilePurger" /> class.
/// </summary>
/// <param name="directory">The folder where archive files are kept.</param>
/// <param name="baseFileName">The base name for archive files.</param>
/// <param name="cap">The number of archive files to keep.</param>
public RollingFlatFilePurger(string directory, string baseFileName, int cap)
{
if (directory == null) throw new ArgumentNullException("directory");
if (baseFileName == null) throw new ArgumentNullException("baseFileName");
if (cap < 1) throw new ArgumentOutOfRangeException("cap");
this.directory = directory;
this.baseFileName = baseFileName;
this.cap = cap;
}
/// <summary>
/// Purges archive files.
/// </summary>
public void Purge()
{
var extension = Path.GetExtension(baseFileName);
var searchPattern = Path.GetFileNameWithoutExtension(baseFileName) + ".*" +
extension;
var matchingFiles = TryGetMatchingFiles(searchPattern);
if (matchingFiles.Length <= cap) return;
// sort the archive files in descending order by creation date and sequence number
var sortedArchiveFiles =
matchingFiles.Select(matchingFile => new ArchiveFile(matchingFile))
.OrderByDescending(archiveFile => archiveFile);
using (var enumerator = sortedArchiveFiles.GetEnumerator())
{
// skip the most recent files
for (var i = 0; i < cap; i++) if (!enumerator.MoveNext()) return;
// delete the older files
while (enumerator.MoveNext()) TryDelete(enumerator.Current.Path);
}
}
private string[] TryGetMatchingFiles(string searchPattern)
{
try
{
return Directory.GetFiles(directory, searchPattern,
SearchOption.TopDirectoryOnly);
}
catch (DirectoryNotFoundException)
{
}
catch (IOException)
{
}
catch (UnauthorizedAccessException)
{
}
return new string[0];
}
private static void TryDelete(string path)
{
try
{
File.Delete(path);
}
catch (UnauthorizedAccessException)
{
// cannot delete the file because of a permissions issue - just skip it
}
catch (IOException)
{
// cannot delete the file, most likely because it is already opened - just skip it
}
}
private static DateTimeOffset GetCreationTime(string path)
{
try
{
return File.GetCreationTimeUtc(path);
}
catch (UnauthorizedAccessException)
{
// will cause file be among the first files when sorting,
// and its deletion will likely fail causing it to be skipped
return DateTimeOffset.MinValue;
}
}
/// <summary>
/// Extracts the sequence number from an archive file name.
/// </summary>
/// <param name="fileName">The archive file name.</param>
/// <returns>The sequence part of the file name.</returns>
public static string GetSequence(string fileName)
{
if (fileName == null) throw new ArgumentNullException(fileName, "fileName");
var extensionDotIndex = fileName.LastIndexOf('.');
if (extensionDotIndex <= 0) return string.Empty;
var sequenceDotIndex = fileName.LastIndexOf('.', extensionDotIndex - 1);
if (sequenceDotIndex < 0) return string.Empty;
return fileName.Substring(sequenceDotIndex + 1,
extensionDotIndex - sequenceDotIndex - 1);
}
internal class ArchiveFile : IComparable<ArchiveFile>
{
private readonly string fileName;
private int? sequence;
private string sequenceString;
public ArchiveFile(string path)
{
Path = path;
fileName = System.IO.Path.GetFileName(path);
CreationTime = GetCreationTime(path);
}
public string Path { get; }
public DateTimeOffset CreationTime { get; }
public string SequenceString
{
get
{
if (sequenceString == null) sequenceString = GetSequence(fileName);
return sequenceString;
}
}
public int Sequence
{
get
{
if (!sequence.HasValue)
{
int theSequence;
if (int.TryParse(SequenceString, NumberStyles.None,
CultureInfo.InvariantCulture, out theSequence))
sequence = theSequence;
else sequence = 0;
}
return sequence.Value;
}
}
public int CompareTo(ArchiveFile other)
{
var creationDateComparison = CreationTime.CompareTo(other.CreationTime);
if (creationDateComparison != 0) return creationDateComparison;
if ((Sequence != 0) && (other.Sequence != 0))
return Sequence.CompareTo(other.Sequence);
// compare the sequence part of the file name as plain strings
return SequenceString.CompareTo(other.SequenceString);
}
}
}
/// <summary>
/// Helper class for working with environment variables.
/// </summary>
public static class EnvironmentHelper
{
/// <summary>
/// Sustitute the Environment Variables
/// </summary>
/// <param name="fileName">The filename.</param>
/// <returns></returns>
public static string ReplaceEnvironmentVariables(string fileName)
{
// Check EnvironmentPermission for the ability to access the environment variables.
try
{
var variables = Environment.ExpandEnvironmentVariables(fileName);
// If an Environment Variable is not found then remove any invalid tokens
var filter = new Regex("%(.*?)%",
RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
var filePath = filter.Replace(variables, "");
if (Path.GetDirectoryName(filePath) == null)
filePath = Path.GetFileName(filePath);
return RootFileNameAndEnsureTargetFolderExists(filePath);
}
catch (SecurityException)
{
throw new InvalidOperationException("Environment Variables access denied.");
}
}
private static string RootFileNameAndEnsureTargetFolderExists(string fileName)
{
var rootedFileName = fileName;
if (!Path.IsPathRooted(rootedFileName))
rootedFileName =
Path.Combine(AppDomain.CurrentDomain.SetupInformation.ApplicationBase,
rootedFileName);
var directory = Path.GetDirectoryName(rootedFileName);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
Directory.CreateDirectory(directory);
return rootedFileName;
}
}
/// <summary>
/// <para>
/// Provides the <see langword='abstract ' />base class for the listeners who
/// monitor trace and debug output.
/// </para>
/// </summary>
[HostProtection(Synchronization = true)]
public abstract class TraceListener : IDisposable
{
private int indentLevel;
private int indentSize = 4;
private string listenerName;
/// <summary>
/// <para>Initializes a new instance of the <see cref='System.Diagnostics.TraceListener' /> class.</para>
/// </summary>
protected TraceListener()
{
}
/// <summary>
/// <para>
/// Initializes a new instance of the <see cref='System.Diagnostics.TraceListener' /> class using the specified
/// name as the
/// listener.
/// </para>
/// </summary>
protected TraceListener(string name)
{
listenerName = name;
}
/// <summary>
/// <para> Gets or sets a name for this <see cref='System.Diagnostics.TraceListener' />.</para>
/// </summary>
public virtual string Name
{
get { return listenerName == null ? "" : listenerName; }
set { listenerName = value; }
}
public virtual bool IsThreadSafe
{
get { return false; }
}
/// <summary>
/// <para>Gets or sets the indent level.</para>
/// </summary>
public int IndentLevel
{
get { return indentLevel; }
set { indentLevel = value < 0 ? 0 : value; }
}
/// <summary>
/// <para>Gets or sets the number of spaces in an indent.</para>
/// </summary>
public int IndentSize
{
get { return indentSize; }
set
{
if (value < 0)
throw new ArgumentOutOfRangeException("IndentSize", value,
"IndentSize must be greater then zero");
indentSize = value;
}
}
/// <summary>
/// <para>Gets or sets a value indicating whether an indent is needed.</para>
/// </summary>
protected bool NeedIndent { get; set; } = true;
/// <summary>
///
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// </summary>
protected virtual void Dispose(bool disposing)
{
}
/// <summary>
/// <para>
/// When overridden in a derived class, closes the output stream
/// so that it no longer receives tracing or debugging output.
/// </para>
/// </summary>
public virtual void Close()
{
}
/// <summary>
/// <para>When overridden in a derived class, flushes the output buffer.</para>
/// </summary>
public virtual void Flush()
{
}
/// <summary>
/// <para>
/// When overridden in a derived class, writes the specified
/// message to the listener you specify in the derived class.
/// </para>
/// </summary>
public abstract void Write(string message);
/// <summary>
/// <para>
/// Writes the name of the <paramref name="o" /> parameter to the listener you specify when you inherit from the
/// <see cref='System.Diagnostics.TraceListener' />
/// class.
/// </para>
/// </summary>
public virtual void Write(object o)
{
if (o == null) return;
Write(o.ToString());
}
/// <summary>
/// <para>
/// Writes a category name and a message to the listener you specify when you
/// inherit from the <see cref='System.Diagnostics.TraceListener' />
/// class.
/// </para>
/// </summary>
public virtual void Write(string message, string category)
{
if (category == null) Write(message);
else Write(category + ": " + (message == null ? string.Empty : message));
}
/// <summary>
/// <para>
/// Writes a category name and the name of the <paramref name="o" /> parameter to the listener you
/// specify when you inherit from the <see cref='System.Diagnostics.TraceListener' />
/// class.
/// </para>
/// </summary>
public virtual void Write(object o, string category)
{
if (category == null) Write(o);
else Write(o == null ? "" : o.ToString(), category);
}
/// <summary>
/// <para>
/// Writes the indent to the listener you specify when you
/// inherit from the <see cref='System.Diagnostics.TraceListener' />
/// class, and resets the <see cref='TraceListener.NeedIndent' /> property to <see langword='false' />.
/// </para>
/// </summary>
protected virtual void WriteIndent()
{
NeedIndent = false;
for (var i = 0; i < indentLevel; i++)
if (indentSize == 4) Write(" ");
else for (var j = 0; j < indentSize; j++) Write(" ");
}
/// <summary>
/// <para>
/// When overridden in a derived class, writes a message to the listener you specify in
/// the derived class, followed by a line terminator. The default line terminator is a carriage return followed
/// by a line feed (\r\n).
/// </para>
/// </summary>
public abstract void WriteLine(string message);
/// <summary>
/// <para>
/// Writes the name of the <paramref name="o" /> parameter to the listener you specify when you inherit from the
/// <see cref='System.Diagnostics.TraceListener' /> class, followed by a line terminator. The default line
/// terminator is a
/// carriage return followed by a line feed
/// (\r\n).
/// </para>
/// </summary>
public virtual void WriteLine(object o)
{
WriteLine(o == null ? "" : o.ToString());
}
/// <summary>
/// <para>
/// Writes a category name and a message to the listener you specify when you
/// inherit from the <see cref='System.Diagnostics.TraceListener' /> class,
/// followed by a line terminator. The default line terminator is a carriage return followed by a line feed (\r\n).
/// </para>
/// </summary>
public virtual void WriteLine(string message, string category)
{
if (category == null) WriteLine(message);
else WriteLine(category + ": " + (message == null ? string.Empty : message));
}
/// <summary>
/// <para>
/// Writes a category
/// name and the name of the <paramref name="o" />parameter to the listener you
/// specify when you inherit from the <see cref='System.Diagnostics.TraceListener' />
/// class, followed by a line terminator. The default line terminator is a carriage
/// return followed by a line feed (\r\n).
/// </para>
/// </summary>
public virtual void WriteLine(object o, string category)
{
WriteLine(o == null ? "" : o.ToString(), category);
}
}
/// <summary>
/// <para>
/// Directs tracing or debugging output to
/// a <see cref='T:System.IO.TextWriter' /> or to a <see cref='T:System.IO.Stream' />,
/// such as <see cref='F:System.Console.Out' /> or <see cref='T:System.IO.FileStream' />.
/// </para>
/// </summary>
[HostProtection(Synchronization = true)]
public class TextWriterTraceListener : TraceListener
{
private readonly string _carriageReturnAndLineFeedReplacement = ",";
private readonly bool _replaceCarriageReturnsAndLineFeedsFromFieldValues = true;
private string fileName;
internal TextWriter writer;
/// <summary>
/// <para>
/// Initializes a new instance of the <see cref='System.Diagnostics.TextWriterTraceListener' /> class with
/// <see cref='System.IO.TextWriter' />
/// as the output recipient.
/// </para>
/// </summary>
public TextWriterTraceListener()
{
}
/// <summary>
/// <para>
/// Initializes a new instance of the <see cref='System.Diagnostics.TextWriterTraceListener' /> class, using the
/// stream as the recipient of the debugging and tracing output.
/// </para>
/// </summary>
public TextWriterTraceListener(Stream stream) : this(stream, string.Empty)
{
}
/// <summary>
/// <para>
/// Initializes a new instance of the <see cref='System.Diagnostics.TextWriterTraceListener' /> class with the
/// specified name and using the stream as the recipient of the debugging and tracing output.
/// </para>
/// </summary>
public TextWriterTraceListener(Stream stream, string name) : base(name)
{
if (stream == null) throw new ArgumentNullException("stream");
writer = new StreamWriter(stream);
}
/// <summary>
/// <para>
/// Initializes a new instance of the <see cref='System.Diagnostics.TextWriterTraceListener' /> class using the
/// specified writer as recipient of the tracing or debugging output.
/// </para>
/// </summary>
public TextWriterTraceListener(TextWriter writer) : this(writer, string.Empty)
{
}
/// <summary>
/// <para>
/// Initializes a new instance of the <see cref='System.Diagnostics.TextWriterTraceListener' /> class with the
/// specified name and using the specified writer as recipient of the tracing or
/// debugging
/// output.
/// </para>
/// </summary>
public TextWriterTraceListener(TextWriter writer, string name) : base(name)
{
if (writer == null) throw new ArgumentNullException("writer");
this.writer = writer;
}
/// <summary>
/// <para>[To be supplied.]</para>
/// </summary>
[ResourceExposure(ResourceScope.Machine)]
public TextWriterTraceListener(string fileName)
{
this.fileName = fileName;
}
/// <summary>
/// <para>[To be supplied.]</para>
/// </summary>
[ResourceExposure(ResourceScope.Machine)]
public TextWriterTraceListener(string fileName, string name) : base(name)
{
this.fileName = fileName;
}
/// <summary>
/// <para>
/// Indicates the text writer that receives the tracing
/// or debugging output.
/// </para>
/// </summary>
public TextWriter Writer
{
get
{
EnsureWriter();
return writer;
}
set { writer = value; }
}
/// <summary>
/// <para>
/// Closes the <see cref='System.Diagnostics.TextWriterTraceListener.Writer' /> so that it no longer
/// receives tracing or debugging output.
/// </para>
/// </summary>
public override void Close()
{
if (writer != null)
try
{
writer.Close();
}
catch (ObjectDisposedException)
{
}
writer = null;
}
/// <internalonly />
/// <summary>
/// </summary>
protected override void Dispose(bool disposing)
{
try
{
if (disposing)
{
Close();
}
else
{
// clean up resources
if (writer != null)
try
{
writer.Close();
}
catch (ObjectDisposedException)
{
}
writer = null;
}
}
finally
{
base.Dispose(disposing);
}
}
/// <summary>
/// <para>Flushes the output buffer for the <see cref='System.Diagnostics.TextWriterTraceListener.Writer' />.</para>
/// </summary>
public override void Flush()
{
if (!EnsureWriter()) return;
try
{
writer.Flush();
}
catch (ObjectDisposedException)
{
}
}
/// <summary>
/// <para>
/// Writes a message
/// to this instance's <see cref='System.Diagnostics.TextWriterTraceListener.Writer' />.
/// </para>
/// </summary>
public override void Write(string message)
{
if (!EnsureWriter()) return;
if (NeedIndent) WriteIndent();
try
{
writer.Write(message);
}
catch (ObjectDisposedException)
{
}
}
/// <summary>
/// <para>
/// Writes a message
/// to this instance's <see cref='System.Diagnostics.TextWriterTraceListener.Writer' /> followed by a line
/// terminator. The
/// default line terminator is a carriage return followed by a line feed (\r\n).
/// </para>
/// </summary>
public override void WriteLine(string message)
{
if (!EnsureWriter()) return;
if (NeedIndent) WriteIndent();
try
{
writer.WriteLine(message);
NeedIndent = true;
}
catch (ObjectDisposedException)
{
}
}
public virtual void WriteCsvLine(params string[] fields)
{
if (!EnsureWriter()) return;
if (NeedIndent) WriteIndent();
try
{
WriteRecord(writer, fields);
}
catch (ObjectDisposedException)
{
}
}
private void WriteRecord(TextWriter writer, params string[] fields)
{
if (null == fields) return;
for (var i = 0; i < fields.Length; i++)
{
var quotesRequired = fields[i].Contains(",");
var escapeQuotes = fields[i].Contains("\"");
var fieldValue = escapeQuotes ? fields[i].Replace("\"", "\"\"") : fields[i];
if (_replaceCarriageReturnsAndLineFeedsFromFieldValues &&
(fieldValue.Contains("\r") || fieldValue.Contains("\n")))
{
quotesRequired = true;
fieldValue = fieldValue.Replace("\r\n",
_carriageReturnAndLineFeedReplacement);
fieldValue = fieldValue.Replace("\r", _carriageReturnAndLineFeedReplacement);
fieldValue = fieldValue.Replace("\n", _carriageReturnAndLineFeedReplacement);
}
writer.Write("{0}{1}{0}{2}",
quotesRequired || escapeQuotes ? "\"" : string.Empty, fieldValue,
i < fields.Length - 1 ? "," : string.Empty);
}
writer.WriteLine();
}
private static Encoding GetEncodingWithFallback(Encoding encoding)
{
// Clone it and set the "?" replacement fallback
var fallbackEncoding = (Encoding)encoding.Clone();
fallbackEncoding.EncoderFallback = EncoderFallback.ReplacementFallback;
fallbackEncoding.DecoderFallback = DecoderFallback.ReplacementFallback;
return fallbackEncoding;
}
// This uses a machine resource, scoped by the fileName variable.
[ResourceExposure(ResourceScope.None)]
[ResourceConsumption(ResourceScope.Machine, ResourceScope.Machine)]
internal bool EnsureWriter()
{
var ret = true;
if (writer == null)
{
ret = false;
if (fileName == null) return ret;
// StreamWriter by default uses UTF8Encoding which will throw on invalid encoding errors.
// This can cause the internal StreamWriter's state to be irrecoverable. It is bad for tracing
// APIs to throw on encoding errors. Instead, we should provide a "?" replacement fallback
// encoding to substitute illegal chars. For ex, In case of high surrogate character
// D800-DBFF without a following low surrogate character DC00-DFFF
// NOTE: We also need to use an encoding that does't emit BOM whic is StreamWriter's default
var noBOMwithFallback = GetEncodingWithFallback(new UTF8Encoding(false));
// To support multiple appdomains/instances tracing to the same file,
// we will try to open the given file for append but if we encounter
// IO errors, we will prefix the file name with a unique GUID value
// and try one more time
var fullPath = Path.GetFullPath(fileName);
var dirPath = Path.GetDirectoryName(fullPath);
var fileNameOnly = Path.GetFileName(fullPath);
for (var i = 0; i < 2; i++)
try
{
writer = new StreamWriter(fullPath, true, noBOMwithFallback, 4096);
ret = true;
break;
}
catch (IOException)
{
// Should we do this only for ERROR_SHARING_VIOLATION?
//if (InternalResources.MakeErrorCodeFromHR(Marshal.GetHRForException(ioexc)) == InternalResources.ERROR_SHARING_VIOLATION) {
fileNameOnly = Guid.NewGuid() + fileNameOnly;
fullPath = Path.Combine(dirPath, fileNameOnly);
}
catch (UnauthorizedAccessException)
{
//ERROR_ACCESS_DENIED, mostly ACL issues
break;
}
catch (Exception)
{
break;
}
if (!ret) fileName = null;
}
return ret;
}
}
/// <summary>
/// Extends <see cref="TextWriterTraceListener" /> to add formatting capabilities.
/// </summary>
public class FormattedTextWriterTraceListener : TextWriterTraceListener
{
/// <summary>
/// Initializes a new instance of <see cref="FormattedTextWriterTraceListener" />.
/// </summary>
public FormattedTextWriterTraceListener()
{
}
/// <summary>
/// Initializes a new instance of <see cref="FormattedTextWriterTraceListener" /> with a <see cref="Stream" />.
/// </summary>
/// <param name="stream">The stream to write to.</param>
public FormattedTextWriterTraceListener(Stream stream) : base(stream)
{
}
/// <summary>
/// Initializes a new instance of <see cref="FormattedTextWriterTraceListener" /> with a <see cref="TextWriter" />.
/// </summary>
/// <param name="writer">The writer to write to.</param>
public FormattedTextWriterTraceListener(TextWriter writer) : base(writer)
{
}
/// <summary>
/// Initializes a new instance of <see cref="FormattedTextWriterTraceListener" /> with a file name.
/// </summary>
/// <param name="fileName">The file name to write to.</param>
public FormattedTextWriterTraceListener(string fileName)
: base(EnvironmentHelper.ReplaceEnvironmentVariables(fileName))
{
}
/// <summary>
/// Initializes a new named instance of <see cref="FormattedTextWriterTraceListener" /> with a <see cref="Stream" />.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="name">The name.</param>
public FormattedTextWriterTraceListener(Stream stream, string name) : base(stream, name)
{
}
/// <summary>
/// Initializes a new named instance of <see cref="FormattedTextWriterTraceListener" /> with a
/// <see cref="TextWriter" />.
/// </summary>
/// <param name="writer">The writer to write to.</param>
/// <param name="name">The name.</param>
public FormattedTextWriterTraceListener(TextWriter writer, string name)
: base(writer, name)
{
}
/// <summary>
/// Initializes a new named instance of <see cref="FormattedTextWriterTraceListener" /> with a
/// <see cref="ILogFormatter" /> and a file name.
/// </summary>
/// <param name="fileName">The file name to write to.</param>
/// <param name="name">The name.</param>
public FormattedTextWriterTraceListener(string fileName, string name)
: base(EnvironmentHelper.ReplaceEnvironmentVariables(fileName), name)
{
}
public sealed override void Write(object o)
{
base.Write(o);
}
}
/// <summary>
/// A <see cref="TraceListener" /> that writes to a flat file, formatting the output with an
/// <see cref="ILogFormatter" />.
/// </summary>
public class FlatFileTraceListener : FormattedTextWriterTraceListener
{
private readonly Func<string> footer;
private readonly Func<string> header;
/// <summary>
/// Initializes a new instance of <see cref="FlatFileTraceListener" />.
/// </summary>
public FlatFileTraceListener()
{
}
/// <summary>
/// Initializes a new instance of <see cref="FlatFileTraceListener" /> with a <see cref="FileStream" />.
/// </summary>
/// <param name="stream">The file stream.</param>
public FlatFileTraceListener(FileStream stream) : base(stream)
{
}
/// <summary>
/// Initializes a new instance of <see cref="FlatFileTraceListener" /> with a <see cref="StreamWriter" />.
/// </summary>
/// <param name="writer">The stream writer.</param>
public FlatFileTraceListener(StreamWriter writer) : base(writer)
{
}
/// <summary>
/// Initializes a new instance of <see cref="FlatFileTraceListener" /> with a file name.
/// </summary>
/// <param name="fileName">The file name.</param>
public FlatFileTraceListener(string fileName) : base(fileName)
{
}
/// <summary>
/// Initializes a new instance of <see cref="FlatFileTraceListener" /> with a file name, a header, and a footer.
/// </summary>
/// <param name="fileName">The file stream.</param>
/// <param name="header">The header.</param>
/// <param name="footer">The footer.</param>
public FlatFileTraceListener(string fileName, Func<string> header, Func<string> footer)
: base(fileName)
{
this.header = header;
this.footer = footer;
}
/// <summary>
/// Initializes a new name instance of <see cref="FlatFileTraceListener" /> with a <see cref="FileStream" />.
/// </summary>
/// <param name="stream">The file stream.</param>
/// <param name="name">The name.</param>
public FlatFileTraceListener(FileStream stream, string name) : base(stream, name)
{
}
/// <summary>
/// Initializes a new named instance of <see cref="FlatFileTraceListener" /> with a <see cref="StreamWriter" />.
/// </summary>
/// <param name="writer">The stream writer.</param>
/// <param name="name">The name.</param>
public FlatFileTraceListener(StreamWriter writer, string name) : base(writer, name)
{
}
/// <summary>
/// Initializes a new named instance of <see cref="FlatFileTraceListener" /> with a file name.
/// </summary>
/// <param name="fileName">The file name.</param>
/// <param name="name">The name.</param>
public FlatFileTraceListener(string fileName, string name) : base(fileName, name)
{
}
public override void WriteLine(string message)
{
if (header != null) base.WriteLine(header());
base.WriteLine(message);
if (footer != null) base.WriteLine(footer());
}
}
/// <summary>
/// Performs logging to a file and rolls the output file when either time or size thresholds are
/// exceeded.
/// </summary>
/// <remarks>
/// Logging always occurs to the configured file name, and when roll occurs a new rolled file name is calculated
/// by adding the timestamp pattern to the configured file name.
/// <para />
/// The need of rolling is calculated before performing a logging operation, so even if the thresholds are exceeded
/// roll will not occur until a new entry is logged.
/// <para />
/// Both time and size thresholds can be configured, and when the first of them occurs both will be reset.
/// <para />
/// The elapsed time is calculated from the creation date of the logging file.
/// </remarks>
public class RollingFlatFileTraceListener : FlatFileTraceListener
{
private readonly string archivedFolderPattern;
private readonly int maxArchivedFiles;
private readonly RollFileExistsBehavior rollFileExistsBehavior;
private readonly RollInterval rollInterval;
private readonly int rollSizeInBytes;
private readonly string timeStampPattern;
/// <summary>
/// Initializes a new instance of <see cref="RollingFlatFileTraceListener" />
/// </summary>
/// <param name="fileName">The filename where the entries will be logged.</param>
/// <param name="header">The header to add before logging an entry.</param>
/// <param name="footer">The footer to add after logging an entry.</param>
/// <param name="formatter">The formatter.</param>
/// <param name="rollSizeKB">The maxium file size (KB) before rolling.</param>
/// <param name="timeStampPattern">The date format that will be appended to the new roll file.</param>
/// <param name="archivedFolderPattern">The archived folder pattern.</param>
/// <param name="rollFileExistsBehavior">Expected behavior that will be used when the roll file has to be created.</param>
/// <param name="rollInterval">The time interval that makes the file rolles.</param>
public RollingFlatFileTraceListener(
string fileName, Func<string> header, Func<string> footer, int rollSizeKB,
string timeStampPattern, string archivedFolderPattern,
RollFileExistsBehavior rollFileExistsBehavior, RollInterval rollInterval)
: this(
fileName, header, footer, rollSizeKB, timeStampPattern, archivedFolderPattern,
rollFileExistsBehavior, rollInterval, 0)
{
}
/// <summary>
/// Initializes a new instance of <see cref="RollingFlatFileTraceListener" />
/// </summary>
/// <param name="fileName">The filename where the entries will be logged.</param>
/// <param name="header">The header to add before logging an entry.</param>
/// <param name="footer">The footer to add after logging an entry.</param>
/// <param name="formatter">The formatter.</param>
/// <param name="rollSizeKB">The maxium file size (KB) before rolling.</param>
/// <param name="timeStampPattern">The date format that will be appended to the new roll file.</param>
/// <param name="archivedFolderPattern">The archived folder pattern.</param>
/// <param name="rollFileExistsBehavior">Expected behavior that will be used when the roll file has to be created.</param>
/// <param name="rollInterval">The time interval that makes the file rolles.</param>
/// <param name="maxArchivedFiles">The maximum number of archived files to keep.</param>
public RollingFlatFileTraceListener(
string fileName, Func<string> header, Func<string> footer, int rollSizeKB,
string timeStampPattern, string archivedFolderPattern,
RollFileExistsBehavior rollFileExistsBehavior, RollInterval rollInterval,
int maxArchivedFiles) : base(fileName, header, footer)
{
this.archivedFolderPattern = archivedFolderPattern;
rollSizeInBytes = rollSizeKB * 1024;
this.timeStampPattern = timeStampPattern;
this.rollFileExistsBehavior = rollFileExistsBehavior;
this.rollInterval = rollInterval;
this.maxArchivedFiles = maxArchivedFiles;
RollingHelper = new StreamWriterRollingHelper(this);
}
/// <summary>
/// Gets the <see cref="StreamWriterRollingHelper" /> for the flat file.
/// </summary>
/// <value>
/// The <see cref="StreamWriterRollingHelper" /> for the flat file.
/// </value>
public StreamWriterRollingHelper RollingHelper { get; }
public override void WriteLine(object o)
{
RollingHelper.RollIfNecessary();
base.WriteLine(o);
}
public override void Write(string message)
{
RollingHelper.RollIfNecessary();
base.Write(message);
}
public override void WriteCsvLine(params string[] fields)
{
RollingHelper.RollIfNecessary();
base.WriteCsvLine(fields);
}
/// <summary>
/// A data time provider.
/// </summary>
public class DateTimeProvider
{
/// <summary>
/// Gets the current data time.
/// </summary>
/// <value>
/// The current data time.
/// </value>
public virtual DateTimeOffset CurrentDateTime
{
get { return DateTimeOffset.UtcNow; }
}
}
/// <summary>
/// Encapsulates the logic to perform rolls.
/// </summary>
/// <remarks>
/// If no rolling behavior has been configured no further processing will be performed.
/// </remarks>
public sealed class StreamWriterRollingHelper
{
/// <summary>
/// The trace listener for which rolling is being managed.
/// </summary>
private readonly RollingFlatFileTraceListener owner;
/// <summary>
/// A flag indicating whether at least one rolling criteria has been configured.
/// </summary>
private readonly bool performsRolling;
private DateTimeProvider dateTimeProvider;
/// <summary>
/// A tally keeping writer used when file size rolling is configured.
/// <para />
/// The original stream writer from the base trace listener will be replaced with
/// this listener.
/// </summary>
private TallyKeepingFileStreamWriter managedWriter;
/// <summary>
/// Initialize a new instance of the <see cref="StreamWriterRollingHelper" /> class with a
/// <see cref="RollingFlatFileTraceListener" />.
/// </summary>
/// <param name="owner">The <see cref="RollingFlatFileTraceListener" /> to use.</param>
public StreamWriterRollingHelper(RollingFlatFileTraceListener owner)
{
this.owner = owner;
dateTimeProvider = new DateTimeProvider();
performsRolling = (this.owner.rollInterval != RollInterval.None) ||
(this.owner.rollSizeInBytes > 0);
}
/// <summary>
/// Gets the provider for the current date. Necessary for unit testing.
/// </summary>
/// <value>
/// The provider for the current date. Necessary for unit testing.
/// </value>
public DateTimeProvider DateTimeProvider
{
set { dateTimeProvider = value; }
}
/// <summary>
/// Gets the next date when date based rolling should occur if configured.
/// </summary>
/// <value>
/// The next date when date based rolling should occur if configured.
/// </value>
public DateTimeOffset? NextRollDateTime { get; private set; }
/// <summary>
/// Calculates the next roll date for the file.
/// </summary>
/// <param name="dateTime">The new date.</param>
/// <returns>The new date time to use.</returns>
public DateTimeOffset CalculateNextRollDate(DateTimeOffset dateTime)
{
switch (owner.rollInterval)
{
case RollInterval.Minute:
return dateTime.AddMinutes(1);
case RollInterval.Hour:
return dateTime.AddHours(1);
case RollInterval.Day:
return dateTime.AddDays(1);
case RollInterval.Week:
return dateTime.AddDays(7);
case RollInterval.Month:
return dateTime.AddMonths(1);
case RollInterval.Year:
return dateTime.AddYears(1);
case RollInterval.Midnight:
return dateTime.AddDays(1).Date;
default:
return DateTimeOffset.MaxValue;
}
}
/// <summary>
/// Checks whether rolling should be performed, and returns the date to use when performing the roll.
/// </summary>
/// <returns>The date roll to use if performing a roll, or <see langword="null" /> if no rolling should occur.</returns>
/// <remarks>
/// Defer request for the roll date until it is necessary to avoid overhead.
/// <para />
/// Information used for rolling checks should be set by now.
/// </remarks>
public DateTimeOffset? CheckIsRollNecessary()
{
// check for size roll, if enabled.
if ((owner.rollSizeInBytes > 0) && (managedWriter != null) &&
(managedWriter.Tally > owner.rollSizeInBytes))
return dateTimeProvider.CurrentDateTime;
// check for date roll, if enabled.
var currentDateTime = dateTimeProvider.CurrentDateTime;
if ((owner.rollInterval != RollInterval.None) && (NextRollDateTime != null) &&
(currentDateTime.CompareTo(NextRollDateTime.Value) >= 0))
return currentDateTime;
// no roll is necessary, return a null roll date
return null;
}
/// <summary>
/// Gets the file name to use for archiving the file.
/// </summary>
/// <param name="actualFileName">The actual file name.</param>
/// <param name="currentDateTime">The current date and time.</param>
/// <returns>The new file name.</returns>
public string ComputeArchiveFileName(
string actualFileName, DateTimeOffset currentDateTime)
{
var directory = Path.GetDirectoryName(actualFileName);
if (!string.IsNullOrWhiteSpace(owner.archivedFolderPattern))
{
var rollingDirectory = Path.Combine(directory,
DateTimeOffset.UtcNow.ToString(owner.archivedFolderPattern));
try
{
if (!Directory.Exists(rollingDirectory))
Directory.CreateDirectory(rollingDirectory);
directory = rollingDirectory;
}
catch (Exception)
{
//Debug.WriteLine(ex);
}
}
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(actualFileName);
var extension = Path.GetExtension(actualFileName);
var fileNameBuilder = new StringBuilder(fileNameWithoutExtension);
if (!string.IsNullOrEmpty(owner.timeStampPattern))
{
fileNameBuilder.Append('.');
fileNameBuilder.Append(currentDateTime.ToString(owner.timeStampPattern,
CultureInfo.InvariantCulture));
}
if (owner.rollFileExistsBehavior == RollFileExistsBehavior.Increment)
{
// look for max sequence for date
var newSequence =
FindMaxSequenceNumber(directory, fileNameBuilder.ToString(), extension) +
1;
fileNameBuilder.Append('.');
fileNameBuilder.Append(newSequence.ToString(CultureInfo.InvariantCulture));
}
fileNameBuilder.Append(extension);
return Path.Combine(directory, fileNameBuilder.ToString());
}
/// <summary>
/// Finds the max sequence number for a log file.
/// </summary>
/// <param name="directoryName">The directory to scan.</param>
/// <param name="fileName">The file name.</param>
/// <param name="extension">The extension to use.</param>
/// <returns>The next sequence number.</returns>
public static int FindMaxSequenceNumber(
string directoryName, string fileName, string extension)
{
var existingFiles = Directory.GetFiles(directoryName,
string.Format("{0}*{1}", fileName, extension));
var maxSequence = 0;
var regex =
new Regex(string.Format(@"{0}\.(?<sequence>\d+){1}$", fileName, extension));
for (var i = 0; i < existingFiles.Length; i++)
{
var sequenceMatch = regex.Match(existingFiles[i]);
if (sequenceMatch.Success)
{
var currentSequence = 0;
var sequenceInFile = sequenceMatch.Groups["sequence"].Value;
if (!int.TryParse(sequenceInFile, out currentSequence))
continue; // very unlikely
if (currentSequence > maxSequence) maxSequence = currentSequence;
}
}
return maxSequence;
}
private static Encoding GetEncodingWithFallback()
{
var encoding = (Encoding)new UTF8Encoding(false).Clone();
encoding.EncoderFallback = EncoderFallback.ReplacementFallback;
encoding.DecoderFallback = DecoderFallback.ReplacementFallback;
return encoding;
}
/// <summary>
/// Perform the roll for the next date.
/// </summary>
/// <param name="rollDateTime">The roll date.</param>
public void PerformRoll(DateTimeOffset rollDateTime)
{
var actualFileName =
((FileStream)((StreamWriter)owner.Writer).BaseStream).Name;
if ((owner.rollFileExistsBehavior == RollFileExistsBehavior.Overwrite) &&
string.IsNullOrEmpty(owner.timeStampPattern))
{
// no roll will be actually performed: no timestamp pattern is available, and
// the roll behavior is overwrite, so the original file will be truncated
owner.Writer.Close();
File.WriteAllText(actualFileName, string.Empty);
}
else
{
// calculate archive name
var archiveFileName = ComputeArchiveFileName(actualFileName, rollDateTime);
// close file
owner.Writer.Close();
// move file
SafeMove(actualFileName, archiveFileName, rollDateTime);
// purge if necessary
PurgeArchivedFiles(archiveFileName);
}
// update writer - let TWTL open the file as needed to keep consistency
owner.Writer = null;
managedWriter = null;
NextRollDateTime = null;
UpdateRollingInformationIfNecessary();
}
/// <summary>
/// Rolls the file if necessary.
/// </summary>
public void RollIfNecessary()
{
if (!performsRolling) return;
if (!UpdateRollingInformationIfNecessary()) return;
DateTimeOffset? rollDateTime;
if ((rollDateTime = CheckIsRollNecessary()) != null)
PerformRoll(rollDateTime.Value);
}
private void SafeMove(
string actualFileName, string archiveFileName, DateTimeOffset currentDateTime)
{
try
{
if (File.Exists(archiveFileName)) File.Delete(archiveFileName);
// take care of tunneling issues http://support.microsoft.com/kb/172190
File.SetCreationTime(actualFileName, currentDateTime.UtcDateTime);
File.Move(actualFileName, archiveFileName);
}
catch (IOException)
{
// catch errors and attempt move to a new file with a GUID
archiveFileName = archiveFileName + Guid.NewGuid();
try
{
File.Move(actualFileName, archiveFileName);
}
catch (IOException)
{
}
}
}
private void PurgeArchivedFiles(string archiveFileName)
{
if (owner.maxArchivedFiles > 0)
{
var directoryName = Path.GetDirectoryName(archiveFileName);
var fileName = Path.GetFileName(archiveFileName);
new RollingFlatFilePurger(directoryName, fileName, owner.maxArchivedFiles)
.Purge();
}
}
/// <summary>
/// Updates bookeeping information necessary for rolling, as required by the specified
/// rolling configuration.
/// </summary>
/// <returns>true if update was successful, false if an error occurred.</returns>
public bool UpdateRollingInformationIfNecessary()
{
StreamWriter currentWriter = null;
// replace writer with the tally keeping version if necessary for size rolling
if ((owner.rollSizeInBytes > 0) && (managedWriter == null))
{
currentWriter = owner.Writer as StreamWriter;
if (currentWriter == null) return false;
var actualFileName = ((FileStream)currentWriter.BaseStream).Name;
currentWriter.Close();
FileStream fileStream = null;
try
{
fileStream = File.Open(actualFileName, FileMode.Append, FileAccess.Write,
FileShare.Read);
managedWriter = new TallyKeepingFileStreamWriter(fileStream,
GetEncodingWithFallback());
}
catch (Exception)
{
// there's a slight chance of error here - abort if this occurs and just let TWTL handle it without attempting to roll
return false;
}
owner.Writer = managedWriter;
}
// compute the next roll date if necessary
if ((owner.rollInterval != RollInterval.None) && (NextRollDateTime == null))
try
{
// casting should be safe at this point - only file stream writers can be the writers for the owner trace listener.
// it should also happen rarely
NextRollDateTime =
CalculateNextRollDate(
File.GetCreationTime(
((FileStream)((StreamWriter)owner.Writer).BaseStream).Name));
}
catch (Exception)
{
NextRollDateTime = DateTimeOffset.MaxValue;
// disable rolling if not date could be retrieved.
// there's a slight chance of error here - abort if this occurs and just let TWTL handle it without attempting to roll
return false;
}
return true;
}
}
/// <summary>
/// Represents a file stream writer that keeps a tally of the length of the file.
/// </summary>
public sealed class TallyKeepingFileStreamWriter : StreamWriter
{
/// <summary>
/// Initialize a new instance of the <see cref="TallyKeepingFileStreamWriter" /> class with a <see cref="FileStream" />
/// .
/// </summary>
/// <param name="stream">The <see cref="FileStream" /> to write to.</param>
public TallyKeepingFileStreamWriter(FileStream stream) : base(stream)
{
Tally = stream.Length;
}
/// <summary>
/// Initialize a new instance of the <see cref="TallyKeepingFileStreamWriter" /> class with a <see cref="FileStream" />
/// .
/// </summary>
/// <param name="stream">The <see cref="FileStream" /> to write to.</param>
/// <param name="encoding">The <see cref="Encoding" /> to use.</param>
public TallyKeepingFileStreamWriter(FileStream stream, Encoding encoding)
: base(stream, encoding)
{
Tally = stream.Length;
}
/// <summary>
/// Gets the tally of the length of the string.
/// </summary>
/// <value>
/// The tally of the length of the string.
/// </value>
public long Tally { get; private set; }
/// <summary>
/// Writes a character to the stream.
/// </summary>
/// <param name="value">The character to write to the text stream. </param>
/// <exception cref="T:System.ObjectDisposedException">
/// <see cref="P:System.IO.StreamWriter.AutoFlush"></see> is true or the
/// <see cref="T:System.IO.StreamWriter"></see> buffer is full, and current writer is closed.
/// </exception>
/// <exception cref="T:System.NotSupportedException">
/// <see cref="P:System.IO.StreamWriter.AutoFlush"></see> is true or the
/// <see cref="T:System.IO.StreamWriter"></see> buffer is full, and the contents of the buffer cannot be written to the
/// underlying fixed size stream because the <see cref="T:System.IO.StreamWriter"></see> is at the end the stream.
/// </exception>
/// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
/// <filterpriority>1</filterpriority>
public override void Write(char value)
{
base.Write(value);
Tally += Encoding.GetByteCount(new[] { value });
}
/// <summary>
/// Writes a character array to the stream.
/// </summary>
/// <param name="buffer">A character array containing the data to write. If buffer is null, nothing is written. </param>
/// <exception cref="T:System.ObjectDisposedException">
/// <see cref="P:System.IO.StreamWriter.AutoFlush"></see> is true or the
/// <see cref="T:System.IO.StreamWriter"></see> buffer is full, and current writer is closed.
/// </exception>
/// <exception cref="T:System.NotSupportedException">
/// <see cref="P:System.IO.StreamWriter.AutoFlush"></see> is true or the
/// <see cref="T:System.IO.StreamWriter"></see> buffer is full, and the contents of the buffer cannot be written to the
/// underlying fixed size stream because the <see cref="T:System.IO.StreamWriter"></see> is at the end the stream.
/// </exception>
/// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
/// <filterpriority>1</filterpriority>
public override void Write(char[] buffer)
{
base.Write(buffer);
Tally += Encoding.GetByteCount(buffer);
}
/// <summary>
/// Writes a subarray of characters to the stream.
/// </summary>
/// <param name="count">The number of characters to read from buffer. </param>
/// <param name="buffer">A character array containing the data to write. </param>
/// <param name="index">The index into buffer at which to begin writing. </param>
/// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
/// <exception cref="T:System.ObjectDisposedException">
/// <see cref="P:System.IO.StreamWriter.AutoFlush"></see> is true or the
/// <see cref="T:System.IO.StreamWriter"></see> buffer is full, and current writer is closed.
/// </exception>
/// <exception cref="T:System.NotSupportedException">
/// <see cref="P:System.IO.StreamWriter.AutoFlush"></see> is true or the
/// <see cref="T:System.IO.StreamWriter"></see> buffer is full, and the contents of the buffer cannot be written to the
/// underlying fixed size stream because the <see cref="T:System.IO.StreamWriter"></see> is at the end the stream.
/// </exception>
/// <exception cref="T:System.ArgumentOutOfRangeException">index or count is negative. </exception>
/// <exception cref="T:System.ArgumentException">The buffer length minus index is less than count. </exception>
/// <exception cref="T:System.ArgumentNullException">buffer is null. </exception>
/// <filterpriority>1</filterpriority>
public override void Write(char[] buffer, int index, int count)
{
base.Write(buffer, index, count);
Tally += Encoding.GetByteCount(buffer, index, count);
}
/// <summary>
/// Writes a string to the stream.
/// </summary>
/// <param name="value">The string to write to the stream. If value is null, nothing is written. </param>
/// <exception cref="T:System.ObjectDisposedException">
/// <see cref="P:System.IO.StreamWriter.AutoFlush"></see> is true or the
/// <see cref="T:System.IO.StreamWriter"></see> buffer is full, and current writer is closed.
/// </exception>
/// <exception cref="T:System.NotSupportedException">
/// <see cref="P:System.IO.StreamWriter.AutoFlush"></see> is true or the
/// <see cref="T:System.IO.StreamWriter"></see> buffer is full, and the contents of the buffer cannot be written to the
/// underlying fixed size stream because the <see cref="T:System.IO.StreamWriter"></see> is at the end the stream.
/// </exception>
/// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
/// <filterpriority>1</filterpriority>
public override void Write(string value)
{
base.Write(value);
Tally += Encoding.GetByteCount(value);
}
}
}
/// <summary>
/// Provides tracing services through a set of <see cref="TraceListener" />s.
/// </summary>
public class LogSource : IDisposable
{
/// <summary>
/// Default Auto Flush property for the LogSource instance.
/// </summary>
public const bool DefaultAutoFlushProperty = true;
public LogSource()
{
var listener = new RollingFlatFileTraceListener("App_Data/trace.log",
() => DateTimeOffset.Now.ToString("'>>'yyyy'-'MM'-'dd'T'HH':'mm':'ss.fffffffK"),
null, 5120, "yyyyMMddHHmm", "'archived'yyyyMMdd",
RollFileExistsBehavior.Increment, RollInterval.Day);
Listeners = new TraceListener[] { listener };
}
/// <summary>
/// Initializes a new instance of the <see cref="LogSource" /> class with a name, a collection of
/// <see cref="TraceListener" />s, a level and the auto flush.
/// </summary>
/// <param name="name">The name for the instance.</param>
/// <param name="traceListeners">The collection of <see cref="TraceListener" />s.</param>
/// <param name="level">The <see cref="SourceLevels" /> value.</param>
/// <param name="autoFlush">If Flush should be called on the Listeners after every write.</param>
public LogSource(TraceListener[] traceListeners, bool autoFlush)
{
Listeners = traceListeners;
AutoFlush = autoFlush;
}
/// <summary>
/// Gets the collection of trace listeners for the <see cref="LogSource" /> instance.
/// </summary>
/// <value>The listeners.</value>
public TraceListener[] Listeners { get; }
/// <summary>
/// Gets or sets the <see cref="AutoFlush" /> values for the <see cref="LogSource" /> instance.
/// </summary>
public bool AutoFlush { get; set; } = DefaultAutoFlushProperty;
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public void WriteLine(object message)
{
WriteLine(message, null);
}
public void WriteLine(object message, string category)
{
foreach (var item in Listeners)
{
var listener = item;
try
{
if (!listener.IsThreadSafe) Monitor.Enter(listener);
listener.WriteLine(message, category);
if (AutoFlush) listener.Flush();
}
finally
{
if (!listener.IsThreadSafe) Monitor.Exit(listener);
}
}
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> if the method is being called from the <see cref="Dispose()" /> method.
/// <see langword="false" /> if it is being called from within the object finalizer.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing) foreach (var listener in Listeners) listener.Dispose();
}
/// <summary>
/// Releases resources for the <see cref="LogSource" /> instance before garbage collection.
/// </summary>
~LogSource()
{
Dispose(false);
}
}
/// <summary>
/// Class to write data to a csv file
/// </summary>
public sealed class CsvWriter : IDisposable
{
#region Members
private StreamWriter _streamWriter;
#endregion Members
#region Properties
/// <summary>
/// Gets or sets whether carriage returns and line feeds should be removed from
/// field values, the default is true
/// </summary>
public bool ReplaceCarriageReturnsAndLineFeedsFromFieldValues { get; set; } = true;
/// <summary>
/// Gets or sets what the carriage return and line feed replacement characters should be
/// </summary>
public string CarriageReturnAndLineFeedReplacement { get; set; } = ",";
#endregion Properties
#region Methods
#region CsvFile write methods
/// <summary>
/// Writes csv content to a file
/// </summary>
/// <param name="csvFile">CsvFile</param>
/// <param name="filePath">File path</param>
public void WriteCsv(CsvFile csvFile, string filePath)
{
WriteCsv(csvFile, filePath, null);
}
/// <summary>
/// Writes csv content to a file
/// </summary>
/// <param name="csvFile">CsvFile</param>
/// <param name="filePath">File path</param>
/// <param name="encoding">Encoding</param>
public void WriteCsv(CsvFile csvFile, string filePath, Encoding encoding)
{
if (File.Exists(filePath)) File.Delete(filePath);
using (var writer = new StreamWriter(filePath, false, encoding ?? Encoding.Default))
{
WriteToStream(csvFile, writer);
writer.Flush();
writer.Close();
}
}
/// <summary>
/// Writes csv content to a stream
/// </summary>
/// <param name="csvFile">CsvFile</param>
/// <param name="stream">Stream</param>
public void WriteCsv(CsvFile csvFile, Stream stream)
{
WriteCsv(csvFile, stream, null);
}
/// <summary>
/// Writes csv content to a stream
/// </summary>
/// <param name="csvFile">CsvFile</param>
/// <param name="stream">Stream</param>
/// <param name="encoding">Encoding</param>
public void WriteCsv(CsvFile csvFile, Stream stream, Encoding encoding)
{
stream.Position = 0;
_streamWriter = new StreamWriter(stream, encoding ?? Encoding.Default);
WriteToStream(csvFile, _streamWriter);
_streamWriter.Flush();
stream.Position = 0;
}
/// <summary>
/// Writes csv content to a string
/// </summary>
/// <param name="csvFile">CsvFile</param>
/// <param name="encoding">Encoding</param>
/// <returns>Csv content in a string</returns>
public string WriteCsv(CsvFile csvFile, Encoding encoding)
{
var content = string.Empty;
using (var memoryStream = new MemoryStream())
{
using (var writer = new StreamWriter(memoryStream, encoding ?? Encoding.Default)
)
{
WriteToStream(csvFile, writer);
writer.Flush();
memoryStream.Position = 0;
using (
var reader = new StreamReader(memoryStream, encoding ?? Encoding.Default)
)
{
content = reader.ReadToEnd();
writer.Close();
reader.Close();
memoryStream.Close();
}
}
}
return content;
}
#endregion CsvFile write methods
#region DataTable write methods
/// <summary>
/// Writes a DataTable to a file
/// </summary>
/// <param name="dataTable">DataTable</param>
/// <param name="filePath">File path</param>
public void WriteCsv(DataTable dataTable, string filePath)
{
WriteCsv(dataTable, filePath, null);
}
/// <summary>
/// Writes a DataTable to a file
/// </summary>
/// <param name="dataTable">DataTable</param>
/// <param name="filePath">File path</param>
/// <param name="encoding">Encoding</param>
public void WriteCsv(DataTable dataTable, string filePath, Encoding encoding)
{
if (File.Exists(filePath)) File.Delete(filePath);
using (var writer = new StreamWriter(filePath, false, encoding ?? Encoding.Default))
{
WriteToStream(dataTable, writer);
writer.Flush();
writer.Close();
}
}
/// <summary>
/// Writes a DataTable to a stream
/// </summary>
/// <param name="dataTable">DataTable</param>
/// <param name="stream">Stream</param>
public void WriteCsv(DataTable dataTable, Stream stream)
{
WriteCsv(dataTable, stream, null);
}
/// <summary>
/// Writes a DataTable to a stream
/// </summary>
/// <param name="dataTable">DataTable</param>
/// <param name="stream">Stream</param>
/// <param name="encoding">Encoding</param>
public void WriteCsv(DataTable dataTable, Stream stream, Encoding encoding)
{
stream.Position = 0;
_streamWriter = new StreamWriter(stream, encoding ?? Encoding.Default);
WriteToStream(dataTable, _streamWriter);
_streamWriter.Flush();
stream.Position = 0;
}
/// <summary>
/// Writes the DataTable to a string
/// </summary>
/// <param name="dataTable">DataTable</param>
/// <param name="encoding">Encoding</param>
/// <returns>Csv content in a string</returns>
public string WriteCsv(DataTable dataTable, Encoding encoding)
{
var content = string.Empty;
using (var memoryStream = new MemoryStream())
{
using (var writer = new StreamWriter(memoryStream, encoding ?? Encoding.Default)
)
{
WriteToStream(dataTable, writer);
writer.Flush();
memoryStream.Position = 0;
using (
var reader = new StreamReader(memoryStream, encoding ?? Encoding.Default)
)
{
content = reader.ReadToEnd();
writer.Close();
reader.Close();
memoryStream.Close();
}
}
}
return content;
}
#endregion DataTable write methods
/// <summary>
/// Writes the Csv File
/// </summary>
/// <param name="csvFile">CsvFile</param>
/// <param name="writer">TextWriter</param>
private void WriteToStream(CsvFile csvFile, TextWriter writer)
{
if (csvFile.Headers.Count > 0) WriteRecord(csvFile.Headers, writer);
csvFile.Records.ForEach(record => WriteRecord(record.Fields, writer));
}
/// <summary>
/// Writes the Csv File
/// </summary>
/// <param name="dataTable">DataTable</param>
/// <param name="writer">TextWriter</param>
private void WriteToStream(DataTable dataTable, TextWriter writer)
{
var fields =
(from DataColumn column in dataTable.Columns select column.ColumnName).ToList();
WriteRecord(fields, writer);
foreach (DataRow row in dataTable.Rows)
{
fields.Clear();
fields.AddRange(row.ItemArray.Select(o => o.ToString()));
WriteRecord(fields, writer);
}
}
/// <summary>
/// Writes the record to the underlying stream
/// </summary>
/// <param name="fields">Fields</param>
/// <param name="writer">TextWriter</param>
private void WriteRecord(IList<string> fields, TextWriter writer)
{
for (var i = 0; i < fields.Count; i++)
{
var quotesRequired = fields[i].Contains(",");
var escapeQuotes = fields[i].Contains("\"");
var fieldValue = escapeQuotes ? fields[i].Replace("\"", "\"\"") : fields[i];
if (ReplaceCarriageReturnsAndLineFeedsFromFieldValues &&
(fieldValue.Contains("\r") || fieldValue.Contains("\n")))
{
quotesRequired = true;
fieldValue = fieldValue.Replace("\r\n", CarriageReturnAndLineFeedReplacement);
fieldValue = fieldValue.Replace("\r", CarriageReturnAndLineFeedReplacement);
fieldValue = fieldValue.Replace("\n", CarriageReturnAndLineFeedReplacement);
}
writer.Write("{0}{1}{0}{2}",
quotesRequired || escapeQuotes ? "\"" : string.Empty, fieldValue,
i < fields.Count - 1 ? "," : string.Empty);
}
writer.WriteLine();
}
/// <summary>
/// Disposes of all unmanaged resources
/// </summary>
public void Dispose()
{
if (_streamWriter == null) return;
_streamWriter.Close();
_streamWriter.Dispose();
}
#endregion Methods
}
/// <summary>
/// Class to read csv content from various sources
/// </summary>
public sealed class CsvReader : IDisposable
{
#region Enums
/// <summary>
/// Type enum
/// </summary>
private enum Type
{
File,
Stream,
String
}
#endregion Enums
#region Members
private FileStream _fileStream;
private Stream _stream;
private StreamReader _streamReader;
private StreamWriter _streamWriter;
private Stream _memoryStream;
private Encoding _encoding;
private readonly StringBuilder _columnBuilder = new StringBuilder(100);
private readonly Type _type = Type.File;
#endregion Members
#region Properties
/// <summary>
/// Gets or sets whether column values should be trimmed
/// </summary>
public bool TrimColumns { get; set; }
/// <summary>
/// Gets or sets whether the csv file has a header row
/// </summary>
public bool HasHeaderRow { get; set; }
/// <summary>
/// Returns a collection of fields or null if no record has been read
/// </summary>
public List<string> Fields { get; private set; }
/// <summary>
/// Gets the field count or returns null if no fields have been read
/// </summary>
public int? FieldCount
{
get { return Fields != null ? Fields.Count : (int?)null; }
}
#endregion Properties
#region Constructors
/// <summary>
/// Initialises the reader to work from a file
/// </summary>
/// <param name="filePath">File path</param>
public CsvReader(string filePath)
{
_type = Type.File;
Initialise(filePath, Encoding.Default);
}
/// <summary>
/// Initialises the reader to work from a file
/// </summary>
/// <param name="filePath">File path</param>
/// <param name="encoding">Encoding</param>
public CsvReader(string filePath, Encoding encoding)
{
_type = Type.File;
Initialise(filePath, encoding);
}
/// <summary>
/// Initialises the reader to work from an existing stream
/// </summary>
/// <param name="stream">Stream</param>
public CsvReader(Stream stream)
{
_type = Type.Stream;
Initialise(stream, Encoding.Default);
}
/// <summary>
/// Initialises the reader to work from an existing stream
/// </summary>
/// <param name="stream">Stream</param>
/// <param name="encoding">Encoding</param>
public CsvReader(Stream stream, Encoding encoding)
{
_type = Type.Stream;
Initialise(stream, encoding);
}
/// <summary>
/// Initialises the reader to work from a csv string
/// </summary>
/// <param name="encoding"></param>
/// <param name="csvContent"></param>
public CsvReader(Encoding encoding, string csvContent)
{
_type = Type.String;
Initialise(encoding, csvContent);
}
#endregion Constructors
#region Methods
/// <summary>
/// Initialises the class to use a file
/// </summary>
/// <param name="filePath"></param>
/// <param name="encoding"></param>
private void Initialise(string filePath, Encoding encoding)
{
if (!File.Exists(filePath))
throw new FileNotFoundException(string.Format("The file '{0}' does not exist.",
filePath));
_fileStream = File.OpenRead(filePath);
Initialise(_fileStream, encoding);
}
/// <summary>
/// Initialises the class to use a stream
/// </summary>
/// <param name="stream"></param>
/// <param name="encoding"></param>
private void Initialise(Stream stream, Encoding encoding)
{
if (stream == null) throw new ArgumentNullException("The supplied stream is null.");
_stream = stream;
_stream.Position = 0;
_encoding = encoding ?? Encoding.Default;
_streamReader = new StreamReader(_stream, _encoding);
}
/// <summary>
/// Initialies the class to use a string
/// </summary>
/// <param name="encoding"></param>
/// <param name="csvContent"></param>
private void Initialise(Encoding encoding, string csvContent)
{
if (csvContent == null)
throw new ArgumentNullException("The supplied csvContent is null.");
_encoding = encoding ?? Encoding.Default;
_memoryStream = new MemoryStream(csvContent.Length);
_streamWriter = new StreamWriter(_memoryStream);
_streamWriter.Write(csvContent);
_streamWriter.Flush();
Initialise(_memoryStream, encoding);
}
/// <summary>
/// Reads the next record
/// </summary>
/// <returns>True if a record was successfuly read, otherwise false</returns>
public bool ReadNextRecord()
{
Fields = null;
var line = _streamReader.ReadLine();
if (line == null) return false;
ParseLine(line);
return true;
}
/// <summary>
/// Reads a csv file format into a data table. This method
/// will always assume that the table has a header row as this will be used
/// to determine the columns.
/// </summary>
/// <returns></returns>
public DataTable ReadIntoDataTable()
{
return ReadIntoDataTable(new System.Type[] { });
}
/// <summary>
/// Reads a csv file format into a data table. This method
/// will always assume that the table has a header row as this will be used
/// to determine the columns.
/// </summary>
/// <param name="columnTypes">Array of column types</param>
/// <returns></returns>
public DataTable ReadIntoDataTable(System.Type[] columnTypes)
{
var dataTable = new DataTable();
var addedHeader = false;
_stream.Position = 0;
while (ReadNextRecord())
{
if (!addedHeader)
{
for (var i = 0; i < Fields.Count; i++)
dataTable.Columns.Add(Fields[i],
columnTypes.Length > 0 ? columnTypes[i] : typeof(string));
addedHeader = true;
continue;
}
var row = dataTable.NewRow();
for (var i = 0; i < Fields.Count; i++) row[i] = Fields[i];
dataTable.Rows.Add(row);
}
return dataTable;
}
/// <summary>
/// Parses a csv line
/// </summary>
/// <param name="line">Line</param>
private void ParseLine(string line)
{
Fields = new List<string>();
var inColumn = false;
var inQuotes = false;
//_columnBuilder.Remove(0, _columnBuilder.Length);
_columnBuilder.Length = 0;
// Iterate through every character in the line
for (var i = 0; i < line.Length; i++)
{
var character = line[i];
// If we are not currently inside a column
if (!inColumn)
{
// If the current character is a double quote then the column value is contained within
// double quotes, otherwise append the next character
if (character == '"') inQuotes = true;
else _columnBuilder.Append(character);
inColumn = true;
continue;
}
// If we are in between double quotes
if (inQuotes)
{
// If the current character is a double quote and the next character is a comma or we are at the end of the line
// we are now no longer within the column.
// Otherwise increment the loop counter as we are looking at an escaped double quote e.g. "" within a column
if ((character == '"') &&
(((line.Length > i + 1) && (line[i + 1] == ',')) ||
(i + 1 == line.Length)))
{
inQuotes = false;
inColumn = false;
i++;
}
else if ((character == '"') && (line.Length > i + 1) && (line[i + 1] == '"'))
i++;
}
else if (character == ',') inColumn = false;
// If we are no longer in the column clear the builder and add the columns to the list
if (!inColumn)
{
Fields.Add(TrimColumns
? _columnBuilder.ToString().Trim()
: _columnBuilder.ToString());
//_columnBuilder.Remove(0, _columnBuilder.Length);
_columnBuilder.Length = 0;
}
else // append the current column
_columnBuilder.Append(character);
}
// If we are still inside a column add a new one
if (inColumn)
Fields.Add(TrimColumns
? _columnBuilder.ToString().Trim()
: _columnBuilder.ToString());
}
/// <summary>
/// Disposes of all unmanaged resources
/// </summary>
public void Dispose()
{
if (_streamReader != null)
{
_streamReader.Close();
_streamReader.Dispose();
}
if (_streamWriter != null)
{
_streamWriter.Close();
_streamWriter.Dispose();
}
if (_memoryStream != null)
{
_memoryStream.Close();
_memoryStream.Dispose();
}
if (_fileStream != null)
{
_fileStream.Close();
_fileStream.Dispose();
}
if (((_type == Type.String) || (_type == Type.File)) && (_stream != null))
{
_stream.Close();
_stream.Dispose();
}
}
#endregion Methods
}
/// <summary>
/// Class to hold csv data
/// </summary>
[Serializable]
public sealed class CsvFile
{
#region Properties
/// <summary>
/// Gets the file headers
/// </summary>
public readonly List<string> Headers = new List<string>();
/// <summary>
/// Gets the records in the file
/// </summary>
public readonly CsvRecords Records = new CsvRecords();
/// <summary>
/// Gets the header count
/// </summary>
public int HeaderCount
{
get { return Headers.Count; }
}
/// <summary>
/// Gets the record count
/// </summary>
public int RecordCount
{
get { return Records.Count; }
}
#endregion Properties
#region Indexers
/// <summary>
/// Gets a record at the specified index
/// </summary>
/// <param name="recordIndex">Record index</param>
/// <returns>CsvRecord</returns>
public CsvRecord this[int recordIndex]
{
get
{
if (recordIndex > Records.Count - 1)
throw new IndexOutOfRangeException(
string.Format("There is no record at index {0}.", recordIndex));
return Records[recordIndex];
}
}
/// <summary>
/// Gets the field value at the specified record and field index
/// </summary>
/// <param name="recordIndex">Record index</param>
/// <param name="fieldIndex">Field index</param>
/// <returns></returns>
public string this[int recordIndex, int fieldIndex]
{
get
{
if (recordIndex > Records.Count - 1)
throw new IndexOutOfRangeException(
string.Format("There is no record at index {0}.", recordIndex));
var record = Records[recordIndex];
if (fieldIndex > record.Fields.Count - 1)
throw new IndexOutOfRangeException(
string.Format("There is no field at index {0} in record {1}.",
fieldIndex, recordIndex));
return record.Fields[fieldIndex];
}
set
{
if (recordIndex > Records.Count - 1)
throw new IndexOutOfRangeException(
string.Format("There is no record at index {0}.", recordIndex));
var record = Records[recordIndex];
if (fieldIndex > record.Fields.Count - 1)
throw new IndexOutOfRangeException(
string.Format("There is no field at index {0}.", fieldIndex));
record.Fields[fieldIndex] = value;
}
}
/// <summary>
/// Gets the field value at the specified record index for the supplied field name
/// </summary>
/// <param name="recordIndex">Record index</param>
/// <param name="fieldName">Field name</param>
/// <returns></returns>
public string this[int recordIndex, string fieldName]
{
get
{
if (recordIndex > Records.Count - 1)
throw new IndexOutOfRangeException(
string.Format("There is no record at index {0}.", recordIndex));
var record = Records[recordIndex];
var fieldIndex = -1;
for (var i = 0; i < Headers.Count; i++)
{
if (string.Compare(Headers[i], fieldName) != 0) continue;
fieldIndex = i;
break;
}
if (fieldIndex == -1)
throw new ArgumentException(
string.Format("There is no field header with the name '{0}'", fieldName));
if (fieldIndex > record.Fields.Count - 1)
throw new IndexOutOfRangeException(
string.Format("There is no field at index {0} in record {1}.",
fieldIndex, recordIndex));
return record.Fields[fieldIndex];
}
set
{
if (recordIndex > Records.Count - 1)
throw new IndexOutOfRangeException(
string.Format("There is no record at index {0}.", recordIndex));
var record = Records[recordIndex];
var fieldIndex = -1;
for (var i = 0; i < Headers.Count; i++)
{
if (string.Compare(Headers[i], fieldName) != 0) continue;
fieldIndex = i;
break;
}
if (fieldIndex == -1)
throw new ArgumentException(
string.Format("There is no field header with the name '{0}'", fieldName));
if (fieldIndex > record.Fields.Count - 1)
throw new IndexOutOfRangeException(
string.Format("There is no field at index {0} in record {1}.",
fieldIndex, recordIndex));
record.Fields[fieldIndex] = value;
}
}
#endregion Indexers
#region Methods
/// <summary>
/// Populates the current instance from the specified file
/// </summary>
/// <param name="filePath">File path</param>
/// <param name="hasHeaderRow">True if the file has a header row, otherwise false</param>
public void Populate(string filePath, bool hasHeaderRow)
{
Populate(filePath, null, hasHeaderRow, false);
}
/// <summary>
/// Populates the current instance from the specified file
/// </summary>
/// <param name="filePath">File path</param>
/// <param name="hasHeaderRow">True if the file has a header row, otherwise false</param>
/// <param name="trimColumns">True if column values should be trimmed, otherwise false</param>
public void Populate(string filePath, bool hasHeaderRow, bool trimColumns)
{
Populate(filePath, null, hasHeaderRow, trimColumns);
}
/// <summary>
/// Populates the current instance from the specified file
/// </summary>
/// <param name="filePath">File path</param>
/// <param name="encoding">Encoding</param>
/// <param name="hasHeaderRow">True if the file has a header row, otherwise false</param>
/// <param name="trimColumns">True if column values should be trimmed, otherwise false</param>
public void Populate(
string filePath, Encoding encoding, bool hasHeaderRow, bool trimColumns)
{
using (
var reader = new CsvReader(filePath, encoding)
{ HasHeaderRow = hasHeaderRow, TrimColumns = trimColumns })
{
PopulateCsvFile(reader);
}
}
/// <summary>
/// Populates the current instance from a stream
/// </summary>
/// <param name="stream">Stream</param>
/// <param name="hasHeaderRow">True if the file has a header row, otherwise false</param>
public void Populate(Stream stream, bool hasHeaderRow)
{
Populate(stream, null, hasHeaderRow, false);
}
/// <summary>
/// Populates the current instance from a stream
/// </summary>
/// <param name="stream">Stream</param>
/// <param name="hasHeaderRow">True if the file has a header row, otherwise false</param>
/// <param name="trimColumns">True if column values should be trimmed, otherwise false</param>
public void Populate(Stream stream, bool hasHeaderRow, bool trimColumns)
{
Populate(stream, null, hasHeaderRow, trimColumns);
}
/// <summary>
/// Populates the current instance from a stream
/// </summary>
/// <param name="stream">Stream</param>
/// <param name="encoding">Encoding</param>
/// <param name="hasHeaderRow">True if the file has a header row, otherwise false</param>
/// <param name="trimColumns">True if column values should be trimmed, otherwise false</param>
public void Populate(
Stream stream, Encoding encoding, bool hasHeaderRow, bool trimColumns)
{
using (
var reader = new CsvReader(stream, encoding)
{ HasHeaderRow = hasHeaderRow, TrimColumns = trimColumns })
{
PopulateCsvFile(reader);
}
}
/// <summary>
/// Populates the current instance from a string
/// </summary>
/// <param name="hasHeaderRow">True if the file has a header row, otherwise false</param>
/// <param name="csvContent">Csv text</param>
public void Populate(bool hasHeaderRow, string csvContent)
{
Populate(hasHeaderRow, csvContent, null, false);
}
/// <summary>
/// Populates the current instance from a string
/// </summary>
/// <param name="hasHeaderRow">True if the file has a header row, otherwise false</param>
/// <param name="csvContent">Csv text</param>
/// <param name="trimColumns">True if column values should be trimmed, otherwise false</param>
public void Populate(bool hasHeaderRow, string csvContent, bool trimColumns)
{
Populate(hasHeaderRow, csvContent, null, trimColumns);
}
/// <summary>
/// Populates the current instance from a string
/// </summary>
/// <param name="hasHeaderRow">True if the file has a header row, otherwise false</param>
/// <param name="csvContent">Csv text</param>
/// <param name="encoding">Encoding</param>
/// <param name="trimColumns">True if column values should be trimmed, otherwise false</param>
public void Populate(
bool hasHeaderRow, string csvContent, Encoding encoding, bool trimColumns)
{
using (
var reader = new CsvReader(encoding, csvContent)
{ HasHeaderRow = hasHeaderRow, TrimColumns = trimColumns })
{
PopulateCsvFile(reader);
}
}
/// <summary>
/// Populates the current instance using the CsvReader object
/// </summary>
/// <param name="reader">CsvReader</param>
private void PopulateCsvFile(CsvReader reader)
{
Headers.Clear();
Records.Clear();
var addedHeader = false;
while (reader.ReadNextRecord())
{
if (reader.HasHeaderRow && !addedHeader)
{
reader.Fields.ForEach(field => Headers.Add(field));
addedHeader = true;
continue;
}
var record = new CsvRecord();
reader.Fields.ForEach(field => record.Fields.Add(field));
Records.Add(record);
}
}
#endregion Methods
}
/// <summary>
/// Class for a collection of CsvRecord objects
/// </summary>
[Serializable]
public sealed class CsvRecords : List<CsvRecord>
{
}
/// <summary>
/// Csv record class
/// </summary>
[Serializable]
public sealed class CsvRecord
{
#region Properties
/// <summary>
/// Gets the Fields in the record
/// </summary>
public readonly List<string> Fields = new List<string>();
/// <summary>
/// Gets the number of fields in the record
/// </summary>
public int FieldCount
{
get { return Fields.Count; }
}
#endregion Properties
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment