-1

I have the following class to create objects from a delimited line:

public class Mapper<T>
{
    public T Map(string line, char delimiter)
    {
        if(String.IsNullOrEmpty(line))
            throw new ArgumentNullException(nameof(line));

        if (Char.IsWhiteSpace(delimiter))
            throw new ArgumentException(nameof(delimiter));

        var splitString =  line.Split(delimiter);

        var properties = typeof(T).GetProperties();

        if(properties.Count() != splitString.Count())
            throw new InvalidOperationException($"Row has {splitString.Count()} columns but object has {properties.Count()}.");

        var obj = Activator.CreateInstance<T>();

        for (var i = 0; i < splitString.Count(); i++)
        {
            var prop = properties[i];
            var propType = prop.PropertyType;
            var valType = Convert.ChangeType(splitString[i], propType);
            prop.SetValue(obj, valType);
        }

        return (T)obj;
    }
}

If I call the map method with a delimited string it will populate all of the properties on the object with the delimited values from the line.

However when I call this from the following:

public class CsvStreamReader<T>
{
    private readonly Mapper<T> _mapper;
    public CsvStreamReader(Mapper<T> mapper)
    {
        _mapper = mapper;
    } 



    public IEnumerable<T> ReadCsvFile(string filePath, bool hasHeader)
    {
        if(hasHeader)
            return File.ReadAllLines(filePath)
                       .Skip(1)
                       .Select(x => _mapper.Map(x, ','));

        return File.ReadAllLines(filePath)
                   .Select(x => _mapper.Map(x, ','));
    } 
}

It will return a list of T but all of the properties will be null and won't have been set.

Update: Just realised my class wasn't a class but was actually a struct.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
  • use debugger to see why its returning null. I don't think linq query can cause such problem. because its not related to that. what is `_mapper`? – M.kazem Akhgary Jan 16 '17 at 16:51
  • 3
    Please read [ask] and provide a [mcve]. For all we know you pass the wrong `T` to your mapper, or your file is empty or contains no data. You also can't rely on `GetProperties()` to return properties in any particular order. The Linq part is irrelevant anyway. – CodeCaster Jan 16 '17 at 16:51
  • Sorry _mapper contains the Map function. – edwin the duck Jan 16 '17 at 16:51
  • You're assuming that the order of the splitted string is the same as the order of the properties returned with reflection? Looks vulnerable. – Jeroen van Langen Jan 16 '17 at 16:53
  • Just realised my class wasn't a class but was actually a struct, oops! – edwin the duck Jan 16 '17 at 17:15

1 Answers1

2

In order to make your Mapper<T> work when T is a value type, you need to set its properties while boxed as an object. Create your obj as an object using the the non-generic Activator.CreateInstance(typeof(T)), set its properties using reflection, then finally cast the it to the required type when returning it:

public class Mapper<T>
{
    readonly List<PropertyInfo> properties = typeof(T).GetProperties().OrderBy(p => p.Name).ToList();
    
    public T Map(string line, char delimiter)
    {
        if (String.IsNullOrEmpty(line))
            throw new ArgumentNullException("line");

        if (Char.IsWhiteSpace(delimiter))
            throw new ArgumentException("delimiter");

        var splitString = line.Split(delimiter);

        if (properties.Count() != splitString.Count())
            throw new InvalidOperationException(string.Format("Row has {0} columns but object has {1}", splitString.Count(), properties.Count()));

        // Create as a reference (boxed if a value type).
        object obj = Activator.CreateInstance(typeof(T));

        // Set the property values on the object reference.
        for (var i = 0; i < splitString.Count(); i++)
        {
            var prop = properties[i];
            var propType = prop.PropertyType;
            var valType = Convert.ChangeType(splitString[i], propType);
            prop.SetValue(obj, valType);
        }

        // Cast to the return type unboxing if required.
        return (T)obj;
    }
}

Sample fiddle.

Note that your code should not depend on the order of properties returned by Type.GetProperties(). From the docs:

The GetProperties method does not return properties in a particular order, such as alphabetical or declaration order. Your code must not depend on the order in which properties are returned, because that order varies.

Thus I modified your code to order by name. You might choose to select another strategy such as using data member order for data contract types.

Finally, you might want to reconsider your design of using mutable structs, see Why are mutable structs “evil”? for some reasons why. To restrict your Mapper<T> to work only for reference types, you can add the following where constraint:

public class Mapper<T> where T : class
{
}
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
dbc
  • 104,963
  • 20
  • 228
  • 340