-1

I am building an action method which export my data as CSV file, inside my asp.net mvc 5 web application. now i tried following the appraoch mentioned inside this link https://www.codeproject.com/Articles/1078092/ASP-MVC-Export-download-Grid-contents-in-different where i added the following class:-

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Web;
using System.Web.Mvc;

namespace MVCExport
{

    /// <summary>
    /// CSV file result impementation
    /// </summary>
    /// <typeparam name="TEntity">Entity list to transform to CSV</typeparam>
    public class CsvFileResult<TEntity> : FileResult where TEntity : class
    {
        #region Fields

        private const string DefaultContentType = "text/csv";
        private string _delimiter;
        private string _lineBreak;
        private Encoding _contentEncoding;
        private IEnumerable<string> _headers;
        private IEnumerable<PropertyInfo> _sourceProperties;
        private IEnumerable<TEntity> _dataSource;
        private Func<TEntity, IEnumerable<string>> _map;

        #endregion

        #region Properties

        public Func<TEntity, IEnumerable<string>> Map
        {
            get
            {

                return _map;
            }
            set { _map = value; }
        }
        public IEnumerable<TEntity> DataSource
        {
            get
            {
                return this._dataSource;
            }
        }
        /// <summary>
        /// CSV delimiter default ,
        /// </summary>
        public string Delimiter
        {
            get
            {
                if (string.IsNullOrEmpty(this._delimiter))
                {
                    this._delimiter = CultureInfo.CurrentCulture.TextInfo.ListSeparator;
                }

                return this._delimiter;
            }

            set { this._delimiter = value; }
        }

        /// <summary>
        /// Content Encoding (default is UTF8).
        /// </summary>
        public Encoding ContentEncoding
        {

            get
            {
                if (this._contentEncoding == null)
                {
                    this._contentEncoding = Encoding.Unicode;
                }

                return this._contentEncoding;
            }

            set { this._contentEncoding = value; }


        }

        /// <summary>
        /// the first line of the CSV file, column headers
        /// </summary>
        public IEnumerable<string> Headers
        {
            get
            {
                if (this._headers == null)
                {
                    this._headers = typeof(TEntity).GetProperties().Select(x => x.Name);
                }

                return this._headers;
            }

            set { this._headers = value; }
        }

        public IEnumerable<PropertyInfo> SourceProperties
        {
            get
            {
                if (this._sourceProperties == null)
                {
                    this._sourceProperties = typeof(TEntity).GetProperties();
                }

                return this._sourceProperties;
            }
        }



        /// <summary>
        ///  byte order mark (BOM)  .
        /// </summary>
        public bool HasPreamble { get; set; }

        /// <summary>
        /// Line  delimiter \n
        /// </summary>
        public string LineBreak
        {
            get
            {
                if (string.IsNullOrEmpty(this._lineBreak))
                {
                    this._lineBreak = Environment.NewLine;
                }

                return this._lineBreak;
            }

            set { this._lineBreak = value; }
        }



        /// <summary>
        /// Get or Set the response output buffer 
        /// </summary>
        public bool BufferOutput { get; set; }

        #endregion

        #region Ctor
        /// <summary>
        /// Creats new instance of CsvFileResult{TEntity}
        /// </summary>
        /// <param name="source">List of data to be transformed to csv</param>
        /// <param name="fileDonwloadName">CSV file name</param>
        /// <param name="contentType">Http response content type</param>
        public CsvFileResult(IEnumerable<TEntity> source, string fileDonwloadName, string contentType)
            : base(contentType)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            this._dataSource = source;

            if (string.IsNullOrEmpty(fileDonwloadName))
                throw new ArgumentNullException("fileDonwloadName");
            this.FileDownloadName = fileDonwloadName;

            this.BufferOutput = true;

        }

        /// <summary>
        /// Creats new instance of CsvFileResult{TEntity}
        /// </summary>
        /// <param name="source">List of data to be transformed to csv</param>
        /// <param name="fileDonwloadName">CSV file name</param>
        public CsvFileResult(IEnumerable<TEntity> source, string fileDonwloadName)
            : this(source, fileDonwloadName, DefaultContentType)
        {

        }

        /// <summary>
        /// Creats new instance of CsvFileResult{TEntity}
        /// </summary>
        /// <param name="source">List of data to be transformed to csv</param>
        /// <param name="fileDonwloadName">CSV file name</param>
        /// <param name="map">Custom transformation delegate</param>
        /// <param name="headers">Columns headers</param>
        public CsvFileResult(IEnumerable<TEntity> source, string fileDonwloadName, Func<TEntity, IEnumerable<string>> map, IEnumerable<string> headers)
            : this(source, fileDonwloadName, DefaultContentType)
        {
            this._headers = headers;
            this._map = map;
        }

        #endregion

        #region override

        protected override void WriteFile(HttpResponseBase response)
        {
            response.ContentEncoding = this.ContentEncoding;
            response.BufferOutput = this.BufferOutput;
            var streambuffer = ContentEncoding.GetBytes(this.GetCSVData());
            if (HasPreamble)
            {
                var preamble = this.ContentEncoding.GetPreamble();
                response.OutputStream.Write(preamble, 0, preamble.Length);
            }

            response.OutputStream.Write(streambuffer, 0, streambuffer.Length);
        }

        #endregion

        #region local routines

        private string GetCSVHeader()
        {
            string csv = "";
            csv = String.Join(this.Delimiter, this.Headers.Select(x => this.FormatCSV(x)));

            return csv;
        }


        private string GetCSVData()
        {
            string csv = GetCSVHeader();
            Func<TEntity, string> expr = x => this.Map == null ? this.FormatPropertiesCSV(x) : this.FormatMapCSV(x);
            csv += this.LineBreak + String.Join(this.LineBreak, this.DataSource.Select(expr));
            return csv;
        }

        private string FormatCSV(string str)
        {
            str = (str ?? "").Replace(this.Delimiter, "\"" + this.Delimiter + "\"");
            str = str.Replace(this.LineBreak, "\"" + this.LineBreak + "\"");
            str = str.Replace("\"", "\"\"");

            return String.Format("\"{0}\"", str);
        }

        private string FormatPropertiesCSV(TEntity obj)
        {
            string csv = "";

            foreach (var pi in this.SourceProperties)
            {
                string val = GetPropertyValue(pi, obj);
                csv += FormatCSV(val) + this.Delimiter;
            }

            csv = csv.TrimEnd(this.Delimiter.ToCharArray());
            return csv;
        }


        private string GetPropertyValue(PropertyInfo pi, object source)
        {
            try
            {
                var result = pi.GetValue(source, null);
                return (result == null) ? "" : result.ToString();
            }
            catch (Exception)
            {
                return "Can not obtain the value";
            }
        }

        private string FormatMapCSV(TEntity obj)
        {
            return String.Join(this.Delimiter, this.Map(obj).Select(x => FormatCSV(x)));
        }


        #endregion

    }
}

then i define the export action method as follow:-

public ActionResult MyExportCSV()
        {
            IEnumerable<Employee> dataList = _dataSource.GetAll();
            return new CsvFileResult<Employee>(dataList, "toto.csv");
        }

now the above worked well in almost 99% of the cases, but the problem i am facing is that when the data contain embedded commas i will get the value inside the .csv file surrounded by "". here is an example:-

  1. i have the following value test,123 inside my database. now when i export the data using the above code, this value will be shown as test","123 when i open the .csv file using MS excel 2010. as shown in this picture enter image description here

  2. and when i open the .csv file using Notepad++ i will get the value as "test"",""123"

so i am not sure where is the problem? is it inside my code? or it is something realted to MS excel 2010? i mean at the end i was expecting the value test,123 to be shown as is and not as test","123 inside MS excel 2010?

John John
  • 1
  • 72
  • 238
  • 501
  • 2
    You are not in the business of making CSV libraries. You are in the business of writing code for your company. Don't waste their time re-inventing the wheel. Use a pre-existing well supported library that is capable of handling the nuances of a CSV file. The most popular library on NuGet when you search for "CSV" is [CsvHelper](http://joshclose.github.io/CsvHelper/). I suggest you start there. – mason Oct 18 '17 at 00:46
  • @mason yes i do agree with you that there is no need to reinvent the wheel... now for example if i want to read csv file i use the following library `Microsoft.VisualBasic.FileIO`, instead of reinventing the wheel.. but i can not find any library to export the data that will work easily with asp.net MVC. so the link i provided https://www.codeproject.com/Articles/1078092/ASP-MVC-Export-download-Grid-contents-in-different seems to provide a simple way which will allow to export the data and i can reuse the same code to export any model class i have... – John John Oct 18 '17 at 02:11
  • @mason ... now regarding the CsvHelper library you provided is there a detailed documentation about how i can use it inside asp.net mvc?? second question, do you know what is causing the problem i mentioned? i mean if i can get it fixed then i think i should be fine... or you suggest to try using CsvHelper instead? – John John Oct 18 '17 at 02:11
  • 1
    Of course I'm suggesting use CsvHelper. You reinvented the wheel. Now you have to maintain and fix bugs in a CSV writing library that some random person with a Code Project account created. Do you really think that's the best use of your time? Or do you think it's better to use of your time to use CsvHelper, an library downloaded nearly 3 million times and used by thousands of developers, where all the kinks have been worked out? As for getting it working in ASP.NET MVC, it's a CSV-creating .NET library. It's trivial to utilize it in ASP.NET MVC. The site I linked you to should make that clear – mason Oct 18 '17 at 02:17
  • 1
    Try implementing CsvHelper. If you get stuck, provide an [MCVE](https://stackoverflow.com/help/MCVe). I'd remove this code you copy pasted from Code Project immediately. It's a huge liability in your application and a waste of your time. – mason Oct 18 '17 at 02:19
  • @mason but where i can find some real examples about CsvHelper ? – John John Oct 18 '17 at 02:26
  • @mason is the approach mentioned in the accepted answer inside this question https://stackoverflow.com/questions/21093150/using-csvhelper-to-output-stream-to-browser a valid way to do things ? – John John Oct 18 '17 at 02:32
  • @mason CsvHelper worked 100% well – John John Oct 24 '17 at 11:47
  • Good, glad to hear it! – mason Oct 24 '17 at 13:19

1 Answers1

1

You are surrounding the delimiter with double quotes, which is not what you want.

private string FormatCSV(string str)
{
    str = (str ?? "").Replace(this.Delimiter, "\"" + this.Delimiter + "\"");
    str = str.Replace(this.LineBreak, "\"" + this.LineBreak + "\"");
    str = str.Replace("\"", "\"\"");

    return String.Format("\"{0}\"", str);
}

You only need to escape the double quotes inside your field, and then put the whole field in double quotes:

private string FormatCSV(string str)
{
    str = str.Replace("\"", "\"\"");
    return String.Format("\"{0}\"", str);
}

The correct data should look like this: "test,123", or like this if your field contains double quotes:

"monitor 24"", Samsung"
Danny_ds
  • 11,201
  • 1
  • 24
  • 46
  • so you mean i do not need these 2 lines of code:- `str = (str ?? "").Replace(this.Delimiter, "\"" + this.Delimiter + "\""); str = str.Replace(this.LineBreak, "\"" + this.LineBreak + "\"");`?? now based on the csv documentation on Wikipedia https://en.wikipedia.org/wiki/Comma-separated_values they mention "Fields with embedded line breaks must be quoted" ,, this could explain why the author of the https://www.codeproject.com/Articles/1078092/ASP-MVC-Export-download-Grid-contents-in-different have added these 2 lines of code?? – John John Oct 18 '17 at 02:12
  • @johnG Yes, those fields must be quoted, but not the line breaks or the delimiters themselves, which is what happens in those first two lines. If you remove those 2 lines all should be fine (assuming `FormatCSV()` only gets called for field values, not for whole rows). – Danny_ds Oct 18 '17 at 10:35