无法使用CSVHelper的ClassMap将自定义对象内的外部对象写入CSV。

huangapple go评论88阅读模式
英文:

Cannot write external object inside of custom object to CSV using CSVHelper with ClassMap

问题

我想将Vector3对象(参见https://docs.unity3d.com/ScriptReference/Vector3.html)写入我的自定义类的CSV文件中:

public class BehaviouralData
{
    public float ActionZ { get; set; }
    public float ActionX { get; set; }
    public int TargetBallAgentHashCode { get; set; }
    public Vector3 TargetBallLocalPosition { get; set; }
    public bool CollectBehaviouralData { get; set; }
    public DateTime ActionTime { get; set; }
    public DateTime Time { get; set; }
}

因此,我使用了一个ClassMap:

public sealed class Vector3Map : ClassMap<Vector3>
{
    public Vector3Map()
    {
        Map(m => m.x);
        Map(m => m.y);
        Map(m => m.z);
    }
}

在写入文件之前,我要注册它:

...
using (var csv = new CsvWriter(writer, config))
{
    csv.Context.RegisterClassMap<Vector3Map>();
    csv.WriteRecords(data);
}
...

当我尝试保存文件时,我收到以下错误消息:

ArgumentException: Incorrect number of arguments supplied for call to method 'Single get_Item(Int32)'
Parameter name: property
System.Linq.Expressions.Expression.Property (System.Linq.Expressions.Expression expression, System.Reflection.PropertyInfo property) (at <1e18c5a6594041c9844bfd0b6618ee4a>:0)
CsvHelper.Expressions.ExpressionManager.CreateGetMemberExpression (System.Linq.Expressions.Expression recordExpression, CsvHelper.Configuration.ClassMap mapping, CsvHelper.Configuration.MemberMap memberMap) (at <9a1379e089274cc1baf4edcf480e4c6d>:0)
CsvHelper.Expressions.ExpressionManager.CreateGetMemberExpression (System.Linq.Expressions.Expression recordExpression, CsvHelper.Configuration.ClassMap mapping, CsvHelper.Configuration.MemberMap memberMap) (at <9a1379e089274cc1baf4edcf480e4c6d>:0)
CsvHelper.Expressions.ObjectRecordWriter.CreateWriteDelegate[T] (T record) (at <9a1379e089274cc1baf4edcf480e4c6d>:0)
CsvHelper.Expressions.RecordWriter.GetWriteDelegate[T] (T record) (at <9a1379e089274cc1baf4edcf480e4c6d>:0)
CsvHelper.Expressions.RecordWriter.Write[T] (T record) (at <9a1379e089274cc1baf4edcf480e4c6d>:0)
CsvHelper.Expressions.RecordManager.Write[T] (T record) (at <9a1379e089274cc1baf4edcf480e4c6d>:0)
CsvHelper.CsvWriter.WriteRecords[T] (System.Collections.Generic.IEnumerable`1[T] records) (at <9a1379e089274cc1baf4edcf480e4c6d>:0)
Rethrow as WriterException: An unexpected error occurred.
IWriter state:
Row: 2
Index: 0
HeaderRecord:
2
CsvHelper.CsvWriter.WriteRecords[T] (System.Collections.Generic.IEnumerable`1[T] records) (at <9a1379e089274cc1baf4edcf480e4c6d>:0)
Util.SaveDataToCSV[T] (System.String path, System.Collections.Generic.List`1[T] data) (at Assets/Scripts/Util/Util.cs:178)
BehaviourMeasurement.SaveRawBehavioralDataToCSV () (at Assets/Scripts/Measurement/BehaviourMeasurementBehaviour.cs:1021)
BehaviourMeasurementBehaviour.OnDisable () (at Assets/Scripts/Measurement/BehaviourMeasurementBehaviour.cs:122)

而空的CSV文件具有以下列:

ActionZ,ActionX,TargetBallAgentHashCode,Item,Item,magnitude,sqrMagnitude,magnitude,sqrMagnitude,CollectBehaviouralData,ActionTime,Time

正如您所见,文件中没有写入坐标,而是其他属性(例如magnitude),我不想包括它们。为什么CSVHelper没有将正确的属性映射到CSV文件中?

英文:

I want to write a Vector3 object (see https://docs.unity3d.com/ScriptReference/Vector3.html) which is part of my custom class to a CSV file:

public class BehaviouralData
{
    public float ActionZ { get; set; }
    public float ActionX { get; set; }
    public int TargetBallAgentHashCode { get; set; }
    public Vector3 TargetBallLocalPosition { get; set; }
    public bool CollectBehaviouralData { get; set; }
    public DateTime ActionTime { get; set; }
    public DateTime Time { get; set; }
}

Therefore I am using a ClassMap

public sealed class Vector3Map : ClassMap&lt;Vector3&gt;
{
    public Vector3Map()
    {
        Map(m =&gt; m.x);
        Map(m =&gt; m.y);
        Map(m =&gt; m.z);
    }
}

Which I register before writing to a file:

...
using (var csv = new CsvWriter(writer, config))
{
    csv.Context.RegisterClassMap&lt;Vector3Map&gt;();
    csv.WriteRecords(data);
}
...

When I try to save the file, I get the following error message:

ArgumentException: Incorrect number of arguments supplied for call to method &#39;Single get_Item(Int32)&#39;
Parameter name: property
System.Linq.Expressions.Expression.Property (System.Linq.Expressions.Expression expression, System.Reflection.PropertyInfo property) (at &lt;1e18c5a6594041c9844bfd0b6618ee4a&gt;:0)
CsvHelper.Expressions.ExpressionManager.CreateGetMemberExpression (System.Linq.Expressions.Expression recordExpression, CsvHelper.Configuration.ClassMap mapping, CsvHelper.Configuration.MemberMap memberMap) (at &lt;9a1379e089274cc1baf4edcf480e4c6d&gt;:0)
CsvHelper.Expressions.ExpressionManager.CreateGetMemberExpression (System.Linq.Expressions.Expression recordExpression, CsvHelper.Configuration.ClassMap mapping, CsvHelper.Configuration.MemberMap memberMap) (at &lt;9a1379e089274cc1baf4edcf480e4c6d&gt;:0)
CsvHelper.Expressions.ObjectRecordWriter.CreateWriteDelegate[T] (T record) (at &lt;9a1379e089274cc1baf4edcf480e4c6d&gt;:0)
CsvHelper.Expressions.RecordWriter.GetWriteDelegate[T] (T record) (at &lt;9a1379e089274cc1baf4edcf480e4c6d&gt;:0)
CsvHelper.Expressions.RecordWriter.Write[T] (T record) (at &lt;9a1379e089274cc1baf4edcf480e4c6d&gt;:0)
CsvHelper.Expressions.RecordManager.Write[T] (T record) (at &lt;9a1379e089274cc1baf4edcf480e4c6d&gt;:0)
CsvHelper.CsvWriter.WriteRecords[T] (System.Collections.Generic.IEnumerable`1[T] records) (at &lt;9a1379e089274cc1baf4edcf480e4c6d&gt;:0)
Rethrow as WriterException: An unexpected error occurred.
IWriter state:
Row: 2
Index: 0
HeaderRecord:
2
CsvHelper.CsvWriter.WriteRecords[T] (System.Collections.Generic.IEnumerable`1[T] records) (at &lt;9a1379e089274cc1baf4edcf480e4c6d&gt;:0)
Util.SaveDataToCSV[T] (System.String path, System.Collections.Generic.List`1[T] data) (at Assets/Scripts/Util/Util.cs:178)
BehaviourMeasurement.SaveRawBehavioralDataToCSV () (at Assets/Scripts/Measurement/BehaviourMeasurementBehaviour.cs:1021)
BehaviourMeasurementBehaviour.OnDisable () (at Assets/Scripts/Measurement/BehaviourMeasurementBehaviour.cs:122)

And the empty CSV file has the following columns:

ActionZ,ActionX,TargetBallAgentHashCode,Item,Item,magnitude,sqrMagnitude,magnitude,sqrMagnitude,CollectBehaviouralData,ActionTime,Time

As you can see not the coordinates are written to the file but other properties (e.g. magnitude) which I do not want to include.
Why does CSVHelper not map the right properties into the CSV file?

答案1

得分: 2

我在 .Net Fiddle 中进行了一些尝试。

当为 BehaviouralData 使用显式映射时,似乎问题已经解决。

public class BehaviouralDataMap : ClassMap<BehaviouralData>
{
    public BehaviouralDataMap()
    {
        Map(o => o.ActionZ);
        Map(o => o.ActionX);
        Map(o => o.TargetBallAgentHashCode);
        Map(o => o.TargetBallLocalPosition);
        Map(o => o.CollectBehaviouralData);
        Map(o => o.ActionTime);
        Map(o => o.Time);
    }
}

当使用自动映射时,可能默认情况下会使用以下方式,但问题再次出现。

public class BehaviouralDataMap : ClassMap<BehaviouralData>
{
    public BehaviouralDataMap()
    {
        AutoMap(CultureInfo.InvariantCulture);
    }
}

也尝试了以下方式,但问题相同。

public class BehaviouralDataMap : ClassMap<BehaviouralData>
{
    public BehaviouralDataMap()
    {
        AutoMap(CultureInfo.InvariantCulture);
        Map(o => o.TargetBallLocalPosition).Ignore();
        Map(o => o.TargetBallLocalPosition.x);
        Map(o => o.TargetBallLocalPosition.y);
        Map(o => o.TargetBallLocalPosition.z);
    }
}

更新

实际上,这种显式映射也不起作用,只会将 Vector3 打印到 CSV 中,因此第二个映射似乎完全被忽略了。

以下方式似乎能够工作,尽管看起来相当显式,最多只能算是一种解决方法。

public class BehaviouralDataMap : ClassMap<BehaviouralData>
{
    public BehaviouralDataMap()
    {
        Map(o => o.ActionZ);
        Map(o => o.ActionX);
        Map(o => o.TargetBallAgentHashCode);
        Map(o => o.TargetBallLocalPosition.x);
        Map(o => o.TargetBallLocalPosition.y);
        Map(o => o.TargetBallLocalPosition.z);
        Map(o => o.CollectBehaviouralData);
        Map(o => o.ActionTime);
        Map(o => o.Time);
    }
}

但是老实说,至少对于写入部分来说,这似乎非常显式,最多只能算是一种解决方法。

英文:

I played around a bit in a .Net Fiddle.

When using an explicit map also for the BehaviouralData

public class BehaviouralDataMap : ClassMap&lt;BehaviouralData&gt;
{
    public BehaviouralDataMap()
    {
        Map(o =&gt; o.ActionZ);
        Map(o =&gt; o.ActionX);
        Map(o =&gt; o.TargetBallAgentHashCode);
        Map(o =&gt; o.TargetBallLocalPosition);
        Map(o =&gt; o.CollectBehaviouralData);
        Map(o =&gt; o.ActionTime);
        Map(o =&gt; o.Time);
    }
}

it seemed to be fixed.

When using automapping like

public class BehaviouralDataMap : ClassMap&lt;BehaviouralData&gt;
{
    public BehaviouralDataMap()
    {
        AutoMap(CultureInfo.InvariantCulture);
    }
}

which is probably what is going to be used by default anyway - it broke again.

Also tried this but same

public class BehaviouralDataMap : ClassMap&lt;BehaviouralData&gt;
{
    public BehaviouralDataMap()
    {
        AutoMap(CultureInfo.InvariantCulture);
        Map(o =&gt; o.TargetBallLocalPosition).Ignore();
        Map(o =&gt; o.TargetBallLocalPosition.x);
        Map(o =&gt; o.TargetBallLocalPosition.y);
        Map(o =&gt; o.TargetBallLocalPosition.z);
    }
}

Update

Actually this explicit mapping didn't work either -_-

It only printed Vector3 into the CSV so the second map seemed to be completely ignored.

Quite hideous but this seems to work

public class BehaviouralDataMap : ClassMap&lt;BehaviouralData&gt;
{
    public BehaviouralDataMap()
    {
        Map(o =&gt; o.ActionZ);
        Map(o =&gt; o.ActionX);
        Map(o =&gt; o.TargetBallAgentHashCode);
        Map(o =&gt; o.TargetBallLocalPosition.x);
        Map(o =&gt; o.TargetBallLocalPosition.y);
        Map(o =&gt; o.TargetBallLocalPosition.z);
        Map(o =&gt; o.CollectBehaviouralData);
        Map(o =&gt; o.ActionTime);
        Map(o =&gt; o.Time);
    }
}

BUT to be honest at least for the writer part this seems extremely explicit and at best would be a workaround.

答案2

得分: 1

您的基本问题是,正如derHugo在这个答案中指出的,当CsvHelper自动映射类型时,它会自动映射所有属性值的类型,而不是使用先前为这些类型注册的类映射。具体来说,在Vector3的情况下,它会自动映射其属性,而不是字段 xyz,最终导致崩溃。<sup>[1]</sup>。

那么,有哪些解决方法呢?除了手动映射 xyz 字段如derHugo的答案中建议的那样,您还有一些其他选项。

首先,您可以自动映射包含 Vector3 的类型,然后在之后修复引用。为此,请定义以下通用的 ClassMap&lt;T&gt; 和相关的扩展方法:

public class CustomAutoMap&lt;TClass&gt; : ClassMap&lt;TClass&gt;
{
    // 自动映射给定的类型,然后修复对 Vector3 的引用(如果有)。
    public CustomAutoMap()
    {
        AutoMap(CultureInfo.InvariantCulture);
        // 现在修复 Vector3 的引用。
        this.UpdateReferences&lt;Vector3Map, Vector3&gt;();
    }
}

public static partial class CsvExtensions
{
    // 更新自动映射的引用映射到不同的映射。
    // 请注意,这只应在 ClassMap 构造函数内部执行。
    public static ClassMap UpdateReferences&lt;TReferenceMap, TReferenceClass&gt;(this ClassMap classMap, params object[] constructorArgs) where TReferenceMap : ClassMap&lt;TReferenceClass&gt;
    {
        // TODO:嵌套引用的引用。
        for (int i = 0, count = classMap.ReferenceMaps.Count; i &lt; count; i++)
            if (classMap.ReferenceMaps[i].Data.Member.MemberType() == typeof(TReferenceClass))
                classMap.UpdateReference&lt;TReferenceMap, TReferenceClass&gt;(i, constructorArgs);
        return classMap;
    }

    public static MemberReferenceMap UpdateReference&lt;TReferenceMap, TReferenceClass&gt;(this ClassMap classMap, int index, params object[] constructorArgs) where TReferenceMap : ClassMap&lt;TReferenceClass&gt;
    {
        if (index &lt; 0 || index &gt;= classMap.ReferenceMaps.Count)
            throw new ArgumentException(nameof(index));
        var oldRef = classMap.ReferenceMaps[index];
        classMap.ReferenceMaps.RemoveAt(index);
        var newRef = classMap.References(typeof(TReferenceMap), oldRef.Data.Member, constructorArgs);
        if (oldRef.Data.Prefix != null)
            newRef.Prefix(oldRef.Data.Prefix);
        classMap.ReferenceMaps.Swap(index, classMap.ReferenceMaps.IndexOfWithHint(newRef, classMap.ReferenceMaps.Count - 1));
        return newRef;
    }

    static Type? MemberType(this MemberInfo m) =&gt;
        m switch
        {
            PropertyInfo p =&gt; p.PropertyType,
            FieldInfo f =&gt; f.FieldType,
            _ =&gt; null,
        };

    static int IndexOfWithHint&lt;T&gt;(this IList&lt;T&gt; list, T item, int hint) where T : class
    {
        if (hint &gt;= 0 &amp;&amp; hint &lt; list.Count &amp;&amp; list[hint] == item)
            return hint;
        return list.IndexOf(item);
    }

    static void Swap&lt;T&gt;(this IList&lt;T&gt; list, int i, int j)
    {
        if (i != j)
        {
            T temp = list[i];
            list[i] = list[j];
            list[j] = temp;
        }
    }
}

现在,将您的 BehaviouralData 序列化如下:

var config = new CsvConfiguration(CultureInfo.InvariantCulture);
using (var csv = new CsvWriter(writer, config))
{
    csv.Context.RegisterClassMap&lt;CustomAutoMap&lt;BehaviouralData&gt;&gt;();
    csv.WriteRecords(data);
}

这将产生以下结果:

ActionZ,ActionX,TargetBallAgentHashCode,CollectBehaviouralData,ActionTime,Time,x,y,z
0,0,1,False,01/01/0001 00:00:00,01/01/0001 00:00:00,1.1,1.1,1.1
0,0,2,False,01/01/0001 00:00:00,01/01/0001 00:00:00,2.1,2.1,2.1
0,0,3,False,01/01/0001 00:00:00,01/01/0001 00:00:00,3.1,3.1,3.1 

如果您希望 xyz 字段具有一些前缀,请在 TargetBallLocalPosition 中添加 [HeaderPrefix("prefix")],如下所示:

public class BehaviouralData
{
    [CsvHelper.Configuration.Attributes.HeaderPrefix("TargetBallLocation_")]
    public Vector3 TargetBallLocalPosition { get; set; }
    // 其余部分不变
}

这将得到以下结果:

ActionZ,ActionX,TargetBallAgentHashCode,CollectBehaviouralData,ActionTime,Time,TargetBallLocation_x,TargetBallLocation_y,TargetBallLocation_z
0,0,1,False,01/01/0001 00:00:00,01/01/0001 00:00:00,1.1,1.1,1.1
0,0,2,False,01/01/0001 00:00:00,01/01/0001 00:00:00,2.1,2.1,2.1
0,0,3,False,01/01/0001 00:00:00,01/01/0001 00:00:00,3.1,3.1,3.1

如果您需要更多对 BehaviouralData 格式的

英文:

Your basic problem is that, as derHugo noted in this answer, when CsvHelper automaps a type, it automaps the types of all property values, rather than using any class maps that might have been previously registered for those types. Specifically in the case of Vector3 it automaps its properties instead of the fields x, y and z, eventually leading to a crash.<sup>[1]</sup>.

So, what are your workarounds? Other than manually mapping the x, y and z fields manually as suggested in derHugo's answer, you have a couple of additional options.

Firstly, you could automap your types containing Vector3 and fix the references afterwards. To do this, define the following generic ClassMap&lt;T&gt; and associated extension methods:

public class CustomAutoMap&lt;TClass&gt; : ClassMap&lt;TClass&gt;
{
	// Automaps the given type then fixes up references to Vector3 (if any).
	public CustomAutoMap()
	{
		AutoMap(CultureInfo.InvariantCulture);
		// Now fix the Vector3 references.
		this.UpdateReferences&lt;Vector3Map, Vector3&gt;();
	}
}

public static partial class CsvExtensions
{
	// Update autoampped reference maps to a different map.
	// Note this should only be done within a ClassMap constructor itself.
	public static ClassMap UpdateReferences&lt;TReferenceMap, TReferenceClass&gt;(this ClassMap classMap, params object[] constructorArgs) where TReferenceMap : ClassMap&lt;TReferenceClass&gt;
	{
		// TODO: nested references of references.
		for (int i = 0, count = classMap.ReferenceMaps.Count; i &lt; count; i++)
			if (classMap.ReferenceMaps[i].Data.Member.MemberType() == typeof(TReferenceClass))
				classMap.UpdateReference&lt;TReferenceMap, TReferenceClass&gt;(i, constructorArgs);
		return classMap;
	}
	
	public static MemberReferenceMap UpdateReference&lt;TReferenceMap, TReferenceClass&gt;(this ClassMap classMap, int index, params object[] constructorArgs) where TReferenceMap : ClassMap&lt;TReferenceClass&gt;
	{
		if (index &lt; 0 || index &gt;= classMap.ReferenceMaps.Count)
			throw new ArgumentException(nameof(index));
		var oldRef = classMap.ReferenceMaps[index];
		classMap.ReferenceMaps.RemoveAt(index);
		var newRef = classMap.References(typeof(TReferenceMap), oldRef.Data.Member, constructorArgs);
		if (oldRef.Data.Prefix != null)
			newRef.Prefix(oldRef.Data.Prefix);
		classMap.ReferenceMaps.Swap(index, classMap.ReferenceMaps.IndexOfWithHint(newRef, classMap.ReferenceMaps.Count - 1));
		return newRef;
	}
	
	static Type? MemberType(this MemberInfo m) =&gt;
		m switch
		{
			PropertyInfo p =&gt; p.PropertyType,
			FieldInfo f =&gt; f.FieldType,
			_ =&gt; null,
		};

	static int IndexOfWithHint&lt;T&gt;(this IList&lt;T&gt; list, T item, int hint) where T : class
	{
		if (hint &gt;= 0 &amp;&amp; hint &lt; list.Count &amp;&amp; list[hint] == item)
			return hint;
		return list.IndexOf(item);
	}
									
    static void Swap&lt;T&gt;(this IList&lt;T&gt; list, int i, int j)
    {
        if (i != j)
        {
            T temp = list[i];
            list[i] = list[j];
            list[j] = temp;
        }
    }
}

And now serialize your BehaviouralData as follows:

var config = new CsvConfiguration(CultureInfo.InvariantCulture);
using (var csv = new CsvWriter(writer, config))
{
	csv.Context.RegisterClassMap&lt;CustomAutoMap&lt;BehaviouralData&gt;&gt;();
	csv.WriteRecords(data);
}

Which results in:

ActionZ,ActionX,TargetBallAgentHashCode,CollectBehaviouralData,ActionTime,Time,x,y,z
0,0,1,False,01/01/0001 00:00:00,01/01/0001 00:00:00,1.1,1.1,1.1
0,0,2,False,01/01/0001 00:00:00,01/01/0001 00:00:00,2.1,2.1,2.1
0,0,3,False,01/01/0001 00:00:00,01/01/0001 00:00:00,3.1,3.1,3.1 

If you would like the x, y and z fields to have some prefix, add [HeaderPrefix(&quot;prefix&quot;)] to TargetBallLocalPosition like so:

public class BehaviouralData
{
	[CsvHelper.Configuration.Attributes.HeaderPrefix(&quot;TargetBallLocation_&quot;)]
	public Vector3 TargetBallLocalPosition { get; set; }
    // Remainder unchanged

And you will get instead:

ActionZ,ActionX,TargetBallAgentHashCode,CollectBehaviouralData,ActionTime,Time,TargetBallLocation_x,TargetBallLocation_y,TargetBallLocation_z
0,0,1,False,01/01/0001 00:00:00,01/01/0001 00:00:00,1.1,1.1,1.1
0,0,2,False,01/01/0001 00:00:00,01/01/0001 00:00:00,2.1,2.1,2.1
0,0,3,False,01/01/0001 00:00:00,01/01/0001 00:00:00,3.1,3.1,3.1

If you need more manual control over the formatting of BehaviouralData you will need to create a manual class map. When you do, use the method ClassMap&lt;TClass&gt;.References&lt;TClassMap&gt;(Expression&lt;Func&lt;TClass, object&gt;&gt; expression, params object[]) rather than ClassMap&lt;TClass&gt;.Map() to reference the contents of Vector3 using your Vector3Map:

public class BehaviouralDataMapExplicit : ClassMap&lt;BehaviouralData&gt;
{
	public BehaviouralDataMapExplicit()
	{
		Map(m =&gt; m.ActionZ);
		Map(m =&gt; m.ActionX);
		Map(m =&gt; m.TargetBallAgentHashCode);
		References&lt;Vector3Map&gt;(m =&gt; m.TargetBallLocalPosition)
			.Prefix(&quot;TargetBallLocation_&quot;);
		Map(m =&gt; m.CollectBehaviouralData);
		Map(m =&gt; m.ActionTime);
		Map(m =&gt; m.Time);
	}
}

Demo fiddle here.


<sup>[1]</sup> CsvHelper seems to have a bug here. Specifically, for classes that do not implement IEnumerable it attempts to automap indexed properties, resulting in the exception you see. This is a straightforward bug in CsvHelper, it should check that PropertyInfo.GetIndexParameters().Length == 0 before automapping any property discovered by reflection. But even if CsvHelper fixed this bug, you still wouldn't get what you want, which is the three fields of Vector3, not its properties.

huangapple
  • 本文由 发表于 2023年7月6日 21:41:40
  • 转载请务必保留本文链接:https://go.coder-hub.com/76629491.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定