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的问题,但都涉及在运行时之前已知的顺序,而我要求用户定义顺序
OleDB非常擅长解析CSV数据,您不必使用反射。以下是使用OleDB类进行映射的主要想法:
- 用户定义了一个映射(使用委托、流畅接口或其他什么),它会进入Mapper类中的Dictionary
- Parser创建一个DataTable并插入映射器中的列
- Parser创建OleDbConnection、Adapter、Command,并以正确的类型从CSV文件中填充dataTable
- 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
}
对于您的场景,请选择扩展方法。这种接口方法(称为隔离)是在扩展方法出现之前实现它的方法。