Home > database >  Why do I have to Ignore all other properties in AutoMapper when using records?
Why do I have to Ignore all other properties in AutoMapper when using records?

Time:04-27

public record Destination(double X, double Y);

public struct Source
{

    public double X { get; set; }

    public Potato Potato { get; set; }

    public double Z { get; set; }
}


public struct Potato
{
    public double Y { get; set; }
}

 public MappingProfile()
{
   CreateMap<Source, Destination>();
   .ForCtorParam(nameof(Destination.Y), e => e.MapFrom(x => x.Potato.Y))
   .ForAllOtherMembers(x => x.Ignore());
}

To map source to destination, I need to manually map one of the children. However, automapper will then give an extremly confusing message, saying Y property is unmapped.

Unmapped members were found. Review the types and members below.
    Add a custom mapping expression, ignore, add a custom resolver, or modify the source/destination type
    For no matching constructor, add a no-arg ctor, add optional arguments, or map all of the constructor parameters
    ==========================================================================================================
    Source -> Destination (Destination member list)


Unmapped properties:
Y

I found by adding the line to ignore all other members, it would 'solve' the issue. Is there a better way of preventing this error occuring?

The error message mentions mapping all the constructor parameters, but even if I add .ForCtorParam(nameof(Destination.X), e => e.MapFrom(x => x.X)) the error still occurs.

CodePudding user response:

This is already solved in 11. Because you cannot upgrade, you'll have to ignore all those properties. The problem is that the properties have setters and in AM 10 the properties are not considered mapped even if they're already mapped through the constructor.

Another solution is to use a struct (or class) without setters instead of record.

CodePudding user response:

I created the following class to help cover my needs, since I can't upgrade to newer AutoMapper versions.

It is naive, and assumes that all properties on a record should be mapped to constructor.

Benefits:

  1. Will automatically map all properties with same name
  2. Can use lambda arguments instead of nameof
  3. Don't need to call ignore other members
  4. Will throw exception if a property is not mapped

To use it, in MappingProfile:

new RecordMapBuilder<TSource, TDestination>(this)
            .Map(x => x.Foo, x => x.Offset.Foo)
            .Build();
        public class RecordMapBuilder<TSource, TDestination>
        {
            private readonly Profile _profile;
            private readonly bool _autoMapMatchingProperties;
            private readonly Dictionary<string, Expression<Func<TSource, object>>> _ctorParamActions = new();
            
            public RecordMapBuilder(Profile profile, bool autoMapMatchingProperties = true)
            {
                _profile = profile;
                _autoMapMatchingProperties = autoMapMatchingProperties;
            }

            public void Build()
            {
                var map = _profile.CreateMap<TSource, TDestination>();

                var unmappedDestinationProperties = new HashSet<string>(
                    typeof(TDestination)
                    .GetProperties()
                    .Where(e => !_ctorParamActions.ContainsKey(e.Name))
                    .Select(e => e.Name));

                var sourceProperties = new HashSet<string>(typeof(TSource)
                    .GetProperties()
                    .Select(e => e.Name));

                var mappableProperties = unmappedDestinationProperties.Intersect(sourceProperties).ToHashSet();
                var unMappableProperties = unmappedDestinationProperties.Except(sourceProperties).ToHashSet();

                if (unMappableProperties.Any())
                {
                    var properties = string.Join(", ", unMappableProperties);
                    throw new InvalidOperationException($"Not all properties mapped for type {typeof(TSource)} -> {typeof(TDestination)}: {properties}");
                }

                if (_autoMapMatchingProperties)
                {
                    foreach (var name in mappableProperties)
                    {
                        map.ForCtorParam(name, x => x.MapFrom(name));
                    }
                }

                foreach (var kv in _ctorParamActions)
                {
                    map.ForCtorParam(kv.Key, x => x.MapFrom(kv.Value));
                }

                map.ForAllOtherMembers(x => x.Ignore());
            }

            public RecordMapBuilder<TSource, TDestination> Map(Expression<Func<TDestination, object>> destination, Expression<Func<TSource, object>> source)
            {
                var name = GetName(destination);
                _ctorParamActions[name] = source;
                return this;
            }

            private static string GetName(Expression<Func<TDestination, object>> destination)
            {
                {
                    if (destination.Body is UnaryExpression ue && ue.Operand is MemberExpression me)
                    {
                        return me.Member.Name;
                    }
                }

                {
                    if (destination.Body is MemberExpression me)
                    {
                        return me.Member.Name;
                    }
                }

                throw new InvalidOperationException($"Unhandled expression of type: {destination.Body.GetType()}");
            }
  • Related