chris ~ $ 

software_developer devops_engineer ethical_hacker cheshire_uk

...

  Custom CSV Parser 

dev

Custom Csv Parser

Interface:

public interface ICsvParser
{
    IEnumerable<T> Parse<T>(string csv, CsvMapper<T> mapper, CsvParserConfig config);
}

Attribute:

[AttributeUsage(AttributeTargets.Property)]
public class CsvParserAttribute(string name) : Attribute
{
    public string Name { get; } = name;
}

Configuration:

public record CsvParserConfig(
    string[]? Headers = null, 
    string[]? RequiredHeaders = null, 
    char SeperatorCharacter = ',', 
    bool IncludesHeaders = true);

Type mapping:

public delegate T CsvMapper<out T>(IEnumerable<KeyValuePair<string, string?>> model);

public static class DefaultCsvParserAttributeMapping
{
    public static T DefaultCsvMapper<T>(
        IEnumerable<KeyValuePair<string, string>> model) 
        where T : new()
    {
        var l = model.ToList();

        var result = new T();
        var t = typeof(T);
        var props = t.GetProperties();
        foreach (var prop in props)
        {
            foreach (var attr in prop.GetCustomAttributes(true))
            {
                if (attr is CsvParserAttribute)
                {
                    var key = ((CsvParserAttribute) attr).Name;
                    var value = l.FirstOrDefault(s => s.Key == key).Value;

                    if (prop.PropertyType == typeof(string))
                    {
                        prop.SetValue(result, value?.Trim());
                    }
                    else if (prop.PropertyType == typeof(decimal))
                    {
                        _ = decimal.TryParse(value?.Trim(), out decimal decValue);
                        prop.SetValue(result, decValue);
                    }
                    else if (prop.PropertyType == typeof(double))
                    {
                        _ = double.TryParse(value?.Trim(), out double doubValue);
                        prop.SetValue(result, doubValue);
                    }
                    else if (prop.PropertyType == typeof(int))
                    {
                        _ = int.TryParse(value?.Trim(), out int intValue);
                        prop.SetValue(result, intValue);
                    }
                    else if (prop.PropertyType == typeof(int?))
                    {
                        _ = int.TryParse(value?.Trim(), out int intValue);
                        prop.SetValue(result, intValue);
                    }                    
                }
            }
        }
        return result;
    }
}

Parser:

public class DefaultCsvParser : ICsvParser
{
    public IEnumerable<T> Parse<T>(string csv, CsvMapper<T> mapper, CsvParserConfig config)
    {
        var lines = csv.Split("\n");
        var headers = config.Headers;
            
        if (config.IncludesHeaders && headers.IsNullOrEmpty())
            yield break;

        for(var i = 0; i < lines.Length; i++)
        {
            var line = lines[i];
            if (string.IsNullOrWhiteSpace(line))
                continue;
                
            var rowData = line.Split(config.SeperatorCharacter).ToArray();
                
            if (config.IncludesHeaders && i == 0 && headers.IsNullOrEmpty())
            {  
                headers = ParseHeaders(rowData, config.RequiredHeaders);
                continue;
            }

            if (headers.IsNullOrEmpty())
                yield break;

            yield return ParseRow(rowData, mapper, headers!);
        }
    }
        
    private T ParseRow<T>(IReadOnlyList<string> csvRow, CsvMapper<T> mapper, string[] headers)
    {
        var rowData = new List<KeyValuePair<string, string?>>();
        foreach(var header in headers)
        {
            int index = Array.IndexOf(headers, header);
            if (index >= csvRow.Count)
            {
                rowData.Add(new KeyValuePair<string, string?>(header, null));
                continue;
            }

            rowData.Add(new KeyValuePair<string, string?>(header, csvRow[index]));
        }

        return mapper(rowData);
    }

    private string[] ParseHeaders(string[]? csvRow, string[]? required)
    {
        if (csvRow == null)
            return [];

        required ??= [];

        var headers = new List<string>();

        foreach (var header in csvRow)
            headers.Add(header.ToLower());

        var missing = new List<string>();
        foreach (var req in required)
        {
            if (!headers.Contains(req.ToLower()))
                missing.Add(req);
        }

        if (missing.Count > 0)
            throw new Exception($"Missing required headers: {string.Join(", ", [.. missing])}");

        return [.. headers];
    }
}