biegehydra / EasyCsv-Dotnet

EasyCsv is a simple and efficient .NET library for handling CSV files in your projects. With a **fluent** user-friendly API, it allows you to easily read, write, and manipulate CSV files with a minimal amount of code.
MIT License
26 stars 3 forks source link

Dynamic Headers #2

Closed akriger closed 7 months ago

akriger commented 7 months ago

What if I do not know how many headers or what the header names will be until runtime. My app gets the headers that will be used from an api and the header names or count of them could change at any time. I currently map them to the expected headers which works but calling GetRecords() fails. I’m putting them in a Dictionary currently.

biegehydra commented 7 months ago

Couple questions. What do you mean by "I currently map them to the expected headers"? Also, It would help to know what exception you are running into. CsvHelper throws some exceptions for missing headers and the like, these exceptions can be disabled through the CsvHelperConfig in the EasyCsvConfiguration. Ex:

var easyCsvConfig = new EasyCsvConfiguration()
{
    CsvHelperConfig = new CsvConfiguration(CultureInfo.InvariantCulture)
    {
         MissingHeaderFound = null
     }
}

However, you need the state of the IEasyCsv to match your class for the data to actually get in the right property. For example, if you have a property called “FirstName”, and you need to match it to “first”, “fir.” or whatever on the csv dynamically at runtime, you would have to do easyCsv.Mutate(x => x.ReplaceColumn(“first”, “FirstName”)); then get the the records.

I’m open to creating a function that would simplify this whole process, what signature would be easiest for you? Take a Dictionary<string, string>?

Also, I already have a Blazor component for when the headers are unknown. But that’s just a component and requires user input. I am considering factoring out the header matching code into an IEasyCsvMapper. All you would need to do is provide ExpectedHeaders for all your properties saying what headers names a csharp property should match to.

akriger commented 7 months ago

For some context the user selects a category from a dropdown list. Based on the category they select, this will change the possible headers that could be matched. These headers are pulled from another source via an API and at anytime could be modified in that other source. So I can't have a statically typed class to pass to T= in CsvTableHeaderMatcher, because the properties would change based on the category selected.

Once they choose a category, I'm calling the API to get the possible headers at that time and then adding them to the ExpectedHeader list.

private List<ExpectedHeader> _expectedHeaders = [];

// other code

private Task selectedFieldSet(string fieldSetId)
{
    selectedName = fieldSetId;
    var fieldSet = fieldSets?.FirstOrDefault(fs => fs.Name == selectedName);
    _fieldTypeIds = fieldSet?.FieldTypeIds;
    foreach (var fieldType in _fieldTypeIds)
    {
        _expectedHeaders.Add(new ExpectedHeader(fieldType));
    }
    return Task.CompletedTask;
}

My issue is what do I pass to the T= in the CsvTableHeaderMatcher component if I don't have a static class?

<CsvTableHeaderMatcher @ref="_tableHeaderMatcher" T="{what goes here}" Csv="_easyCsv" Frozen="_frozen" AllHeadersValidChanged="StateHasChanged" ExpectedHeaders="_expectedHeaders" AutoMatch="AutoMatching.Lenient"></CsvTableHeaderMatcher>
biegehydra commented 7 months ago

@akriger Take a look at #3 I'm thinking about moving the type parameter from the CsvTableHeaderMatcher and moving it to GetRecords<T>. The only other thing the type parameter is used for is autogenerating expected headers so I added a new parameter for that. Specifically, look at the new section I added to the read.me Also, an important thing to note. Is that your current code needs change. It should be more like this, you have to create new lists.

private List<ExpectedHeader> _expectedHeaders;
private Task selectedFieldSet(string fieldSetId)
{
    List<ExpectedHeaders> newExpectedHeaders = [];
    selectedName = fieldSetId;
    var fieldSet = fieldSets?.FirstOrDefault(fs => fs.Name == selectedName);
    _fieldTypeIds = fieldSet?.FieldTypeIds;
    foreach (var fieldType in _fieldTypeIds)
    {
        newExpectedHeaders.Add(new ExpectedHeader(fieldType));
    }
    _expectedHeaders = newExpectedHeaders.
    return Task.CompletedTask;
}

Another important thing to note is that this constructor new ExpectedHeader(fieldType) expects fieldType to be the CSharpPropertyName on the class you will be deserializing into, which I'm not sure if that's what you want.

akriger commented 7 months ago

@biegehydra I think removing the type parameter from CsvTableHeaderMatcher and passing the type with GetRecords<T> will fix the issue. Also, thanks for the catch on the _expectedHeaders list.

biegehydra commented 7 months ago

@akriger I just published v1.0.4 of EasyCsv.Components. Let me know if that works for you and I will close this issue.

akriger commented 7 months ago

@biegehydra I updated and tested, but no records are returned if dynamic is passed as the type. I tried the below after updating. _records comes back null.

private List<dynamic>? _records;

// other code

private async Task GetRecords()
{
    if (_tableHeaderMatcher == null) return;
    _records = await _tableHeaderMatcher.GetRecords<dynamic>();
}

In the meantime, I was able to do the below, which works.

private List<dynamic>? _records;

// other code

private async Task GetRecords()
{
    if (_tableHeaderMatcher == null) return;
    _records = await _easyCsv?.GetRecordsAsync<dynamic>(EasyCsvConfig.CsvHelperConfig, csvContextProfile);
}
biegehydra commented 7 months ago

Can you show me the configurations you used that made it work?

akriger commented 7 months ago

@biegehydra here is what I have in the GetRecords() method.

private async Task GetRecords()
{
    if (_tableHeaderMatcher == null) return;

    var csvContextProfile = new CsvContextProfile();
    if (ClassMaps != null)
    {
        csvContextProfile.ClassMaps = ClassMaps;
    }

    try
    {
        _easyCsv.RemoveUnusedHeaders();
        _records = await _easyCsv?.GetRecordsAsync<dynamic>(EasyCsvConfig.CsvHelperConfig, csvContextProfile);
    }
    catch (Exception ex)
    {
        Logger?.LogError(ex, "Error getting records. CsvTableHeaderMatcher");
    }
}
biegehydra commented 7 months ago

Okay, I'm going to close the issue.