3

I have a lots of DTOs that must be mapped to my domain's objects. In general, the mapping to monetary values need to apply a rounding rule. That's apply for more than 95% of the cases, but I have some data that need a different rounding rule. I was planned to do the next:

1) Create a ITypeConverter for the general rounding rule, that apply for default in for this type conversions.

2) Create a ValueResolver for the special rounding cases, and specify their use in the member mapping.

I created a test case to try my approach, but after the application of the ValueResolver for the special cases, AutoMapper uses the TypeConverter to get the final value, using the general rule.

That's a wrong approach?? May be I'm missing something??

Then the test case:

[TestFixture]
public class RoundingTests
{
    public class MoneyConverter : ITypeConverter<decimal, decimal>
    {
        public decimal Convert(ResolutionContext context)
        {
            return Math.Round((decimal)context.SourceValue, 2, MidpointRounding.AwayFromZero);
        }
    }

    public class Money12Resolver : ValueResolver<decimal, decimal>
    {
        protected override decimal ResolveCore(decimal source)
        {
            return Math.Round(source, 12, MidpointRounding.AwayFromZero);
        }
    }

    public class PercentResolver : ValueResolver<decimal, decimal>
    {
        protected override decimal ResolveCore(decimal source)
        {
            return Math.Round(source, 4, MidpointRounding.AwayFromZero);
        }
    }
    internal class Source
    {
        public decimal Price { get; set; }
        public decimal Percent { get; set; }
        public decimal Prorate { get; set; }
    }

    internal class Destination
    {
        public decimal Price { get; set; }
        public decimal Percent { get; set; }
        public decimal Prorate { get; set; }
    }

    [Test]
    public void MappingTest()
    {
        Mapper.CreateMap<decimal, decimal>().ConvertUsing<MoneyConverter>();

        Mapper.CreateMap<Source, Destination>()
            .ForMember(d => d.Percent, o => o.ResolveUsing<PercentResolver>().FromMember(s => s.Percent))
            .ForMember(d => d.Prorate, o => o.ResolveUsing<Money12Resolver>().FromMember(s => s.Prorate));

        Mapper.AssertConfigurationIsValid();

        var source = new Source
                     {
                         Price = 12345.6789m,
                         Percent = 0.123456m,
                         Prorate = 123.123451234512345m
                     };

        var convertion = Mapper.Map<Destination>(source);

        Assert.That(convertion, Is.Not.Null);

        Assert.That(convertion.Price, Is.EqualTo(12345.68m));
        Assert.That(convertion.Percent, Is.EqualTo(0.1235m));
        Assert.That(convertion.Prorate, Is.EqualTo(123.123451234512m));
    }
}

Test Result:

  Expected: 0.1235m
  But was:  0.12m
Mario Fuentes
  • 31
  • 1
  • 2

1 Answers1

0

You're telling AutoMapper to convert all decimal->decimal mappings using your MoneyConverter. AutoMapper does, in fact, use your resolver (set a break point to see), but the result of resolving is a decimal value that is then used by the MoneyConverter you applied.

Note: This appears to be the design of AutoMapper; I don't see a way to override a type converter.

Not all of your decimal properties represent money, so you may want to take a different approach. Also ask yourself, are you rounding purely for values used for presentation, or are you potentially losing precision you'd want to retain in those domain objects? If you really need the rounding, you could be explicit and set a resolver per member, skipping the converter completely. Alternatively, you could ignore the members which are exceptions and handle that with a .ConstructUsing(...).

But since your original question relates to using a Resolver and Converter for the same type, here's one way you can make it work:


Basically we want the converter to skip the default conversion for certain properties. We can't do it via configuration, so we'll have to do it at run-time. Assuming you have access to the Destination class, just decorate properties with non-default behavior using a custom attribute.

class PercentAttribute : Attribute
{   }

class Destination
{
    [Percent]
    public decimal Percent { get; set; }
    ...
}

In your converter, you can then look for properties which are [Percent] and return the source value, which came from your PercentResolver.

public class MoneyConverter : ITypeConverter<decimal, decimal>
{
    public decimal Convert(ResolutionContext context)
    {
        var propInfo = context.PropertyMap.DestinationProperty.MemberInfo;
        bool isPercent = propInfo.GetCustomAttributes(typeof(PercentAttribute), true).Any();
        if (isPercent) return (decimal)context.SourceValue;

        return Math.Round((decimal)context.SourceValue, 2, MidpointRounding.AwayFromZero);
    }
}

If you're going to go that route, just have the converter do Money, Percent, and Money12 conversions based on attributes, and skip the resolvers completely.

drew
  • 306
  • 2
  • 11