C#用户定义的CSV映射到POCO

本文关键字:映射 POCO CSV 用户 定义 | 更新日期: 2023-09-27 18:28:57

我有一个从串行/UDP/TCP源读取输入数据的系统。输入数据只是不同数据类型(例如DateTime、double、int、string)的CSV。一个示例字符串可能是:

2012/03/23 12:00:00,1.000,23,information,1.234

我目前有(未测试的)代码,允许用户指定CSV列表中的哪个值属于POCO的哪个属性。

所以在上面的例子中,我会有一个这样的对象:

public class InputData
{
 public DateTime Timestamp{get;set;}
 public double Distance{get;set;}
 public int Metres{get;set;}
 public string Description{get;set;}
 public double Height{get;set;}
} 

现在在这个类中,我有一个方法来解析CSV字符串并填充属性。这种方法还需要"映射"信息,因为无法保证CSV数据将按照哪个顺序到达——由用户定义正确的顺序。

这是我的映射类:

//This general class handles mapping CSV to objects
public class CSVMapping
{
    //A dictionary holding Property Names (Key) and CSV indexes (Value)
    //0 Based index
    public IDictionary<string, int> Mapping { get; set; }
}

现在我的方法ParseCSV():

//use reflection to parse the CSV survey input
public bool ParseCSV(string pCSV, CSVMapping pMapping)
{
    if (pMapping == null) return false;
    else
    {
        Type t = this.GetType();
        IList<PropertyInfo> properties = t.GetProperties();
        //Split the CSV values
        string[] values = pCSV.Split(new char[1] { ',' });
        //for each property set its value from the CSV
        foreach (PropertyInfo prop in properties)
        {
            if (pMapping.Mapping.Keys.Contains(prop.Name))
            {
                if (prop.GetType() == typeof(DateTime))
                {
                    if (pMapping.Mapping[prop.Name] >= 0 && pMapping.Mapping[prop.Name] < values.Length)
                    {
                        DateTime tmp;
                        DateTime.TryParse(values[pMapping.Mapping[prop.Name]], out tmp);
                        prop.SetValue(this, tmp, null);
                    }
                }
                else if (prop.GetType() == typeof(short))
                {
                    if (pMapping.Mapping[prop.Name] >= 0 && pMapping.Mapping[prop.Name] < values.Length)
                    {
                        double tmp;
                        double.TryParse(values[pMapping.Mapping[prop.Name]], out tmp);
                        prop.SetValue(this, Convert.ToInt16(tmp), null);
                    }
                }
                else if (prop.GetType() == typeof(double))
                {
                    if (pMapping.Mapping[prop.Name] >= 0 && pMapping.Mapping[prop.Name] < values.Length)
                    {
                        double tmp;
                        double.TryParse(values[pMapping.Mapping[prop.Name]], out tmp);
                        prop.SetValue(this, tmp, null);
                    }
                }
                else if (prop.GetType() == typeof(string))
                {
                    if (pMapping.Mapping[prop.Name] >= 0 && pMapping.Mapping[prop.Name] < values.Length)
                    {
                        prop.SetValue(this, values[pMapping.Mapping[prop.Name]], null);
                    }
                }
            }
        }
        return true;
    }
}

现在我的问题是:

我可能有几个类需要此功能。实现一个泛型类或扩展类来为我进行解析会有好处吗?我的方法是解析CSV数据和提升对象的好方法吗?或者有更好的方法吗?

我读过其他关于动态解析CSV的问题,但都涉及在运行时之前已知的顺序,而我要求用户定义顺序

C#用户定义的CSV映射到POCO

OleDB非常擅长解析CSV数据,您不必使用反射。以下是使用OleDB类进行映射的主要想法:

  1. 用户定义了一个映射(使用委托、流畅接口或其他什么),它会进入Mapper类中的Dictionary
  2. Parser创建一个DataTable并插入映射器中的列
  3. Parser创建OleDbConnection、Adapter、Command,并以正确的类型从CSV文件中填充dataTable
  4. Parser从DataTable中提取IDataRecords,您的Mapper需要从IDataRecord映射到对象。关于记录到对象映射的指导,我建议阅读ORM映射程序的来源,如Dapper.NET、Massive、PetaPoco

OleDb CSV解析:将CSV加载到OleDb中,并强制所有推断的数据类型字符串

更新

由于只有一个字符串,所以不用说,使用最简单的方法会更好。因此,对于问题:

实现泛型类——如果不需要进一步的解析(将来不再有字符串,不再有约束/特性),我会选择一个包含对象、字符串和映射信息的静态类。它的外观和你现在的差不多。以下是经过一些修改的版本(可能不会编译,但应该反映出总体想法):

public static class CSVParser
{
    public static void FillPOCO(object poco, string csvData, CSVMapping mapping)
    {
        PropertyInfo[] relevantProperties = poco.GetType().GetProperties().Where(x => mapping.Mapping.Keys.Contains(x)).ToArray();
        string[] dataStrings = csvData.Split(',');
        foreach (PropertyInfo property in relevantProperties)
            SetPropertyValue(poco, property, dataStrings[mapping.Mapping[property.Name]]);
    }
    private static void SetPropertyValue(object poco, PropertyInfo property, string value)
    {
        // .. here goes code to change type to the necessary one ..
        property.SetValue(poco, value);
    }
}

关于字符串到类型化值的转换,有Convert.ChangeType方法可以处理大多数情况。布尔变量有一个特殊的问题(当它被赋予"0"而不是"false"时)。

至于数据填充——尽管反射被认为是缓慢的,但对于单个对象和很少使用的对象,它应该足够了,因为它简单明了。处理poco填充问题的常用方法有:运行时转换方法创建(使用反射进行初始化,然后像任何其他方法一样进行编译和调用)-通常使用DynamicMethod、Expression Trees等实现-这里有很多主题;动态对象的使用(从C#4.0开始可用)-在哪里分配/获取变量不需要声明;使用市场上可用的库(通常来自ORM系统,因为它们严重依赖于数据到对象的转换)。

就我个人而言,我会衡量反思是否适合我的表现需求,并会继续解决问题。

我会100%同意@Dimitry的观点,因为我在过去几周里写了5-10个CSV解析器。

编辑:(注意,这需要使用类似Path.GetTempFile()的东西将文本保存到临时文件中,但它将允许您所需的灵活性)

使用DataTable的参数最好是当连接字符串使用得当时——使用Extended Properties='true;FMT=Delimited;HDR=Yes',它将进入DataTable,列标题(在这种情况下会对您有所帮助)将被保留。

所以你可以写一个类似的CSV

Name,Age,City
Dominic,29,London
Bill,20,Seattle

这将生成一个具有您指定的列标题的DataTable。否则,请像以前一样坚持使用序数。

为了集成这一点,添加一个构造函数(或扩展方法,我很快就会了解),当传递DataRow时,它将剥离数据:

public UserData(DataRow row)
{
     // At this point, the row may be reliable enough for you to
     // attempt to reference by column names. If not, fall back to indexes
    this.Name = Convert.ToString(row.Table.Columns.Contains("Name") ? row["Name"] : row[0]);
    this.Age = Convert.ToInt32(row.Table.Columns.Contains("Age") ? row["Age"] : row[1]);
    this.City = Convert.ToString(row.Table.Columns.Contains("City") ? row["City"] : row[2] );
}

有些人会争辩说,转换过程实际上不是UserData类的责任,因为它是一个POCO。而是在ConverterExtensions.cs类中实现一个扩展方法。

public static class ConverterExtensions
{
     public static void LoadFromDataRow<UserData>(UserData data, DataRow row)
     {
         data.Name = Convert.ToString(row.Table.Columns.Contains("Name") ? row["Name"] : row[0]);
         data.Age = Convert.ToInt32(row.Table.Columns.Contains("Age") ? row["Age"] : row[1]);
         data.City = Convert.ToString(row.Table.Columns.Contains("City") ? row["City"] : row[2] );
     }
}

一个更合理的体系结构方法是实现一个定义转换的接口。使用转换过程实现该接口,然后在内部存储该接口引用。这将为您进行转换,保持映射完全分离,并保持您的POCO整洁。它还将允许您"插入"映射器。

public interface ILoadFromDataRow<T>
{
     void LoadFromDataRow<T>(T object, DataRow dr);
}
public class UserLoadFromDataRow : ILoadFromDataRow<UserData>
{
     public void LoadFromDataRow<UserData>(UserData data, DataRow dr)
     {
        data.Name = Convert.ToString(row.Table.Columns.Contains("Name") ? row["Name"] : row[0]);
        data.Age = Convert.ToInt32(row.Table.Columns.Contains("Age") ? row["Age"] : row[1]);
        data.City = Convert.ToString(row.Table.Columns.Contains("City") ? row["City"] : row[2] );
     }
}
public class UserData
{
    private ILoadFromDataRow<UserData> converter;
    public UserData(DataRow dr = null, ILoadFromDataRow<UserData> converter = new LoadFromDataRow<UserData>())
    {
         this.converter = (converter == null ? new LoadFromDataRow<UserData>() : converter);
         if(dr!=null)
             this.converter.LoadFromDataRow(this,dr);
    }
    // POCO as before
}

对于您的场景,请选择扩展方法。这种接口方法(称为隔离)是在扩展方法出现之前实现它的方法。