69

I've been looking everywhere: stackoverflow, automapper documentation, internets and just couldn't find any info on this one, even tho this seems to be a very common problem.

My mapping:

CreateMap<StatusLevelDTO, StatusLevel>()
            .ForAllMembers(opt => opt.Condition(src => src != null));

This doesn't work because src represents source object (StatusLevelDTO), not a source property (I think).

To be more specific, If I map ObjectA to ObjectB, ObjectA.SomeValue is null and ObjectB.SomeValue is 2, I want the destination object to keep its value (2).

I've seen this question: Automapper skip null values with custom resolver and tried the first two answers but they both seem to be outdated for version 6.

Is there any way to make this happen in Automapper 6? I am using 6.0.2 to be exact.

Community
  • 1
  • 1
Sikor
  • 11,628
  • 5
  • 28
  • 43

6 Answers6

114

Method Condition now has five overloads, one of which accepts predicate of type

Func<TSource, TDestination, TMember, bool>

this TMember parameter is the source member. So you can check source member for null:

CreateMap<StatusLevelDTO, StatusLevel>()
     .ForAllMembers(opts => opts.Condition((src, dest, srcMember) => srcMember != null));
Sergey Berezovskiy
  • 232,247
  • 41
  • 429
  • 459
  • 5
    Actually, I've tried something similar before posting this question. The only difference was that I used 4 params, like this: (src, dest, srcMember, dstMember) and it didn't work. I've checked again with your version and it still doesn't work. – Sikor May 13 '17 at 00:00
  • @Sikor I've check both my solution and overload with four parameters - both work fine and keep destination values when source member is null. Make sure your destination object has non-null values when you pass it to mapper – Sergey Berezovskiy May 13 '17 at 00:02
  • Weird, are you sure that you have version 6.0.2? – Sikor May 13 '17 at 00:06
  • @Sikor yep, it's 6.0.2.0 – Sergey Berezovskiy May 13 '17 at 00:09
  • 1
    This is interesting. When I call `Mapper.Map(updatedStatusLevel, level);` 2 fields in the destination object change and they shouldn't. One changes from 2 to 0 (int type) and the other changes from list with 4 entries to null because both of these fields are null in the source object. – Sikor May 13 '17 at 00:11
  • @Sikor dotnet fiddle does not support fresh AutoMapper package, but you can check screenshot here https://www.dropbox.com/s/8ajqywzobe5tc28/automapper.png?dl=0 – Sergey Berezovskiy May 13 '17 at 00:18
  • 1
    Yeah, looks like it's working for you. I think the only difference is that I am debugging a unit test. I will try again running the whole application. Maybe something is not right with the tests. – Sikor May 13 '17 at 00:24
  • Still doesn't work. This is my source object: https://puu.sh/vOeec/887de28e2f.png and this is my destination object after mapping: https://puu.sh/vOeg6/296a67f89d.png. Before mapping Translations had count 4 and Level was 2. No idea why it doesn't work for me. – Sikor May 13 '17 at 00:31
  • 2
    @Sikor seems like you have `int?` in source object and `int` in destination. When AutoMapper tries to map this field it checks whether destination is nullable and uses default int value as sourceMember value. That's why null check condition fails and this default value is assigned to destination object – Sergey Berezovskiy May 13 '17 at 00:32
  • Fair point, but what about the list? Translations shouldn't change, right? – Sikor May 13 '17 at 00:34
  • 1
    @Sikor similar stuff - AutoMapper by default does not preserve null values for collections. There is setting `cfg.AllowNullCollections = true` to disable such behavior – Sergey Berezovskiy May 13 '17 at 00:39
  • Thank you very much :). I'm kind of new to AutoMapper and you helped a lot. There is only one problem (not related to the question tho). I can't really make the `int` nullable in the destination object because it is required in the database and if I make it non nullable in source, then it will always map 0. Looks like I will have to write another DTO with the fields I want to update only. Thanks for help once again and have a good night :) – Sikor May 13 '17 at 00:44
  • By the way, I've set the `AllowNullCollections` to `true` but it changes the destination list from Count 4 to Count 0 even tho source is null. – Sikor May 13 '17 at 00:50
  • @Sikor I would either make `int` non-nullable in source object if this field is required by your business logic, or removed not null constraint from database if business rules allow skipping this field – Sergey Berezovskiy May 13 '17 at 00:51
  • It is required. I made it nullable here because this specific call is meant to update only 2 fields and keep the rest untouched. But as I said, I think I should be using a different DTO in this case. – Sikor May 13 '17 at 00:54
  • @Sikor How did you solve the collection overwritten by a new empty list? facing same issue here, even with `AllowNullCollections=true`... – GGirard May 16 '17 at 23:09
  • @GGirard Unfortunately I didn't which is kinda lame given I did everything as suggested not only in this thread but all over the internet. I "solved" it by creating another DTO object that didn't have those fields. This way, the destination field is untouched because there is nothing to map from. However, that's more like a workaround than a solution. Let me know if you find something interesting. – Sikor May 17 '17 at 22:39
  • 1
    This worked great but doesn't seem to work with `ReverseMap()` – RoLYroLLs Dec 06 '20 at 19:21
27

This might be late, but for those who are still looking, this might solve your problem the same as mine.

I agree with @sergey to use:

CreateMap<StatusLevelDTO, StatusLevel>()
    .ForAllMembers(opts => opts.Condition((src, dest, srcMember) => srcMember != null));

But mapping nullable to non nullable will be an issue like int? to int it will always return 0. to fix it you can convert int? to int in mapping.

CreateMap<int?, int>().ConvertUsing((src, dest) => src ?? dest);
CreateMap<StatusLevelDTO, StatusLevel>()
     .ForAllMembers(opts => opts.Condition((src, dest, srcMember) => srcMember != null));
AmirJabari
  • 406
  • 3
  • 9
CherryBlossom
  • 393
  • 4
  • 8
9

The solution here works for my project, which is using AutoMapper 6.0.2. In previous projects using AutoMapper 4, I had used IsSourceValueNull to achieve the same behavior.

I made a small change to the original solution. Instead of checking the type of the property to be mapped, I set the filter in ForAllPropertyMaps to check the type of the source object, so that the custom resolver only applies to maps from that source object. But the filter can be set to anything as needed.

var config = new MapperConfiguration(cfg =>
{
    cfg.ForAllPropertyMaps(
        pm => pm.TypeMap.SourceType == typeof(<class of source object>),
        (pm, c) => c.ResolveUsing<object, object, object, object>(new IgnoreNullResolver(), pm.SourceMember.Name));
});

class IgnoreNullResolver : IMemberValueResolver<object, object, object, object>
{
    public object Resolve(object source, object destination, object sourceMember, object destinationMember, ResolutionContext context)
    {
        return sourceMember ?? destinationMember;
    }
}
Tim
  • 1,344
  • 1
  • 11
  • 12
  • 5
    Need some changes to use in *AutoMapper 8.1.1*: *cfg.ForAllPropertyMaps(pm => pm.TypeMap.SourceType == typeof(), (pm, c) => c.MapFrom(new IgnoreNullResolver(), pm.SourceMember.Name))* – vladimir Jun 12 '19 at 22:09
8

I inspired from @Sergey Berezovskiy's answer, and made this configuration for all members of all maps in the main config:

Mapper.Initialize(cfg =>
{
  cfg.ForAllMaps((obj, cnfg) => cnfg.ForAllMembers(opts => opts.Condition((src, dest, srcMember) => srcMember != null)));
}
Farid Imranov
  • 2,017
  • 1
  • 19
  • 31
3

As I don't have the reputation to comment, I'll add my answer down here for @Sikor @sensei

If you're using a Model that has the nullable data types of your DTO you can use this extension method below to negate the effects of Automapper resorting to the particular data type's default value.

Model examples

public class Foo {
    public bool? Example { get; set; }
}

public class FooDto {
    public bool Example { get; set; }
}

Extension Method:

public static TTarget MapModelProperties<TTarget, TSource>(this TTarget target, TSource source) where TTarget : class
                                                                                                where TSource : class

    {
        // Map target into the source, where the source property is null
        Mapper.Initialize(cfg =>
        {
            cfg.CreateMap<TTarget, TSource>()
                .ForAllMembers(opt => opt.Condition((src, dest, srcMember, destMember) => destMember == null));
        });
        Mapper.Map(target, source);

        // Map the source into the target to apply the changes
        Mapper.Initialize(cfg => cfg.CreateMap<TSource, TTarget>());
        Mapper.Map(source, target);

        return target;
    }

Usage

public class Foo
{
    public bool? Example { get; set; }
}

public class FooDto
{
    public bool Example { get; set; }
}

public void Example()
{
    var foo = new Foo
    {
        Example = null
    };

    var fooDto = new FooDto
    {
        Example = true
    };

    fooDto.MapModelProperties(foo);
}

This maps the Dto property values into all model's property values that are null. Then maps the model property values back into the Dto, thus only changing the Dto values that are present in the model.

tw1tch01
  • 49
  • 1
  • 4
0

This solution allows ignoring nulls including destination properties that are non-nullable

/// <summary>
        /// Extension method for the <see cref="IMappingExpression" /> that causes the mapping to not attempt to map null properties on the Source to the Destination including those properties that are non-nullable types on the Destination
        /// This method was created because there is no easy way to configure AutoMapper not to map null values on the Source object when the Destination object has non-nullable types on it. The default behavior of
        /// Automapper is to set non-nullable destination types to its Default if the Source value is null. The IgnoreNullsOnSource method overrides this default behavior.
        /// </summary>
        /// <typeparam name="TSource">Source object - null values on this object will not be mapped to the Destination</typeparam>
        /// <typeparam name="TDestination">Destination object</typeparam>
        /// <param name="mappingExpression">The <see cref="IMappingExpression"/> that will be configured to ignore null values on the source</param>
        public static IMappingExpression<TSource, TDestination> IgnoreNullsOnSource<TSource, TDestination>(this IMappingExpression<TSource, TDestination> mappingExpression)
        {
            mappingExpression.IsRequired();

            foreach (PropertyInfo destinationProperty in typeof(TDestination).GetProperties())
            {
                mappingExpression.ForMember(destinationProperty.Name,
                    propertyMap => propertyMap.Condition((source, _, _, _, resolutionContext) =>
                    {
                        TypeMap typeMap = resolutionContext.ConfigurationProvider.ResolveTypeMap(typeof(TSource), typeof(TDestination));
                        PropertyMap propertyMap = typeMap.FindOrCreatePropertyMapFor(destinationProperty);
                        if (propertyMap.HasSource() && propertyMap.SourceMember is PropertyInfo sourceProperty)
                        {
                            object? originalSourceValue = sourceProperty.GetValue(source);
                            bool shouldPerformMapping = originalSourceValue != null;
                            return shouldPerformMapping;
                        }
                        return true; // the default is to map all values
                    }));
            }

            return mappingExpression;
        }

And then call it like this

CreateMap<Source, Dest>()
   .IgnoreNullsOnSOurce()
Agreene
  • 508
  • 7
  • 16