namespace Unosquare.Swan.Formatters { using Reflection; using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; /// /// 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 Unosquare.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 bool _leaveStreamOpen; private bool _isDisposing; private ulong _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, bool leaveOpen, Encoding encoding) { _outputStream = outputStream; _encoding = encoding; _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 ulong Count { get { lock (_syncLock) { return _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 int SaveRecords(IEnumerable items, Stream stream, bool truncateData = false) { // truncate the file if it had data if (truncateData && stream.Length > 0) stream.SetLength(0); using (var writer = new CsvWriter(stream)) { writer.WriteHeadings(); writer.WriteObjects(items); return (int)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 int 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) => 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) => 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) => 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 (_syncLock) { var length = items.Count(); var separatorBytes = _encoding.GetBytes(new[] { SeparatorCharacter }); var endOfLineBytes = _encoding.GetBytes(NewLineSequence); // Declare state variables here to avoid recreation, allocation and // reassignment in every loop bool needsEnclosing; string textValue; byte[] output; for (var 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(SeparatorCharacter) >= 0 || textValue.IndexOf(EscapeCharacter) >= 0 || textValue.IndexOf('\r') >= 0 || textValue.IndexOf('\n') >= 0; // Escape the escape characters by repeating them twice for every instance textValue = textValue.Replace($"{EscapeCharacter}", $"{EscapeCharacter}{EscapeCharacter}"); // Enclose the text value if we need to if (needsEnclosing) textValue = string.Format($"{EscapeCharacter}{textValue}{EscapeCharacter}", textValue); // Get the bytes to write to the stream and write them output = _encoding.GetBytes(textValue); _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) _outputStream.Write(separatorBytes, 0, separatorBytes.Length); } // output the newline sequence _outputStream.Write(endOfLineBytes, 0, endOfLineBytes.Length); _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 (_syncLock) { switch (item) { case IDictionary typedItem: WriteLine(GetFilteredDictionary(typedItem)); return; case ICollection typedItem: WriteLine(typedItem.Cast()); return; default: WriteLine(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) => 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 (_syncLock) { foreach (var item in items) 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)); var properties = GetFilteredTypeProperties(type).Select(p => p.Name).Cast(); WriteLine(properties); } /// /// Writes the headings. /// /// The type of object to extract headings. public void WriteHeadings() => 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)); WriteLine(GetFilteredDictionary(dictionary, true)); } #if NET452 /// /// Writes the headings. /// /// The object to extract headings. /// item /// Unable to cast dynamic object to a suitable dictionary - item public void WriteHeadings(dynamic item) { if (item == null) throw new ArgumentNullException(nameof(item)); if (!(item is IDictionary dictionary)) throw new ArgumentException("Unable to cast dynamic object to a suitable dictionary", nameof(item)); WriteHeadings(dictionary); } #else /// /// Writes the headings. /// /// The object to extract headings. /// obj. public void WriteHeadings(object obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); WriteHeadings(obj.GetType()); } #endif #endregion #region IDisposable Support /// public void Dispose() => 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(bool disposeAlsoManaged) { if (_isDisposing) return; if (disposeAlsoManaged) { if (_leaveStreamOpen == false) { _outputStream.Dispose(); } } _isDisposing = true; } #endregion #region Support Methods private IEnumerable GetFilteredDictionary(IDictionary dictionary, bool filterKeys = false) => dictionary .Keys .Cast() .Select(key => key == null ? string.Empty : key.ToStringInvariant()) .Where(stringKey => !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 => !IgnorePropertyNames.Contains(p.Name)); #endregion } }