【问题标题】:C# User Defined CSV Mapping to POCOC# 用户定义的 CSV 映射到 POCO
【发布时间】:2012-04-07 17:01:59
【问题描述】:

我有一个从串行/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 的其他问题,但都处理在运行前已知的顺序,而我要求用户定义顺序。

【问题讨论】:

  • 在下面的帖子stackoverflow.com/questions/1941392/…中有关于这个的讨论
  • TextFieldParser 类将使我的代码更加健壮,谢谢。但是,我对所提出的映射方面更感兴趣,而该问题并未真正解决。
  • 只是一个简单的问题。你怎么不知道顺序?
  • 我知道什么数据会下来,但取决于哪个客户和他们向我们发送数据的方式,决定了顺序。 ClientA 可能先发送日期,ClientB 可能发送距离。这就是为什么我的应用程序的用户需要通过对话框指定映射。

标签: c# generics csv


【解决方案1】:

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

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

OleDb CSV 解析:Load csv into oleDB and force all inferred datatypes to string

更新

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

实现泛型类 - 如果不需要进一步推进解析(没有更多的字符串,将来没有更多的约束/功能),我会选择一个接受对象、字符串和映射信息的静态类。它的外观与您现在的外观几乎相同。这里有一些修改的版本(可能无法编译,但应该反映总体思路):

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 系统,因为它们严重依赖数据到对象的转换)。

就我个人而言,我会衡量反射是否适合我的性能需求,是否会向前解决问题。

【讨论】:

  • 感谢您的信息。不幸的是,我认为这有点矫枉过正。我只有 1 行 CSV 数据要解析(它不是文本文件)。在我看来,从 CSV - DataTable - POCO 出发是额外的 1 步,除非有人能说服我
  • 对不起,我不知何故错过了“一条线”的意思。更新了答案。
【解决方案2】:

我 100% 同意 @Dimitriy 的观点,因为过去几周我已经编写了 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
}

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-31
    • 2012-02-08
    • 2011-10-18
    • 2020-10-01
    • 2018-03-19
    相关资源
    最近更新 更多