CsvHelper icon indicating copy to clipboard operation
CsvHelper copied to clipboard

Map().Index(0).Constant("abc") throws System.InvalidOperationException - Member is not a property or a field.

Open kl1mm opened this issue 3 years ago • 5 comments

Hi, we use the MemberMap.Constant to always write a constant value in a format given to us.

 this.Map().Index(0).Constant("const value");

Since the update from v25 to the latest version we get the error:

System.InvalidOperationException
Member is not a property or a field.
   at CsvHelper.Expressions.ExpressionManager.CreateGetFieldExpression(MemberMap memberMap)
   at CsvHelper.Expressions.ExpressionManager.CreateMemberAssignmentsForMapping(ClassMap mapping, List`1 assignments)
   at CsvHelper.Expressions.ObjectRecordCreator.CreateCreateRecordDelegate(Type recordType)
   at CsvHelper.Expressions.RecordCreator.GetCreateRecordDelegate(Type recordType)
   at CsvHelper.Expressions.RecordCreator.Create[T]()
   at CsvHelper.Expressions.RecordManager.Create[T]()
   at CsvHelper.CsvReader.GetRecord[T]()

To Reproduce Steps to reproduce the behavior:

  1. Create a ClassMap<T> with a MemberMap this.Map().Index(0).Constant("const value");
  2. Create a CsvReader and read the record -> csvReader.GetRecord<T>()
  3. System.InvalidOperationException is thrown

Expected behavior It reads the record without any error

Additional context For upgrading i used our migration-website but i can't find anything on this topic.

Any recommendations? cheers mMilk

kl1mm avatar Apr 04 '22 07:04 kl1mm

Can you create an example that reproduces this issue? This works for me.

void Main()
{
    var s = new StringBuilder();
    s.Append("Id,Name\r\n");
    s.Append("1\r\n");
    var config = new CsvConfiguration(CultureInfo.InvariantCulture)
    {
    };
    using (var reader = new StringReader(s.ToString()))
    using (var csv = new CsvReader(reader, config))
    {
        csv.Context.RegisterClassMap<FooMap>();
        csv.GetRecords<Foo>().ToList().Dump();
    }
}

private class Foo
{
    public int Id { get; set; }
    public string Name { get; set; }
}

private class FooMap : ClassMap<Foo>
{
    public FooMap()
    {
        Map(m => m.Id).Index(0);
        Map(m => m.Name).Index(1).Constant("bar");
    }
}

JoshClose avatar May 09 '22 18:05 JoshClose

I ran into a similar issue. I think it's caused by not mapping a property to the constant mapping.

In my case, I wanted to just create a constant "Read Date" column for every record, so I thought I could do as @kl1mm did and set an empty mapping to a constant. It would be something like this:

private class Foo
{
    public int Id { get; set; }
    public string Name { get; set; }
}
public class FooMap : ClassMap<Foo>
{
    public FooMap()
    {
        // Print the properties in Foo
        Map(m => m.Id).Index(0);
        Map(m => m.Name).Index(1).Constant("bar");

        // Print a constant that Foo doesn't need, but is useful in exporting
        Map().Index(2).Name("Foo Read Date").Constant(DateTime.Now);
    }
}

And the output would look something like

Id Name Foo Read Date
1 Foo1 4/27/2022
2 Foo2 4/27/2022

For now, my workaround has been to add a dummy property inside of Foo that maps to it because I don't want to have to do any custom "WriteField()" calls for those blanks. Is it possible to do this without the dummy field?

bburgess19 avatar Jun 23 '22 20:06 bburgess19

Are there any new findings regarding to the issue? cheers mMilk

kl1mm avatar Nov 08 '22 11:11 kl1mm

Hi, because the package is developing rapidly and this problem still exists, I have found a small workaround hack for our project via reverse engineering.

/// <summary>
/// All the stuff for Workaround: https://github.com/JoshClose/CsvHelper/issues/1967
///
/// this.Map(     ).Index(0).Constant("myValue"); not possible so we workaround it with ConstantFixed
///           ^^^ NOTHING here cause we dont need a field/prop here and its allowed from CSVHelper-API
/// </summary>
internal static class MemberMapExtensions
{
    public static MemberMap<TClass, TMember> ConstantFixed<TClass, TMember>(this MemberMap<TClass, TMember> mm, TMember constantValue)
    {
        mm.Constant(constantValue);

        // Hacky stuff here
        var setter = mm.Data.GetType().GetProperty(nameof(mm.Data.Member))?.GetSetMethod(true);
        setter?.Invoke(mm.Data, new object?[] { new MyPropInfo(constantValue) });
        
        return mm;
    }

    private class MyPropInfo : PropertyInfo
    {
        private readonly object? constantValue;

        public MyPropInfo(object? constantValue) => this.constantValue = constantValue;

        public object? Getter() => this.constantValue;

        public override object[] GetCustomAttributes(bool inherit) => Array.Empty<object>();
        public override object[] GetCustomAttributes(Type attributeType, bool inherit) => Array.Empty<object>();
        public override bool IsDefined(Type attributeType, bool inherit) => false;
        public override Type? DeclaringType => null;
        public override string Name => "dynamic";
        public override Type? ReflectedType => null;
        public override MethodInfo[] GetAccessors(bool nonPublic) => Array.Empty<MethodInfo>();
        public override MethodInfo? GetGetMethod(bool nonPublic) => this.GetType().GetMethod(nameof(this.Getter));

        public override ParameterInfo[] GetIndexParameters() => Array.Empty<ParameterInfo>();
        public override MethodInfo? GetSetMethod(bool nonPublic) => null;
        public override object? GetValue(object? obj, BindingFlags invokeAttr, Binder? binder, object?[]? index, CultureInfo? culture) => null;
        public override void SetValue(object? obj, object? value, BindingFlags invokeAttr, Binder? binder, object?[]? index, CultureInfo? culture) { }
        public override PropertyAttributes Attributes => PropertyAttributes.None;
        public override bool CanRead => true;
        public override bool CanWrite => false;
        public override Type PropertyType => typeof(string);
    }
}

And usage


internal sealed class MyMap: ClassMap<MyClass>
{
    public MyMap()
    {
        this.Map().Index(0).ConstantFixed("<"); // use ConstantFixed insted of Constant
        this.Map().Index(1).ConstantFixed(DateTime.Now);
        this.Map().Index(2).ConstantFixed(Constants.MyClass.ConstValue);
        this.Map(m => m.Id).Index(3);
        this.Map(m => m.Name).Index(4);
        this.Map().Index(5).ConstantFixed(">");
    }
}

For sure its not perfect, but for the moment it helps us. cheers mMilk

kl1mm avatar Jan 17 '23 12:01 kl1mm