Last active
March 21, 2017 09:15
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //=============================================================================== | |
| #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