Csvhelper: Quoting only specific output fields using ShouldQuote()

Created on 23 Jan 2019  路  7Comments  路  Source: JoshClose/CsvHelper

I need to quote only specific fields in my output file. I set up a ClassMap to do this with ConvertUsing, and it mostly works (clip of code is below). The problem is that it inserts extra sets of quotes. For example, the raw input file contains a quoted description field like this:

    ... 129383894985,"ACME SUPER STRONG MINTS",1.25,2, ...

On the input side, calling GetRecord() works as expected, populating the desc field with ACME SUPER STRONG MINTS:

    var record = csv.GetRecord<ItemRec>();

My code does a little tweaking of a couple of fields, and then writes the record to an output file using WriteRecord():

    csvout.WriteRecord(record);

If I do not implement ClassMap, none of the fields are quoted:

  ... 129383894985,ACME SUPER STRONG MINTS,1.25,2, ...

If I do implement ClassMap it works in that it quotes the correct fields, but inserts extra quotes:

    Map(m => m.desc).ConvertUsing(m => $"\"{m.desc}\"");

Produces output data that looks like this:

   ... 129383894985,"""ACME SUPER STRONG MINTS""",1.25,2, ...

I could not find a way to convince (or trick) CsvHelper that we only need a single set of quotation marks. Being under a deadline, I went ahead and just wrote plain code to generate the output, but for future reference would like to know if I am I missing or misunderstanding something (or just got something flat-out wrong... it would not be the first time). Is there a built-in way to do this?

Thanks!

Bob

(CsvHelper is very slick, BTW)
...
Here is the code I am using to implement the ClassMap:

    csvout.Configuration.RegisterClassMap<MyClassMap>();

   (...snip...)

    public sealed class MyClassMap : ClassMap<ItemRec>
    {
        public MyClassMap()
        {
            AutoMap();
            Map(m => m.tran_id).ConvertUsing(m => $"\"{m.tran_id}\"");
            Map(m => m.dep).ConvertUsing(m => $"\"{m.dep}\"");
            Map(m => m.code).ConvertUsing(m => $"\"{m.code}\"");
            Map(m => m.desc).ConvertUsing(m => $"\"{m.desc}\"");
        }
    }

Most helpful comment

Good example, thanks Josh. Would an attribute per field not be the simplest way to configure this feature?
Wanted to check first before logging feature request.

All 7 comments

There is a configuration for that.

csv.Configuration.ShouldQuote = (field, context) =>
{
    return true;
};

Josh, you have to hold the record for fastest response time to questions. Thanks very much... I will give it a shot.

Bob

Hi, Josh,
I am having trouble figuring out the context parameter...

    csv.Configuration.ShouldQuote = (field, context) => true;

I have tried several different approaches, but run into Intellisense errors. Here are some failed attempts (csvout is my CsvWriter):

            csvout.Configuration.ShouldQuote = ("desc", csvout.Configuration) => true;
            csvout.Configuration.ShouldQuote = ("desc", new WritingContext(csvout, csvout.Configuration, false)) ) ) => true;
            csvout.Configuration.ShouldQuote = ("desc", csvout.Context ) => true;

Intellisense says it's looking for a CsvHelper.Configuration.Configuration, and I just can't figure out what the context parameter should be.

Bob

Here is the default implementation. https://github.com/JoshClose/CsvHelper/blob/894fc921ac12b9795d5c0780855ca33c41bc735a/src/CsvHelper/Configuration/ConfigurationFunctions.cs#L90

It's supposed to be so you can inspect the field and decide whether to quote it or not. There is no configuration to have a specific field always be quoted, but not another. You can kinda hack it like this though.

void Main()
{
    var records = new List<Foo>
    {
        new Foo { Id = 1, Name = "one" },
        new Foo { Id = 2, Name = "two" },
    };

    using (var writer = new StringWriter())
    using (var csv = new CsvWriter(writer))
    {
        csv.Configuration.ShouldQuote = (field, context) =>
        {
            return context.Record.Count == 1;
        };
        csv.Configuration.RegisterClassMap<FooMap>();
        csv.WriteRecords(records);

        writer.ToString().Dump();
    }
}

public class Foo
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class FooMap : ClassMap<Foo>
{
    public FooMap()
    {
        Map(m => m.Id);
        Map(m => m.Name);
    }
}

If you know the index of the field, you can just specify it. The count of the Record will be the current index.

Hi, Josh,

Fantastic! Works like a charm. In my case, I need to quote fields 0, 2, 3, and 4. Following your guidance I added this bit of code and it worked perfectly. csvout is the CsvWriter...

            csvout.Configuration.ShouldQuote = (field, context) =>
            {
                var quote = new[] { 0, 2, 3, 4 }.Contains(context.Record.Count);
                return quote;
            };

Creating the new array containing indexes of the to-be-quoted fields seemed to be more compact that a series of or's. Thanks very much for your help.

Bob

This will be called on every field. It will be faster to create a single instance outside of this method that is used, so a new instance isn't created each time.

var indexes = new[] { 0, 2, 3, 4 };
csv.Configuration.ShouldQuote = (field, context) => indexes.Contains(context.Record.Count);

Good example, thanks Josh. Would an attribute per field not be the simplest way to configure this feature?
Wanted to check first before logging feature request.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dsotiriades picture dsotiriades  路  3Comments

DmitryEfimenko picture DmitryEfimenko  路  3Comments

rh78 picture rh78  路  3Comments

SuperSkippy picture SuperSkippy  路  5Comments

GraceYuJuSong picture GraceYuJuSong  路  4Comments