using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using Swan.Reflection; namespace Swan.Formatters { /// /// A CSV writer useful for exporting a set of objects. /// /// /// The following code describes how to save a list of objects into a CSV file. /// /// using System.Collections.Generic; /// using Swan.Formatters; /// /// class Example /// { /// class Person /// { /// public string Name { get; set; } /// public int Age { get; set; } /// } /// /// static void Main() /// { /// // create a list of people /// var people = new List<Person> /// { /// new Person { Name = "Artyom", Age = 20 }, /// new Person { Name = "Aloy", Age = 18 } /// } /// /// // write items inside file.csv /// CsvWriter.SaveRecords(people, "C:\\Users\\user\\Documents\\file.csv"); /// /// // output /// // | Name | Age | /// // | Artyom | 20 | /// // | Aloy | 18 | /// } /// } /// /// public class CsvWriter : IDisposable { private static readonly PropertyTypeCache TypeCache = new PropertyTypeCache(); private readonly Object _syncLock = new Object(); private readonly Stream _outputStream; private readonly Encoding _encoding; private readonly Boolean _leaveStreamOpen; private Boolean _isDisposing; private UInt64 _mCount; #region Constructors /// /// Initializes a new instance of the class. /// /// The output stream. /// if set to true [leave open]. /// The encoding. public CsvWriter(Stream outputStream, Boolean leaveOpen, Encoding encoding) { this._outputStream = outputStream; this._encoding = encoding; this._leaveStreamOpen = leaveOpen; } /// /// Initializes a new instance of the class. /// It automatically closes the stream when disposing this writer. /// /// The output stream. /// The encoding. public CsvWriter(Stream outputStream, Encoding encoding) : this(outputStream, false, encoding) { // placeholder } /// /// Initializes a new instance of the class. /// It uses the Windows 1252 encoding and automatically closes /// the stream upon disposing this writer. /// /// The output stream. public CsvWriter(Stream outputStream) : this(outputStream, false, Definitions.Windows1252Encoding) { // placeholder } /// /// Initializes a new instance of the class. /// It opens the file given file, automatically closes the stream upon /// disposing of this writer, and uses the Windows 1252 encoding. /// /// The filename. public CsvWriter(String filename) : this(File.OpenWrite(filename), false, Definitions.Windows1252Encoding) { // placeholder } /// /// Initializes a new instance of the class. /// It opens the file given file, automatically closes the stream upon /// disposing of this writer, and uses the given text encoding for output. /// /// The filename. /// The encoding. public CsvWriter(String filename, Encoding encoding) : this(File.OpenWrite(filename), false, encoding) { // placeholder } #endregion #region Properties /// /// Gets or sets the field separator character. /// /// /// The separator character. /// public Char SeparatorCharacter { get; set; } = ','; /// /// Gets or sets the escape character to use to escape field values. /// /// /// The escape character. /// public Char EscapeCharacter { get; set; } = '"'; /// /// Gets or sets the new line character sequence to use when writing a line. /// /// /// The new line sequence. /// public String NewLineSequence { get; set; } = Environment.NewLine; /// /// Defines a list of properties to ignore when outputting CSV lines. /// /// /// The ignore property names. /// public List IgnorePropertyNames { get; } = new List(); /// /// Gets number of lines that have been written, including the headings line. /// /// /// The count. /// public UInt64 Count { get { lock(this._syncLock) { return this._mCount; } } } #endregion #region Helpers /// /// Saves the items to a stream. /// It uses the Windows 1252 text encoding for output. /// /// The type of enumeration. /// The items. /// The stream. /// true if stream is truncated, default false. /// Number of item saved. public static Int32 SaveRecords(IEnumerable items, Stream stream, Boolean truncateData = false) { // truncate the file if it had data if(truncateData && stream.Length > 0) { stream.SetLength(0); } using CsvWriter writer = new CsvWriter(stream); writer.WriteHeadings(); writer.WriteObjects(items); return (Int32)writer.Count; } /// /// Saves the items to a CSV file. /// If the file exits, it overwrites it. If it does not, it creates it. /// It uses the Windows 1252 text encoding for output. /// /// The type of enumeration. /// The items. /// The file path. /// Number of item saved. public static Int32 SaveRecords(IEnumerable items, String filePath) => SaveRecords(items, File.OpenWrite(filePath), true); #endregion #region Generic, main Write Line Method /// /// Writes a line of CSV text. Items are converted to strings. /// If items are found to be null, empty strings are written out. /// If items are not string, the ToStringInvariant() method is called on them. /// /// The items. public void WriteLine(params Object[] items) => this.WriteLine(items.Select(x => x == null ? String.Empty : x.ToStringInvariant())); /// /// Writes a line of CSV text. Items are converted to strings. /// If items are found to be null, empty strings are written out. /// If items are not string, the ToStringInvariant() method is called on them. /// /// The items. public void WriteLine(IEnumerable items) => this.WriteLine(items.Select(x => x == null ? String.Empty : x.ToStringInvariant())); /// /// Writes a line of CSV text. /// If items are found to be null, empty strings are written out. /// /// The items. public void WriteLine(params String[] items) => this.WriteLine((IEnumerable)items); /// /// Writes a line of CSV text. /// If items are found to be null, empty strings are written out. /// /// The items. public void WriteLine(IEnumerable items) { lock(this._syncLock) { Int32 length = items.Count(); Byte[] separatorBytes = this._encoding.GetBytes(new[] { this.SeparatorCharacter }); Byte[] endOfLineBytes = this._encoding.GetBytes(this.NewLineSequence); // Declare state variables here to avoid recreation, allocation and // reassignment in every loop Boolean needsEnclosing; String textValue; Byte[] output; for(Int32 i = 0; i < length; i++) { textValue = items.ElementAt(i); // Determine if we need the string to be enclosed // (it either contains an escape, new line, or separator char) needsEnclosing = textValue.IndexOf(this.SeparatorCharacter) >= 0 || textValue.IndexOf(this.EscapeCharacter) >= 0 || textValue.IndexOf('\r') >= 0 || textValue.IndexOf('\n') >= 0; // Escape the escape characters by repeating them twice for every instance textValue = textValue.Replace($"{this.EscapeCharacter}", $"{this.EscapeCharacter}{this.EscapeCharacter}"); // Enclose the text value if we need to if(needsEnclosing) { textValue = String.Format($"{this.EscapeCharacter}{textValue}{this.EscapeCharacter}", textValue); } // Get the bytes to write to the stream and write them output = this._encoding.GetBytes(textValue); this._outputStream.Write(output, 0, output.Length); // only write a separator if we are moving in between values. // the last value should not be written. if(i < length - 1) { this._outputStream.Write(separatorBytes, 0, separatorBytes.Length); } } // output the newline sequence this._outputStream.Write(endOfLineBytes, 0, endOfLineBytes.Length); this._mCount += 1; } } #endregion #region Write Object Method /// /// Writes a row of CSV text. It handles the special cases where the object is /// a dynamic object or and array. It also handles non-collection objects fine. /// If you do not like the way the output is handled, you can simply write an extension /// method of this class and use the WriteLine method instead. /// /// The item. /// item. public void WriteObject(Object item) { if(item == null) { throw new ArgumentNullException(nameof(item)); } lock(this._syncLock) { switch(item) { case IDictionary typedItem: this.WriteLine(this.GetFilteredDictionary(typedItem)); return; case ICollection typedItem: this.WriteLine(typedItem.Cast()); return; default: this.WriteLine(this.GetFilteredTypeProperties(item.GetType()).Select(x => x.ToFormattedString(item))); break; } } } /// /// Writes a row of CSV text. It handles the special cases where the object is /// a dynamic object or and array. It also handles non-collection objects fine. /// If you do not like the way the output is handled, you can simply write an extension /// method of this class and use the WriteLine method instead. /// /// The type of object to write. /// The item. public void WriteObject(T item) => this.WriteObject(item as Object); /// /// Writes a set of items, one per line and atomically by repeatedly calling the /// WriteObject method. For more info check out the description of the WriteObject /// method. /// /// The type of object to write. /// The items. public void WriteObjects(IEnumerable items) { lock(this._syncLock) { foreach(T item in items) { this.WriteObject(item); } } } #endregion #region Write Headings Methods /// /// Writes the headings. /// /// The type of object to extract headings. /// type. public void WriteHeadings(Type type) { if(type == null) { throw new ArgumentNullException(nameof(type)); } IEnumerable properties = this.GetFilteredTypeProperties(type).Select(p => p.Name).Cast(); this.WriteLine(properties); } /// /// Writes the headings. /// /// The type of object to extract headings. public void WriteHeadings() => this.WriteHeadings(typeof(T)); /// /// Writes the headings. /// /// The dictionary to extract headings. /// dictionary. public void WriteHeadings(IDictionary dictionary) { if(dictionary == null) { throw new ArgumentNullException(nameof(dictionary)); } this.WriteLine(this.GetFilteredDictionary(dictionary, true)); } /// /// Writes the headings. /// /// The object to extract headings. /// obj. public void WriteHeadings(Object obj) { if(obj == null) { throw new ArgumentNullException(nameof(obj)); } this.WriteHeadings(obj.GetType()); } #endregion #region IDisposable Support /// public void Dispose() => this.Dispose(true); /// /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(Boolean disposeAlsoManaged) { if(this._isDisposing) { return; } if(disposeAlsoManaged) { if(this._leaveStreamOpen == false) { this._outputStream.Dispose(); } } this._isDisposing = true; } #endregion #region Support Methods private IEnumerable GetFilteredDictionary(IDictionary dictionary, Boolean filterKeys = false) => dictionary.Keys.Cast() .Select(key => key == null ? String.Empty : key.ToStringInvariant()) .Where(stringKey => !this.IgnorePropertyNames.Contains(stringKey)) .Select(stringKey => filterKeys ? stringKey : dictionary[stringKey] == null ? String.Empty : dictionary[stringKey].ToStringInvariant()); private IEnumerable GetFilteredTypeProperties(Type type) => TypeCache.Retrieve(type, t => t.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanRead)).Where(p => !this.IgnorePropertyNames.Contains(p.Name)); #endregion } }