8

I have a DTO I want to map to an entity. The entity has some properties decorated with the MaxLength attribute.

I would like AutoMapper to truncate all the strings coming from the DTO when mapping to my entity according to the MaxLength for each property, so that I don't get validation errors when saving the entity.

So, if entity is defined like this:

public class Entity 
{
    [MaxLength(10)]
    string Name { get; set; }
}

I would like that doing this:

var myDto = new MyDto() { Name = "1231321312312312312312" };
var entity = Mapper.Map<Entity>(myDto);

The resulting entity should have its Name limited to a maximum of 10 characters.

SuperJMN
  • 13,110
  • 16
  • 86
  • 185
  • 1
    Honestly you should be controlling that on your DTO, and in any UI that populates it. – juharr Apr 12 '18 at 14:49
  • That's certainly doable with custom mapping but I personally don't think it's a good idea., although I don't know much about your situation. – JuanR Apr 12 '18 at 14:49
  • @juharr Sorry, but I cannot do that. I don't have control over either classes. – SuperJMN Apr 12 '18 at 14:50
  • 2
    @SuperJMN Then I'd suggest having whoever does have control fix it. Assuming that the data is coming from a user having it truncate instead of warning up front seems bad. If it's data that's being transferred it also seems wrong to just truncate instead of at least throwing out a warning or logging that it's being truncated, which means a bit more work than a simple mapping. – juharr Apr 12 '18 at 14:56
  • I cannot do it in my situation. The entity is generated automatically by AutoFixture from an interface and mapped to the entity with the restrictions. Since I don't have any way to filter the data, this is the best option I have. So, please, if you know how to do it, help me :) – SuperJMN Apr 12 '18 at 15:00
  • I wasn't talking about changing the entity, I was talking about changing the DTO. – juharr Apr 12 '18 at 15:01
  • @juharr I don't have a DTO, I used just to illustrate the scenario. What I have is a proxy returned by AutoFixture, constructed from an interface. So no, I cannot modify it. Sorry. I had to simplify the question. If I told you the full explanation, the question would have been skipped by everyone :) – SuperJMN Apr 12 '18 at 15:07
  • AutoMapper has a `ForMember` overload that takes a `Expression>` that is applied to the source value. I have no idea what that expression would have to like like though. – Hintham Apr 12 '18 at 15:38
  • I think I know what you mean by "complicated" questions getting skipped, but usually over simplified ones like this get down voted and closed for lack of research and not showing code. You've been lucky so far. – juharr Apr 12 '18 at 15:46
  • I'm confused. AutoFixture seems to be for simplifying unit tests, so I'm not sure how you're using it. Ultimately it sounds like you need to do what @Hintham suggests and just fill in the reflection code to get the length from the attribute on the entity. – juharr Apr 12 '18 at 15:49
  • @juharr Yes, but AutoFixture doesn't know anything about the length restrictions (and it shouldn't). It just generates strings randomly, that is the way it should work. About oversimplifying, I think the question is well explained and the reason it doesn't have any code is that I cannot show anything because I don't have anything :) – SuperJMN Apr 12 '18 at 16:07
  • [this](https://stackoverflow.com/questions/31706697/custom-mapping-with-automapper) ? – Stavm Apr 12 '18 at 16:23
  • @stavm, no, that way I woulds have to specify the properties one by one. I want to do it for every member of type string having a MaxLength attribute. – SuperJMN Apr 12 '18 at 16:48
  • I assume that these are integration tests if your hitting the DB, otherwise you should be mocking that out for unit tests. If you have requirements on the values you want to test with that AutoFixture cannot accommodate then I'm not sure AutoFixture is the correct tool. – juharr Apr 12 '18 at 16:57

3 Answers3

4

I'm not sure that it's a good place to put that logic, but here is an example that should work in your case (AutoMapper 4.x): Custom Mapping with AutoMapper

In this example, I'm reading a custom MapTo property on my entity, you could do the same with MaxLength.

Here a full example with the current version of AutoMapper (6.x)

class Program
{
    static void Main(string[] args)
    {
        Mapper.Initialize(configuration =>
            configuration.CreateMap<Dto, Entity>()
                .ForMember(x => x.Name, e => e.ResolveUsing((dto, entity, value, context) =>
                {
                    var result = entity.GetType().GetProperty(nameof(Entity.Name)).GetCustomAttribute<MaxLengthAttribute>();
                    return dto.MyName.Substring(0, result.Length);
                })));

        var myDto = new Dto { MyName = "asadasdfasfdaasfasdfaasfasfd12" };
        var myEntity = Mapper.Map<Dto, Entity>(myDto);
    }
}

public class Entity
{
    [MaxLength(10)]
    public string Name { get; set; }
}

public class Dto
{
    public string MyName { get; set; }
}
Bidou
  • 7,378
  • 9
  • 47
  • 70
  • Hey @bidou! Thanks for the answer. Please, see my "autoanswer" post. I got something! Please, review it. – SuperJMN Apr 13 '18 at 08:45
0

For Automapper 4.x I got something with this code:

class Program
{
    static void Main(string[] args)
    {
        Mapper.Initialize(configuration =>
            configuration.CreateMap<Dto, Entity>()
                .ForMember(x => x.Name, e => e.MapFrom(d => d.MyName))
                .ForMember(x => x.Free, e => e.MapFrom(d => d.Free))
                .ForMember(x => x.AnotherName, e => e.MapFrom(d => d.Another))
                .LimitStrings());

        var dto = new Dto() { MyName = "asadasdfasfdaasfasdfaasfasfd12", Free = "ASFÑLASJDFÑALSKDJFÑALSKDJFAMLSDFASDFASFDASFD", Another = "blalbalblalblablalblablalblablalblablabb"};
        var entity = Mapper.Map<Entity>(dto);
    }
}

public static class Extensions
{
    public static IMappingExpression<TSource, TDestination> LimitStrings<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
    {
        var sourceType = typeof(TSource);
        var destinationType = typeof(TDestination);

        var existingMaps = Mapper.GetAllTypeMaps().First(b => b.SourceType == sourceType && b.DestinationType == destinationType);

        var propertyMaps = existingMaps.GetPropertyMaps().Where(map => !map.IsIgnored() && ((PropertyInfo)map.SourceMember).PropertyType == typeof(string));

        foreach (var propertyMap in propertyMaps)
        {
            var attr = propertyMap.DestinationProperty.MemberInfo.GetCustomAttribute<MaxLengthAttribute>();
            if (attr != null)
            {
                expression.ForMember(propertyMap.DestinationProperty.Name,
                    opt => opt.ResolveUsing(new StringLimiter(attr.Length, (PropertyInfo) propertyMap.SourceMember)));
            }                                            
        }

        return expression;
    }
}

public class StringLimiter : IValueResolver
{
    private readonly int length;
    private readonly PropertyInfo propertyMapSourceMember;

    public StringLimiter(int length, PropertyInfo propertyMapSourceMember)
    {
        this.length = length;
        this.propertyMapSourceMember = propertyMapSourceMember ?? throw new ArgumentNullException(nameof(propertyMapSourceMember));
    }

    public ResolutionResult Resolve(ResolutionResult source)
    {
        var sourceValue = (string)propertyMapSourceMember.GetValue(source.Context.SourceValue);
        var result = new string(sourceValue.Take(length).ToArray());
        return source.New(result);
    }
}

Please, tell me if it has sense or has some bugs!

Thanks to @bidou for the tip. Here is the post where I took the inspiration here

Chris Nevill
  • 5,922
  • 11
  • 44
  • 79
SuperJMN
  • 13,110
  • 16
  • 86
  • 185
  • It does in this scenario, but I think there is something wrong. I don't now. Please, review it. – SuperJMN Apr 13 '18 at 08:57
  • Looks ok for AutoMapper 4.x. If you use the latest (6.x currently) then you will have to refactore the code a bit because a lot of things are now obsolete. – Bidou Apr 13 '18 at 09:25
  • Do you think you could provide us with a 6.x version of the code? Thank you! – SuperJMN Apr 13 '18 at 09:36
  • Bidou - I'm slightly confused did you provide a 6.x version of the code somewhere? This answer is definitely for 4.x David Bonds answer doesn't appear to work from my testing. – Chris Nevill Nov 06 '19 at 11:55
  • @ChrisNevill yes, check my answer in this post (currently the accepted answer) – Bidou Jan 19 '20 at 09:39
0

For AutoMapper 8.0, and building on @SuperJMN's answer:

Create a file AutoMapperExtensions.cs in your project:

using AutoMapper;
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;

namespace YourNamespaceHere
{
    public static class AutoMapperExtensions
    {
        public static IMappingExpression<TSource, TDestination> LimitStrings<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
        {
            var sourceType = typeof(TSource);
            var destinationType = typeof(TDestination);

            var existingMaps = Mapper.Configuration.GetAllTypeMaps().First(b => b.SourceType == sourceType && b.DestinationType == destinationType);

            var propertyMaps = existingMaps.PropertyMaps.Where(map => !map.Ignored && ((PropertyInfo)map.SourceMember).PropertyType == typeof(string));

            foreach (var propertyMap in propertyMaps)
            {
                var attr = propertyMap.DestinationMember.GetCustomAttribute<MaxLengthAttribute>();
                if (attr != null)
                {
                    expression.ForMember(propertyMap.DestinationMember.Name,
                        opt => opt.ConvertUsing(new StringLimiter(attr.Length), propertyMap.SourceMember.Name));
                }
            }

            return expression;
        }
    }

    public class StringLimiter : IValueConverter<string, string>
    {
        private readonly int length;
        private readonly PropertyInfo propertyMapSourceMember;

        public StringLimiter(int length)
        {
            this.length = length;
            propertyMapSourceMember = propertyMapSourceMember ?? throw new ArgumentNullException(nameof(propertyMapSourceMember));
        }

        public string Convert(string sourceMember, ResolutionContext context)
        {
            var sourceValue = (string)propertyMapSourceMember.GetValue(sourceMember);
            return new string(sourceValue.Take(length).ToArray());
        }
    }
}

... and add the following to the end of your CreateMap (e.g.):

.ForMember(
    dest => dest.ShortField,
    opts => opts.MapFrom(src => src.LongField))
.LimitStrings();
David Bond
  • 149
  • 1
  • 9
  • I'm having problems with this. I'm getting the following 'Mapper not initialized. Call Initialize with appropriate configuration. If you are trying to use mapper instances through a container or otherwise, make sure you do not have any calls to the static Mapper.Map methods, and if you're using ProjectTo or UseAsDataSource extension methods, make sure you pass in the appropriate IConfigurationProvider instance.' – Chris Nevill Nov 05 '19 at 17:19