JoshClose / CsvHelper

Library to help reading and writing CSV files
http://joshclose.github.io/CsvHelper/
Other
4.63k stars 1.05k forks source link

Set Custom Headers that derives from the property value. #2240

Open ammar91 opened 3 months ago

ammar91 commented 3 months ago

I have the following data model and trying to generate csv report that would have headers User, Product, Video 1, Video 2.. Video5 where the "Video 1" to "Video 5" would be the actual title of the videos extracted from Videos property list..

 public class UserProduct
 {
     public int User { get; set; }
     public string Product { get; set; }
     public List<UserProductVideo> Videos { get; set; }
 }
 public class UserProductVideo
 {
     public string VideoTitle { get; set; }  // use to generate header
     public string Status { get; set; }       //  rowv alue for corresponding title
     public bool IsCompleted { get; set; }
 }

and corresponding ClassMapas follows, I also have the custom converter defined so it generate row value for each video title but not sure how to set header as mentioned above

public sealed class UserProductCsvMap : ClassMap<UserProduct>
{
    public UserProductCsvMap()
    {
        Map(m => m.User);
        Map(m => m.Product);
        Map(m => m.Videos).TypeConverter<VideoConverter>();     // how to set the header?
    }
}
public class VideoConverter : DefaultTypeConverter
{
    public override string ConvertToString(object value, IWriterRow row, MemberMapData memberMapData)
    {
        var videos = (List<UserProductVideo>)value;

        foreach (var video in videos)
        {
            var status = video.IsCompleted ? "Completed" : "In Progress";
            row.WriteField(status);
        }

        return null;
    }
}

code to write the csv

 var configuration = new CsvConfiguration(CultureInfo.InvariantCulture);

 using (var streamWriter = new StreamWriter(memoryStream))
 {
     using (var csvWriter = new CsvWriter(streamWriter, configuration))
     {
                csvWriter.Context.RegisterClassMap<UserProductCsvMap>();
                csvWriter.WriteRecords(data);
                streamWriter.Flush();
      }
 }

The above generated the following output

User, Product, Videos //header
John, P1, vid, vid2,vid3,vid4,vid5
....

I'm pretty much close to get the desired output but struggling to set the custom headers for the Videos property. Any help on this would be highly appreciated.

ammar91 commented 3 months ago

@JoshClose Can you please put your thoughts on the above?

AltruCoder commented 3 months ago

Adding an index to the map of Videos, which has a start and end index, should work.

public sealed class UserProductCsvMap : ClassMap<UserProduct>
{
    public UserProductCsvMap()
    {
        Map(m => m.User);
        Map(m => m.Product);
        Map(m => m.Videos).TypeConverter<VideoConverter>().Name("Video").Index(2,6);   
    }
}
ammar91 commented 3 months ago

@AltruCoder sorry for the confusion but I'm shooting for something like below where column header from index 2-6 would have to be the actual video title. Also the videos may vary and really depend on the product, so in other report it could have 10 videos or may be more and the header would have the title of all those video but I think you get the idea what I mean.

image

sbandaru08 commented 3 months ago

Or can we have a dynamic function to take an array of headers i.e [User, Product, Video 1, Video 2.. Video5] to the map.

JoshClose commented 3 months ago

Will the list of video in each UserProduct always have the same count and be in the same order?

I would say it's easiest to just write by hand.

void Main()
{
    var records = new List<UserProduct>
    {       
        new UserProduct
        {
            User = 1,
            Product = "the product",
            Videos = new List<UserQuery.UserProductVideo>
            {
                new UserQuery.UserProductVideo
                {
                    VideoTitle = "Video 1",
                    IsCompleted = true,
                    Status = "Completed",
                },
                new UserQuery.UserProductVideo
                {
                    VideoTitle = "Video 2",
                    IsCompleted = false,
                    Status = "In Progress",
                },
            },
        },
    };
    var config = new CsvConfiguration(CultureInfo.InvariantCulture)
    {
    };
    using (var writer = new StringWriter())
    using (var csv = new CsvWriter(writer, config))
    {
        for (var i = 0; i < records.Count; i++)
        {
            var record = records[i];
            if (i == 0) 
            {
                csv.WriteField("User");
                csv.WriteField("Product");
                foreach (var video in record.Videos)
                {
                    csv.WriteField(video.VideoTitle);
                }
                csv.NextRecord();
            }

            csv.WriteField(record.User);
            csv.WriteField(record.Product);
            foreach (var video in record.Videos)
            {
                csv.WriteField(video.Status);
            }
            csv.NextRecord();
        }

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

public class UserProduct
{
    public int User { get; set; }
    public string Product { get; set; }
    public List<UserProductVideo> Videos { get; set; }
}

public class UserProductVideo
{
    public string VideoTitle { get; set; }  // use to generate header
    public string Status { get; set; }       //  rowv alue for corresponding title
    public bool IsCompleted { get; set; }
}
ammar91 commented 3 months ago

@JoshClose Thanks for the detailed answer but wonder if there is another way to do the same, ideally that won't involve doing too much manually. Since the converter is already there to take care of writing on the row level for the specific property. It is just header where it is getting harder to get it right.

The other thought is, if it is possible to add converter that get's called once to set the header for a given property or alternatively another overload of .Name(...) that would take function with the property value as a parameter and possibly writer too and that allows to writer header for a given property.

JoshClose commented 3 months ago

Just write the header manually then. Set HasHeaderRecord = false