Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

CSV File Processor - Manage Comma Separated Value Files

5.00/5 (9 votes)
31 May 2022CPOL2 min read 10.5K   252  
Class to manage the content of Comma Separated Value Files or to deal with Structured Tables in memory
In this tip, you will learn about fast data access via Row Index and Unique Key, Select, Insert, Update, Delete and Clear interface methods. Events on data manipulation are provided.

Introduction

Have you ever had the situation where you have to deal with structured tables, but a database is not part of the project or would even be some kind of an over-engineered solution? CvsFileProcessor might be a solution here. You can read the data from a file or you can create the structured table in memory, Select the data, manipulate the data via the Insert, Update and Delete methods and finally save the actual content into a file.

To be more flexible, it might be a good idea to be able to handle the content of the columns as any flat data type, like: string, byte, int, double, bool and so on. The class provides an interface method to define a unique key for the structured table. Please bear in mind that multiple columns can be member of the unique key. Furthermore, the Insert, Update, Delete and Clear manipulation actions might be in the interest of other threads in a multi-threading application environment. Therefore, the class is raising events for such actions. Interested objects can just subscribe the events of interest and they will be notified.

What is this Article Update About?

This updated version provides some new features for the Select, Insert, Update and Delete methods. Furthermore, it provides the handling of CSV files where the values are to be handled with or without quote character embedding. Quote character handling is implemented in the new class CsvValue.

The CsvFileProcessor Class in Detail

The implementation of CsvFileProcessor is no rocket science. It is just a set of collections to handle the mapping of the Column Names to the Column Indexes, the Row Indexes to the Row Content, the Row Indexes to the Unique Keys and vice versa. The Select and Update methods are implemented to handle generic types. The implementation is straight forward and should be easy to understand.

C#
/*###############################################################################*/
/*#                                                                             #*/
/*#                (c) Jochen Haar                                              #*/
/*#                ===============                                              #*/
/*#                                                                             #*/
/*# Class-Name   : CsvFileProcessor                                             #*/
/*#                                                                             #*/
/*# Derived from : none                                                         #*/
/*#                                                                             #*/
/*# File-Name    : CsvFileProcessor.cs                                          #*/
/*#                                                                             #*/
/*# Author       : Dipl.-Ing. Jochen Haar                                       #*/
/*#                                                                             #*/
/*# Date         : 01.05.2022                                                   #*/
/*#                                                                             #*/
/*# Tool/Compiler: Visual Studio 2022                                           #*/
/*#                                                                             #*/
/*# Events       : Will be raised on Insert, Update, Delete and Clear actions.  #*/
/*#                                                                             #*/
/*# Exceptions   : Will be thrown on logical errors. Therefore, please use      #*/
/*#                try/catch blocks.                                            #*/
/*#                                                                             #*/
/*#=============================================================================#*/
/*# Change report                                                               #*/
/*#----------+-------------------+----------------------------------------------#*/
/*# Date     | Name              | description of the reason for changes        #*/
/*#----------+-------------------+----------------------------------------------#*/
/*#          |                   |                                              #*/
/*#----------+-------------------+----------------------------------------------#*/
/*###############################################################################*/

using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;

//---------------------------------------------------------------------------------
namespace CommonLibrary
//---------------------------------------------------------------------------------
{
    //-----------------------------------------------------------------------------
    public class CsvFileProcessor
    //-----------------------------------------------------------------------------
    {
        /// The Insert event handler
        public event EventHandler<CsvFileProcessorEventArgs> InsertEvent;
        /// The Update event handler
        public event EventHandler<CsvFileProcessorEventArgs> UpdateEvent;
        /// The Delete event handler
        public event EventHandler<CsvFileProcessorEventArgs> DeleteEvent;
        /// The Clear event handler
        public event EventHandler<CsvFileProcessorEventArgs> ClearEvent;
        /// The column and field separator
        public char Separator { get; private set; } = ',';
        /// The name of the Comma Separated Value file
        public string FileName { get; private set; } = null;
        /// The header column names as a string array
        private string[] ColumnNames = null;
        /// The counter for the RowIndex
        private uint RowCounter = 0;
        /// The CsvValue object to handle the values on reading from and writing 
        /// to files with or without embedded quote characters
        private CsvValue CsvValue = null;
        /// The collection for fast hash access to the ColumnIndex in the header 
        /// with the ColumnName as key
        private readonly Dictionary<string, 
        int> ColumnNameToIndexCollection = new Dictionary<string, int>();
        /// The collection for fast hash access to the value rows with RowIndex
        /// as key. The RowIndex in the collection might not be consecutive 
        /// due to the fact that methods to delete and insert rows are provided
        private readonly Dictionary<uint, 
        string[]> RowIndexToValueCollection = new Dictionary<uint, string[]>();
        /// The collection for fast hash access to the content row indexes with
        /// the UniqueKey as key. The RowIndex in the collection might not be 
        /// consecutive due to the fact that methods to delete and insert rows
        /// are provided
        private readonly Dictionary<string, uint> 
        UniqueKeyToRowIndexCollection = new Dictionary<string, uint>();
        /// The collection for fast hash access to the UniqueKey of the row 
        /// indexes with the RowIndex as key. The RowIndex in the collection 
        /// might not be consecutive due to the fact that methods to delete 
        /// and insert rows are provided
        private readonly Dictionary<uint, 
        string> RowIndexToUniqueKeyCollection = new Dictionary<uint, string>();
        /// The list of the columns which describe the unique key of the 
        /// UniqueKeyToRowIndexCollection
        private readonly List<string> UniqueKeyColumnList = new List<string>();
        /// Synchronization object for multi-threading
        private readonly object Locker = new object();

        /// Constructor
        //-------------------------------------------------------------------------
        public CsvFileProcessor(char separator = ',', bool isQuoted = true, 
        char quoteCharacter = '"') => this.Initialize(null, separator, 
                                                      isQuoted, quoteCharacter);
        //-------------------------------------------------------------------------

        /// Constructor for existing files. The column definition will be read from 
        /// the file.
        //-------------------------------------------------------------------------
        public CsvFileProcessor(string fileName, char separator = ',', 
                                bool isQuoted = true, char quoteCharacter = '"')
        //-------------------------------------------------------------------------
        {
            this.Initialize(fileName, separator, isQuoted, quoteCharacter);
            this.ReadColumnsFromFile();
        }

        /// Constructor for non existing files. The FileName will be used for 
        /// saving the content into a file with that name. The columns are 
        /// separated by the Separator character.
        //-------------------------------------------------------------------------
        public CsvFileProcessor(string fileName, string columns, char separator = ',', 
                                bool isQuoted = true, char quoteCharacter = '"')
        //-------------------------------------------------------------------------
        {
            this.Initialize(fileName, separator, isQuoted, quoteCharacter);
            this.AddColumns(columns);
        }

        /// Constructor for non existing files. The FileName will be used for 
        /// saving the content into a file with that name. The columns array 
        /// defines the ColumnNames.
        //-------------------------------------------------------------------------
        public CsvFileProcessor(string fileName, string[] columns, char separator = ',', 
                                bool isQuoted = true, char quoteCharacter = '"')
        //-------------------------------------------------------------------------
        {
            this.Initialize(fileName, separator, isQuoted, quoteCharacter);
            this.AddColumns(columns);
        }

        /// Resets the instance according the parameter settings. This method is 
        /// meant for situations where the same instance will be used for different
        /// Comma Separated Files or different Structured Tables. Please bear in 
        /// mind, that the previous content of the instance will be cleared 
        /// completely.
        //-------------------------------------------------------------------------
        public void Reset(string fileName, char separator = ',', 
                          bool isQuoted = true, char quoteCharacter = '"')
        //-------------------------------------------------------------------------
        {
            lock (this.Locker)
            {
                this.Initialize(fileName, separator, isQuoted, quoteCharacter);
                this.ReadColumnsFromFile();
            }
        }

        /// Resets the instance according the parameter settings and defines the 
        /// ColumnNames according the columns array. This method is meant for 
        /// situations where the same instance will be used for different Comma 
        /// Separated Files or different Structured Tables. Please bear in mind, 
        /// that the previous content of the instance will be cleared completely.
        //-------------------------------------------------------------------------
        public void Reset(string fileName, string[] columns, char separator = ',', 
                          bool isQuoted = true, char quoteCharacter = '"')
        //-------------------------------------------------------------------------
        {
            lock (this.Locker)
            {
                this.Initialize(fileName, separator, isQuoted, quoteCharacter);
                this.AddColumns(columns);
            }
        }

        /// Initializes the instance according the parameter settings
        //-------------------------------------------------------------------------
        private void Initialize(string fileName, char separator, 
                                bool isQuoted, char quoteCharacter)
        //-------------------------------------------------------------------------
        {
            string newFileName = fileName ?? this.FileName;
            this.Clear();
            this.FileName = newFileName;
            this.Separator = separator;
            this.CsvValue = new CsvValue(this.Separator, isQuoted, quoteCharacter);
        }

        /// Reads the column definition as content from the Comma Separated file
        //-------------------------------------------------------------------------
        private void ReadColumnsFromFile()
        //-------------------------------------------------------------------------
        {
            string columns = null;
            using (StreamReader streamReader = new StreamReader(this.FileName)) 
            { columns = streamReader.ReadLine(); }
            this.AddColumns(this.CsvValue.Split(this.EraseLastSeparator(columns)));
        }

        /// Adds a column name to the list of the unique key members
        //-------------------------------------------------------------------------
        public void AddColumnNameToUniqueKey(string columnName)
        //-------------------------------------------------------------------------
        {
            if (this.ColumnNameToIndexCollection.ContainsKey(columnName)) 
            this.UniqueKeyColumnList.Add(columnName);
            else throw new Exception($@"'{columnName}' is an unknown column 
                 and can't be added as a member of the unique key!");
        }

        /// Reads the content rows of the file. Please bear in mind, the header 
        /// will be skipped.
        //-------------------------------------------------------------------------
        public void ReadFile()
        //-------------------------------------------------------------------------
        {
            lock (this.Locker)
            {
                string row;
                uint rowIndex = 0;
                using (StreamReader streamReader = new StreamReader(this.FileName))
                {
                    while (streamReader.Peek() >= 0)
                    {
                        row = streamReader.ReadLine();
                        if (rowIndex > 0 && !string.IsNullOrEmpty(row)) 
                            this.Insert(row);
                        rowIndex++;
                    }
                }
            }
        }

        /// Returns the UniqueKey of a specific RowIndex or null
        //-------------------------------------------------------------------------
        private string GetUniqueKeyOfRowIndex(uint rowIndex)
        //-------------------------------------------------------------------------
        {
            string key = null;
            if (this.RowIndexToUniqueKeyCollection.ContainsKey(rowIndex)) key = 
            this.RowIndexToUniqueKeyCollection[rowIndex];
            return key;
        }

        /// Clears the current content of the instance completely
        //-------------------------------------------------------------------------
        public void Clear()
        //-------------------------------------------------------------------------
        {
            lock (this.Locker)
            {
                this.FileName = null;
                this.ColumnNames = null;
                this.ColumnNameToIndexCollection.Clear();
                this.RowIndexToValueCollection.Clear();
                this.UniqueKeyToRowIndexCollection.Clear();
                this.RowIndexToUniqueKeyCollection.Clear();
                this.UniqueKeyColumnList.Clear();
                this.RowCounter = 0;
            }

            this.ClearEvent?.Invoke(this, new CsvFileProcessorEventArgs());
        }

        /// Erases the last and unneeded Separator in the value string if 
        /// there is one
        //-------------------------------------------------------------------------
        private string EraseLastSeparator(string value) => value[value.Length - 1] == 
        this.Separator ? value.Substring(0, value.Length - 1) : value;
        //-------------------------------------------------------------------------

        /// Determines wether the instance contains the RowIndex
        //-------------------------------------------------------------------------
        public bool ContainsKey(uint rowIndex) => 
               this.RowIndexToValueCollection.ContainsKey(rowIndex);
        //-------------------------------------------------------------------------

        /// Determines wether the instance contains the UniqueKey
        //-------------------------------------------------------------------------
        public bool ContainsKey(string key) => 
               this.UniqueKeyToRowIndexCollection.ContainsKey(key);
        //-------------------------------------------------------------------------

        /// Returns the amount of columns of the header and of each row
        //-------------------------------------------------------------------------
        public int GetColumnCount() => this.ColumnNames != null ? 
                                               this.ColumnNames.Length : 0;
        //-------------------------------------------------------------------------

        /// Returns the amount of rows. The header is not included!
        //-------------------------------------------------------------------------
        public int GetRowCount() => this.RowIndexToValueCollection.Count;
        //-------------------------------------------------------------------------

        /// Returns the column name of a specific column index or an empty string 
        /// if the column is out of the range
        //-------------------------------------------------------------------------
        public string GetColumnName(uint columnIndex) => this.ColumnNames != null && 
        columnIndex < this.ColumnNames.Length ? this.ColumnNames[columnIndex] : @"";
        //-------------------------------------------------------------------------

        /// Returns the index of a specific column name or -1 if the column is 
        /// not valid
        //-------------------------------------------------------------------------
        public int GetColumnIndex(string columnName) => columnName != null && 
        this.ColumnNameToIndexCollection.ContainsKey(columnName) ? 
                            this.ColumnNameToIndexCollection[columnName] : -1;
        //-------------------------------------------------------------------------

        /// Returns true if the column name is part of the unique key
        //-------------------------------------------------------------------------
        public bool IsColumnNameUniqueKeyMember(string columnName) => 
                               this.UniqueKeyColumnList.Contains(columnName);
        //-------------------------------------------------------------------------

        /// Exports the content of the instance into a Comma Separated Value File 
        /// (CSV). The values of the columns will be embedded in quote characters 
        /// and single quote characters which are part of the value content will 
        /// be replaced by double quote characters
        //-------------------------------------------------------------------------
        public long ToQuotedCSV() => this.ToCSV(true);
        //-------------------------------------------------------------------------

        /// Exports the content of the instance into a plain Comma Separated Value 
        /// File (CSV). The values of the columns will be exported as plain 
        /// values - they are not embedded in quote characters
        //-------------------------------------------------------------------------
        public long ToPlainCSV() => this.ToCSV(false);
        //-------------------------------------------------------------------------

        /// Adds the columns definition to the instance by splitting the string 
        /// into the single column names.
        //-------------------------------------------------------------------------
        private void AddColumns(string columns)
        //-------------------------------------------------------------------------
        {
            columns = columns.Trim();
            if (string.IsNullOrEmpty(columns)) throw new Exception
            ($@"Missing header definition in/for file: '{this.FileName}'!");
            columns = this.EraseLastSeparator(columns);
            this.AddColumns(this.CsvValue.Split(columns));
        }

        /// Adds the columns definition to the instance. The ColumnNames are defined
        /// in the string array.
        //-------------------------------------------------------------------------
        private void AddColumns(string[] columns)
        //-------------------------------------------------------------------------
        {
            this.ColumnNames = new string[columns.Length];
            for (int i = 0; i < columns.Length; i++)
            {
                this.ColumnNames[i] = columns[i];
                this.ColumnNameToIndexCollection.Add(columns[i], i);
            }
        }

        /// Returns an ascending sorted list of all existing RowIndexes. 
        //-------------------------------------------------------------------------
        public List<uint> GetRowIndexList()
        //-------------------------------------------------------------------------
        {
            List<uint> list = new List<uint>();
            foreach (var pair in this.RowIndexToValueCollection) list.Add(pair.Key);
            list.Sort();
            return list;
        }

        /// Returns an unsorted list of all existing UniqueKeys. The order depends 
        /// on the manipulation of the content.
        //-------------------------------------------------------------------------
        public List<string> GetUniqueKeyList()
        //-------------------------------------------------------------------------
        {
            List<string> list = new List<string>();
            foreach (var pair in this.UniqueKeyToRowIndexCollection) list.Add(pair.Key);
            return list;
        }

        /// Returns the definition of the unique key as a string with the name of 
        /// each unique key column member separated by a '+' character
        //-------------------------------------------------------------------------
        private string GetUniqueKeyColumns()
        //-------------------------------------------------------------------------
        {
            string key = @"";
            foreach (var column in this.UniqueKeyColumnList) key += 
            column != this.UniqueKeyColumnList[this.UniqueKeyColumnList.Count - 1] ? 
                                               $@"{column}+" : column;
            return key; 
        }

        /// Validates the parameter "rowIndex" and "columnName" and throws an 
        /// Exception on any errors
        //-------------------------------------------------------------------------
        private void Validate(uint rowIndex, string columnName = null)
        //-------------------------------------------------------------------------
        {
            if (rowIndex == 0) throw new Exception
            ($@"Invalid row index: '{rowIndex}'. Must be greater than zero!");
            if (columnName != null && this.GetColumnIndex(columnName) == -1) 
            throw new Exception($@"'{columnName}' is not available 
            as a column in/for file: '{this.FileName}'!");
            if (!this.RowIndexToValueCollection.ContainsKey(rowIndex)) 
            throw new Exception($@"No content for row index: '{rowIndex}' 
            in/for file: '{this.FileName}' available!");
        }

        /// Updates the UniqueKey in the collections. If the column value 
        /// modification leads to a Duplicate Key the new value will be denied 
        /// and an Exception will be thrown - rollback scenario to the previous 
        /// value.
        //-------------------------------------------------------------------------
        private void UpdateUniqueKey(uint rowIndex, string columnName, 
                                     string newValue, string oldValue)
        //-------------------------------------------------------------------------
        {
            string newKey = @"";
            foreach (var keyColumn in this.UniqueKeyColumnList) newKey += 
            this.RowIndexToValueCollection[rowIndex][this.GetColumnIndex(keyColumn)];
            if (!this.UniqueKeyToRowIndexCollection.ContainsKey(newKey))
            {
                string oldKey = this.RowIndexToUniqueKeyCollection[rowIndex];
                this.UniqueKeyToRowIndexCollection.Remove(oldKey);
                this.UniqueKeyToRowIndexCollection.Add(newKey, rowIndex);
                this.RowIndexToUniqueKeyCollection[rowIndex] = newKey;
            }
            else  // modification leads to a Duplicate Key - rollback scenario
            {
                this.RowIndexToValueCollection[rowIndex]
                [this.GetColumnIndex(columnName)] = oldValue;
                throw new Exception($@"Duplicate unique key: 
                '{this.GetUniqueKeyColumns()}' with new value: '{newValue}' 
                for column: '{columnName}' and row: '{rowIndex}' 
                in/for file: '{this.FileName}'!. New value denied.");
            }
        }

        /// Inserts a row content to the instance by splitting the content into the
        /// single column values.
        //-------------------------------------------------------------------------
        private uint Insert(string values)
        //-------------------------------------------------------------------------
        {
            uint rowIndex;
            string key = @"";
            lock (this.Locker)
            {
                values = values.Trim();
                values = this.EraseLastSeparator(values);
                string[] valueArray = this.CsvValue.Split(values);
                if (this.GetColumnCount() == 0) throw new Exception
                ($@"Missing header definition in/for file: '{this.FileName}'!");
                if (valueArray.Length != this.ColumnNames.Length) 
                    throw new Exception($@"Column count mismatch. 
                    Row '{values}' has '{valueArray.Length}' columns. 
                    The header has: '{this.ColumnNames.Length}' 
                    columns in/for file: '{this.FileName}'!");
                foreach (var keyColumn in this.UniqueKeyColumnList) 
                         key += valueArray[this.GetColumnIndex(keyColumn)];
                if (this.UniqueKeyColumnList.Count > 0)
                {
                    if (this.UniqueKeyToRowIndexCollection.ContainsKey(key)) 
                    throw new Exception($@"Duplicate unique key: 
                                       '{this.GetUniqueKeyColumns()}' 
                    with row: '{values}' in/for file: '{this.FileName}'!");
                    this.UniqueKeyToRowIndexCollection.Add(key, this.RowCounter + 1);
                    this.RowIndexToUniqueKeyCollection.Add(this.RowCounter + 1, key);
                }

                this.RowIndexToValueCollection.Add(this.RowCounter + 1, valueArray);
                this.RowCounter++;  // increment the RowCounter here to 
                                    // avoid gaps in case of duplicate keys
                rowIndex = this.RowCounter;  // RowCounter can increase during Invoke 
                // in multi-threading applications - be safe and continue 
                // with a stack variable
                key = this.GetUniqueKeyOfRowIndex(rowIndex);
            }

            this.InsertEvent?.Invoke
                 (this, new CsvFileProcessorEventArgs(rowIndex, key));
            return rowIndex;
        }

        /// Inserts a row content to the instance. The values array contain the 
        /// content of the columns values.
        //-------------------------------------------------------------------------
        public uint Insert(string[] values) => this.Insert(this.CsvValue.ToCSV(values));
        //-------------------------------------------------------------------------

        /// Inserts an empty row content to the instance. Please use the returned 
        /// RowIndex and Update all columns which belong to the UniqueKey if there 
        /// are any right after the Insert() action.
        //-------------------------------------------------------------------------
        public uint Insert()
        //-------------------------------------------------------------------------
        {
            uint rowIndex;
            lock (this.Locker)
            {
                string[] values = new string[this.GetColumnCount()];
                for (int i = 0; i < this.GetColumnCount(); i++) 
                     values[i] = string.Empty;
                rowIndex = this.Insert(this.CsvValue.ToCSV(values));
            }

            return rowIndex;
        }

        /// Deletes a dedicated row and throws an Exception if the RowIndex does 
        /// not exist.
        //-------------------------------------------------------------------------
        public bool Delete(uint rowIndex)
        //-------------------------------------------------------------------------
        {
            string key;
            lock (this.Locker)
            {
                if (rowIndex == 0) throw new Exception
                ($@"Invalid row index: '{rowIndex}'. Must be greater than zero!");
                if (!this.RowIndexToValueCollection.ContainsKey(rowIndex)) 
                throw new Exception($@"No content for row index: 
                '{rowIndex}' in/for file: '{this.FileName}' available!");
                key = this.GetUniqueKeyOfRowIndex(rowIndex);
                this.UniqueKeyToRowIndexCollection.Remove
                     (this.RowIndexToUniqueKeyCollection[rowIndex]);
                this.RowIndexToUniqueKeyCollection.Remove(rowIndex);
                this.RowIndexToValueCollection.Remove(rowIndex);
            }

            this.DeleteEvent?.Invoke
            (this, new CsvFileProcessorEventArgs(rowIndex, key));
            return true;
        }

        /// Deletes a dedicated row addressed via the UniqueKey and throws an 
        /// Exception if the Key/RowIndex does not exist.
        //-------------------------------------------------------------------------
        public bool Delete(string key)
        //-------------------------------------------------------------------------
        {
            bool success;
            lock (this.Locker)
            {
                uint rowIndex = 0;
                success = this.UniqueKeyToRowIndexCollection.ContainsKey(key);
                if (success) rowIndex = this.UniqueKeyToRowIndexCollection[key];
                if (rowIndex > 0) success = this.Delete(rowIndex);
            }

            return success;
        }

        /// Delete all rows where the column value is equal to the generic Value
        //-------------------------------------------------------------------------
        public uint Delete<T>(string columnName, T value)
        //-------------------------------------------------------------------------
        {
            uint counter = 0;
            lock (this.Locker)
            {
                int columnIndex = this.GetColumnIndex(columnName);
                if (columnIndex == -1) throw new Exception($@"'{columnName}' 
                is not available as a column in/for file: '{this.FileName}'!");
                string strValue = TypeConverter.ConvertTo<string>(value);
                List<uint> list = new List<uint>();
                foreach (var pair in this.RowIndexToValueCollection.Where
                (pair => pair.Value[columnIndex] == strValue)) list.Add(pair.Key);
                foreach (uint rowIndex in list)
                {
                    if (this.Delete(rowIndex)) counter++;
                }
            }

            return counter;
        }

        /// Returns the column value as a generic type of a dedicated row index
        //-------------------------------------------------------------------------
        public bool Select<T>(uint rowIndex, string columnName, out T value)
        //-------------------------------------------------------------------------
        {
            this.Validate(rowIndex, columnName);
            value = TypeConverter.ConvertTo<T>
            (this.RowIndexToValueCollection[rowIndex][this.GetColumnIndex(columnName)]);
            return true;
        }

        /// Returns the column value as a generic type of the row which belongs 
        /// to the unique key
        //-------------------------------------------------------------------------
        public bool Select<T>(string key, string columnName, out T value)
        //-------------------------------------------------------------------------
        {
            if (!this.UniqueKeyToRowIndexCollection.ContainsKey(key)) 
            throw new Exception($@"Unknown key: '{key}' 
                                in/for file: '{this.FileName}'!");
            return this.Select(this.UniqueKeyToRowIndexCollection[key], 
                               columnName, out value);
        }

        /// Returns all column values in a string array or throws an Exception 
        /// if RowIndex is not valid
        //-------------------------------------------------------------------------
        public string[] Select(uint rowIndex)
        //-------------------------------------------------------------------------
        {
            this.Validate(rowIndex);
            return (string[])this.RowIndexToValueCollection[rowIndex].Clone();
        }

        /// Returns all column values in a string array or throws an Exception 
        /// if the UniqueKey is not valid
        //-------------------------------------------------------------------------
        public string[] Select(string key)
        //-------------------------------------------------------------------------
        {
            if (!this.UniqueKeyToRowIndexCollection.ContainsKey(key)) 
            throw new Exception($@"Unknown key: 
                               '{key}' in/for file: '{this.FileName}'!");
            return this.Select(this.UniqueKeyToRowIndexCollection[key]);
        }

        /// Updates the column value as a generic type of a dedicated row index. 
        /// The UniqueKey of the row will be updated with the column value to be 
        /// updated if the column is member of the UniqueKey.
        //-------------------------------------------------------------------------
        public bool Update<T>(uint rowIndex, string columnName, T value)
        //-------------------------------------------------------------------------
        {
            string key, newValue, oldValue;
            lock (this.Locker)
            {
                this.Validate(rowIndex, columnName);
                newValue = TypeConverter.ConvertTo<string>(value);
                oldValue = this.RowIndexToValueCollection[rowIndex]
                           [this.GetColumnIndex(columnName)];
                key = this.GetUniqueKeyOfRowIndex(rowIndex);
                this.RowIndexToValueCollection[rowIndex]
                     [this.GetColumnIndex(columnName)] = newValue;
                if (this.IsColumnNameUniqueKeyMember(columnName) && 
                newValue != oldValue) this.UpdateUniqueKey
                           (rowIndex, columnName, newValue, oldValue);
            }

            if (newValue != oldValue) this.UpdateEvent?.Invoke
            (this, new CsvFileProcessorEventArgs
                   (rowIndex, key, columnName, newValue, oldValue));
            return true;
        }

        /// Updates the column value as a generic type of the row which belongs to
        /// the unique key. The UniqueKey of the row will be updated with the 
        /// column value to be updated if the column is member of the UniqueKey.
        //-------------------------------------------------------------------------
        public bool Update<T>(string key, string columnName, T value)
        //-------------------------------------------------------------------------
        {
            if (!this.UniqueKeyToRowIndexCollection.ContainsKey(key)) 
            throw new Exception
                  ($@"Unknown key: '{key}' in/for file: '{this.FileName}'!");
            return this.Update
                  (this.UniqueKeyToRowIndexCollection[key], columnName, value);
        }

        /// Updates all column values of a specific column which contain the 
        /// OldValue with the NewValue as a generic type. The UniqueKey of the 
        /// rows will be updated with the column value to be updated if the 
        /// column is member of the UniqueKey. If the modification leads to a 
        /// Duplicate Key, an Exception will be thrown. This could lead to the 
        /// situation that the modification will end up partially.
        //-------------------------------------------------------------------------
        public uint Update<T>(string columnName, T oldValue, T newValue)
        //-------------------------------------------------------------------------
        {
            uint counter = 0;
            lock (this.Locker)
            {
                int columnIndex = this.GetColumnIndex(columnName);
                if (columnIndex == -1) throw new Exception($@"'{columnName}'
                    is not available as a column in/for file: '{this.FileName}'!");
                string strOldValue = TypeConverter.ConvertTo<string>(oldValue);
                foreach (var pair in this.RowIndexToValueCollection.Where
                        (pair => pair.Value[columnIndex] == strOldValue))
                {
                    if (this.Update(pair.Key, columnName, newValue)) counter++;
                }
            }

            return counter;
        }

        /// Exports the content of the instance into a Comma Separated Value File.
        //-------------------------------------------------------------------------
        private long ToCSV(bool isQuoted)
        //-------------------------------------------------------------------------
        {
            long length;
            lock (this.Locker)
            {
                File.Delete(this.FileName);
                CsvValue csv = new CsvValue(this.Separator, isQuoted, 
                                            this.CsvValue.QuoteCharacter);
                using (StreamWriter streamWriter = File.AppendText(this.FileName))
                {
                    streamWriter.WriteLine(csv.ToCSV(this.ColumnNames));
                    // iterate through the sorted RowIndexList 
                    // to keep the original order in case of intensive manipulation    
                    List<uint> list = this.GetRowIndexList();
                    foreach (uint rowIndex in list) 
                    streamWriter.WriteLine(csv.ToCSV
                                 (this.RowIndexToValueCollection[rowIndex]));
                }

                length = new FileInfo(this.FileName).Length;
            }

            return length;
        }
    }
}

Using the CsvFileProcessor Class

Please find below the implementation of how to read existing data from a file and an implementation of how to create a structured table in the memory, to do some manipulation stuff and to save the content of the instance into a file.

C#
===================================================================================
Example of an existing file:
===================================================================================

Row 0 = "Column000","Column001","Column002","Column003","Val","Column005","Column006"
Row 1 = "R1C0Value","R1C1Value","R1C2Value","R1C3Value","123","R1C5Value","R1C6Value"
Row 2 = "R2C0Value","R2C1Value","R2C2Value","R2C3Value","555","R2C5Value","R2C6Value"
Row 3 = "R3C0Value","R3C1Value","R3C2Value","R3C3Value","999","R3C5Value","R3C6Value"

===================================================================================
Usage for an existing file: 
===================================================================================

//---------------------------------------------------------------------------------
private void DoSomethingWithExistingFile()
//---------------------------------------------------------------------------------
{
    try
    {
        CsvFileProcessor csv = new CsvFileProcessor("YourFile.csv");// comma as default 
                                                       // column/field separator
        csv.AddColumnNameToUniqueKey("Column000");
        csv.AddColumnNameToUniqueKey("Column001");
        csv.ReadFile();
        string key = "R2C0Value" + "R2C1Value";
        bool success = csv.Select(2, "Val", out int value);
        // The statement above leads to the same result as the statement below: 
        success = csv.Select(key, "Val", out value);
        // The out int value would be: 555 in this example.
        success = csv.Update(key, "Val", value + 222);
        // The content of the "Val" column in row 2 was set to "777" 
        // in the statement above.
        long length = csv.ToPlainCSV();        // save without quote character embedding
        csv.Reset(null, csv.Separator, false); // reset with same file name, 
                                               // but without quote character handling
        csv.ReadFile();                        // read the plain CSV file back
        long length = csv.ToQuotedCSV();       // save with quote character embedding
    }
    catch (Exception e) { Console.WriteLine($@"ERROR: {e.Message}"); }
}    

===================================================================================
Usage for non existing files:
===================================================================================

//---------------------------------------------------------------------------------
private void DoSomethingWithNonExistingFile()
//---------------------------------------------------------------------------------
{
    try
    {
        double pi = 3.1415;
        string[] columns = new string[] { "Column000", "Column001", 
        "Column002", "Column003", "Val", "Column005", "Column006, the last one" };
        CsvFileProcessor csv = 
           new CsvFileProcessor("YourFile.csv", columns, '|'); // pipe as 
                                                               // column/field separator
        csv.AddColumnNameToUniqueKey("Column000");             // UniqueKey definition
        csv.InsertEvent += OnInsert;                        // subscribe the InsertEvent
        csv.UpdateEvent += OnUpdate;                        // subscribe the UpdateEvent
        csv.DeleteEvent += OnDelete;                        // subscribe the DeleteEvent
        csv.ClearEvent  += OnClear;                         // subscribe the ClearEvent
        for (int i = 1; i <= 9; i++) csv.Insert(new string[] 
                                     { $"R{i}C0Value", $"R{i}C1Value", 
        $"R{i}C2Value", $"R{i}C3Value", $"{i*100}", $"R{i}C5Value", $"R{i}C6Value" });
        string[] valueArray = csv.Select("R1C0Value");       // read complete row 
                                                             // into an array
        bool success = csv.Select(1, "Val", out int value);  // read as int with index
        success = csv.Select("R1C0Value", "Val", out value); // read as int with key
        success = csv.Update("R2C0Value", "Val", value + 222); // update as int
        success = csv.Update("R3C0Value", "Val", value + 333); // update as int
        success = csv.Update("R4C0Value", "Val", pi);          // update as double
        success = csv.Update("R5C0Value", "Val", pi * 2);      // update as double
        success = csv.Update("R6C0Value", "Val", true);        // update as boolean
        success = csv.Update("R7C0Value", "Val", true);        // update as boolean
        success = csv.Select("R7C0Value", "Val", out bool b);  // read as boolean
        uint rows = csv.Update("Val", b, !b);                  // toggle update of 
                                                               // all values in column 
                                                               // "Val" where value = b
        rows = csv.Delete("Val", false);                       // delete all rows 
                                                               // where the value of 
                                                               // column "Val" is false
        uint rowIndex = csv.Insert();                          // insert an empty row
        success = csv.Update(rowIndex, "Column000", "\"AnyValue\"");  // update the key 
                                                               // column of the empty row
        rowIndex = csv.Insert(new string[] { "Dog", "Cat", "Mouse", "Worm", "500", "Fly", 
                                             "I saw the movie, but didn't understand!" });
        long length = csv.ToQuotedCSV();                       // export with quote 
                                                               // characters and get the
                                                               // file length in bytes
    }
    catch (Exception e) { Console.WriteLine($@"ERROR: {e.Message}"); }
}

//---------------------------------------------------------------------------------
private void OnInsert(object sender, CsvFileProcessorEventArgs e) => 
   Console.WriteLine($@"Insert row: <{e.RowIndex}>, 
                     key: <{(e.Key == null ? @"null" : e.Key)}>");
//---------------------------------------------------------------------------------
//---------------------------------------------------------------------------------
private void OnUpdate(object sender, CsvFileProcessorEventArgs e) => 
   Console.WriteLine($@"Update row: <{e.RowIndex}>, 
                     key: <{(e.Key == null ? @"null" : e.Key)}>,                      
                     column: <{e.ColumnName}>, NewValue: <{e.NewValue}>, 
                     OldValue: <{e.OldValue}>");
//---------------------------------------------------------------------------------
//---------------------------------------------------------------------------------
private void OnDelete(object sender, CsvFileProcessorEventArgs e) => 
   Console.WriteLine($@"Delete row: <{e.RowIndex}>, 
                     key: <{(e.Key == null ? @"null" : e.Key)}>");
//---------------------------------------------------------------------------------
//---------------------------------------------------------------------------------
private void OnClear(object  sender, CsvFileProcessorEventArgs e) => 
   Console.WriteLine($@"Clear action");
//---------------------------------------------------------------------------------

Using CsvFileProcessor might be helpful not only if you want to deal with data in Comma Separated Value Files, it might also be helpful if you deal with some central data which can be manipulated by different threads and the threads have to react according the data content.

Conclusion

I hope this small class and the example is kind of useful for somebody or the implementation might help anybody who is searching for a solution in a completely different area.

History

  • 31st May, 2022: Initial post
  • 10th June, 2022: Article updated

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)