Search code examples
c#mappingautomapperobject-object-mapping

How to specify automapper exception caused data?


I am reading the coordinate values from a file using 3rd tool, and tool casts data to a Point class collection.

public class Point
{
    public string Lon {get;set;}
    public string Lat {get;set;}
    public string Elevation {get;set;}
}

And I want to map Point class to PointEntity using automapper.

public class PointEntity
{
    public float Lon {get;set;}
    public float Lat {get;set;}
    public float Elevation {get;set;}
}

I created a Map using Automapper.

public class LocationProfile : Profile
{
    public LocationProfile()
    {
        CreateMap<Point, PointEntity>();
    }
}

And I have created a generic mapper extension to map List.

public static class MapperHelper
{
    public static TDest MapTo<TDest>(this object src)
    {
        return (TDest)AutoMapper.Mapper.Map(src, src.GetType(), typeof(TDest));
    }
}

IList<Point> data = readData<Point>("file");
var mapped = data.MapTo<PointEntity>();

But sometimes users may have wrong type entries in file like following.

Lon    Lat    Elevation
---    ---    ---------
11.5   25.6   80.56
12ab   89.87  14.83
1.7    x.8    9.3

In this situation, the code throw exception.

So how can I find which row and value is wrong type? (For example row1 and Lon value is wrong)


Solution

  • Change your design thinking. Mapping is simply transferring one data to another data. And automapper is very good at that task. Keep it simple. So, keep that as simple as possible. I read that you want to perform a validation during mapping and provide information about it. That make mapping more complex. In my design I wouldn't let this happen during mapping. Validate this information before mapping. Iterate this before mapping by using an validation rule class. Or validates the data at the time of reading so that you can immediately inform a line and column position.

    Example

    Use the ConvertUsing (but not needed if validated before) Step to Implementation

    public class LocationProfile : Profile
    {
        public LocationProfile()
        {
            CreateMap<Point, PointEntity>().ConvertUsing<PointEntityConverter>();
        }
    }
    
    public class PointEntityConverter : ITypeConverter<Point, PointEntity>
    {
        public PointEntity Convert(Point source, PointEntity destination, ResolutionContext context)
        {
            if (destination == null)
                destination = new PointEntity();
    
            destination.Lon = Parse(source.Lon, nameof(source.Lon));
            destination.Lat = Parse(source.Lat, nameof(source.Lat));
            destination.Elevation = Parse(source.Elevation, nameof(source.Elevation));
    
            return destination;
        }
    
        private float Parse(string s, string paramName)
        {
            if (float.TryParse(s, out float result))
                return result;
    
            throw new ArgumentException($"Invalide value ({s}) for parameter {paramName}", paramName);
        }
    

    Implementation

    private readonly IMapper mapper;
    private readonly IValidator<Point> validator;
    
    public HowToSpecifyAutomapperExceptionCausedData(IMapper mapper)
    {
        this.mapper = mapper;
        this.validator = new PointValidator();
    }
    
    public void Example()
    {
        var data = readData<Point>("file");
    
        for (int i = 0; i < data.Count; i++)
            validator.AssertValide(data[i], (m, p) => OnInvalidArgument(m, p, i));
    
        var mapped = mapper.Map<IList<PointEntity>>(data);
    }
    
    private void OnInvalidArgument(string message, string paramName, int index)
    {
        throw new ArgumentException($"{message} on index {index}", paramName);
    }
    

    Validator

    internal interface IValidator<T>
    {
        void AssertValide(T value, InvalideArgumentHandler handler);
    }
    
    internal delegate void InvalideArgumentHandler(string message, string paramName);
    
    internal class PointValidator : IValidator<Point>
    {
        public void AssertValide(Point value, InvalideArgumentHandler handler)
        {
            AssertValideFloat(value.Lon, nameof(value.Lon), handler);
            AssertValideFloat(value.Lat, nameof(value.Lat), handler);
            AssertValideFloat(value.Elevation, nameof(value.Elevation), handler);
        }
    
        private void AssertValideFloat(string s, string paramName, InvalideArgumentHandler handler)
        {
            if (!float.TryParse(s, out float result))
                handler($"Invalide value ({s}) for parameter {paramName}", paramName);
        }
    }
    

    Why using a delegate? Other example by collecting all (also for each parameter [lon, lat, ...]) invalide parameters. Here see now the power using a handler for it.

    public void Example()
    {
        var data = readData<Point>("file");
    
        var errors = new List<string>();
    
        for (int i = 0; i < data.Count; i++)
            validator.AssertValide(data[i], (m, p) => errors.Add($"{m} on index {i}"));
    
        if (errors.Any()) //using System.Linq;
            throw new ApplicationException(string.Join(Environment.NewLine, errors));
    
        var mapped = mapper.Map<IList<PointEntity>>(data);
    }