<commit_before>﻿using System;
using System.Data;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Web;
using Newtonsoft.Json;

namespace VS.CsvResponse
{
    public class CsvFormatter : BufferedMediaTypeFormatter
    {
        public Func<object, HttpRequestMessage, object> Selector;
        
        static readonly char[] SpecialChars = { ',', '\n', '\r', '"' };

        private readonly HttpRequestMessage _request;

        private readonly string _fields;

        private CsvFormatter(HttpRequestMessage request) : this()
        {
            if (request == null) return;

            _request = request;
            _fields = HttpUtility.ParseQueryString(_request.RequestUri.Query)["fields"] ?? "*";
        }

        public CsvFormatter()
        {
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/csv"));
        }

        public override MediaTypeFormatter GetPerRequestFormatterInstance(Type type, HttpRequestMessage request, MediaTypeHeaderValue mediaType)
        {
            return new CsvFormatter(request)
            {
                Selector = Selector
            };
        }

        public override bool CanWriteType(Type type)
        {
            if (type == null)
            {
                throw new ArgumentNullException(nameof(type));
            }

            return true;
        }

        public override bool CanReadType(Type type)
        {
            return false;
        }

        public override void WriteToStream(Type type, object value, Stream writeStream, HttpContent content)
        {
            using (var writer = new StreamWriter(writeStream))
            {
                var dt = (DataTable)JsonConvert.DeserializeObject(JsonConvert.SerializeObject(ApplyFunc(value, _request)), (typeof(DataTable)));

                var cols = dt.Columns.Cast<DataColumn>().Where(column => _fields.IndexOf(column.ColumnName, StringComparison.OrdinalIgnoreCase) >= 0 || _fields == "*").Select(x => x.ColumnName).ToList();

                var columnNames = cols.Select(column => "\"" + column.Replace("\"", "\"\"") + "\"").ToArray();

                writer.WriteLine(string.Join(",", columnNames));

                foreach (DataRow row in dt.Rows)
                {
                    var fields = cols.Select(column => Escape(row[column])).ToList();

                    writer.WriteLine(string.Join(",", fields));
                }
            }
        }

        private object ApplyFunc(object value, HttpRequestMessage request)
        {
            return Selector != null ? Selector(value, request) : value;
        }

        private static string Escape(object o)
        {            
            var field = o.ToString();
            return field.IndexOfAny(SpecialChars) != -1 ? $"\"{field.Replace("\"", "\"\"")}\"" : field;
        }
    }
}<commit_msg>Add Content-Disposition header and allow filename to be specified Allow querystring mapping for accept=text/csv<commit_after>﻿using System;
using System.Data;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Web;
using Newtonsoft.Json;

namespace VS.CsvResponse
{
    public class CsvFormatter : BufferedMediaTypeFormatter
    {
        public Func<object, HttpRequestMessage, object> Selector;

        public string Filename = "export.csv";
         
        static readonly char[] SpecialChars = { ',', '\n', '\r', '"' };

        private readonly HttpRequestMessage _request;

        private readonly string _fields;

        private CsvFormatter(HttpRequestMessage request) : this()
        {
            if (request == null) return;

            _request = request;
            _fields = HttpUtility.ParseQueryString(_request.RequestUri.Query)["fields"] ?? "*";
        }

        public CsvFormatter()
        {
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/csv"));
            this.AddQueryStringMapping("accept", "text/csv", "text/csv");
        }

        public override MediaTypeFormatter GetPerRequestFormatterInstance(Type type, HttpRequestMessage request, MediaTypeHeaderValue mediaType)
        {
            return new CsvFormatter(request)
            {
                Selector = Selector,
                Filename = Filename
            };
        }

        public override bool CanWriteType(Type type)
        {
            if (type == null)
            {
                throw new ArgumentNullException(nameof(type));
            }

            return true;
        }

        public override bool CanReadType(Type type)
        {
            return false;
        }

        public override void WriteToStream(Type type, object value, Stream writeStream, HttpContent content)
        {
            using (var writer = new StreamWriter(writeStream))
            {
                var dt = (DataTable)JsonConvert.DeserializeObject(JsonConvert.SerializeObject(ApplyFunc(value, _request)), (typeof(DataTable)));

                var cols = dt.Columns.Cast<DataColumn>().Where(column => _fields.IndexOf(column.ColumnName, StringComparison.OrdinalIgnoreCase) >= 0 || _fields == "*").Select(x => x.ColumnName).ToList();

                var columnNames = cols.Select(column => "\"" + column.Replace("\"", "\"\"") + "\"").ToArray();

                writer.WriteLine(string.Join(",", columnNames));

                foreach (DataRow row in dt.Rows)
                {
                    var fields = cols.Select(column => Escape(row[column])).ToList();

                    writer.WriteLine(string.Join(",", fields));
                }
            }
        }

        public override void SetDefaultContentHeaders(Type type, HttpContentHeaders headers, MediaTypeHeaderValue mediaType)
        {
            base.SetDefaultContentHeaders(type, headers, mediaType);
            headers.Add("Content-Disposition", "attachment; filename=" + Filename);
        }

        private object ApplyFunc(object value, HttpRequestMessage request)
        {
            return Selector != null ? Selector(value, request) : value;
        }

        private static string Escape(object o)
        {            
            var field = o.ToString();
            return field.IndexOfAny(SpecialChars) != -1 ? $"\"{field.Replace("\"", "\"\"")}\"" : field;
        }
    }
}
{'fraction_non_alphanumeric': 0.1119088496956454, 'fraction_numerical': 0.0006243171531137818, 'mean_word_length': 2.104651162790698, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 17, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5502495', 'n_tokens_mistral': 1662, 'n_tokens_neox': 1597, 'n_words': 370}