0

I'm using Automapper and Hot Chocolate. I am using a struct type on my source class properties named Optional<> which has the methods .Value and .HasValue, this is used to determine whether the client passed in these values or not. This is used for a partial update/patching from client to DB. So for example:

My model/dto: public Optional<string> Name { get; init; }

In my automapper profile I can do this:

.ForMember(dest => dest.Name, opt => {
    opt.PreCondition(src => src.Name.HasValue))
    opt.MapFrom(src => src.Name.Value)
})

This works and has the behaviour I want to have. However, I dont want to end up writing manual mapping code for all my attributes, that's why I'm using Automapper in the first place.

Therefore, I'm attempting to make this behaviour generic of all source type properties of type IOptional.

I've tried a bunch of different things. Extension methods and reflections, and using type converters.

Automapper custom type converter won't work since you have to return a value and can't "bail" out of the mapping.

I've tried to make extension methods for PreCondition, but I only have access to the AutoMapper's ResolverContextobject, and I havent figured out how to utilize this. For example:

.ForAllOtherMembers(o => o.PreCondition((src, dest, rc) => 
    // what do    
));

Has anyone done anything similar? I suppose it's the same concept as a Nullable<> struct, but I couldnt find any implementations for that which worked for this case as well.

oysandvik
  • 36
  • 5
  • But why the `PreCondition`? You could simply write the check inside the `MapFrom`. Are you mapping _into_ an existing object? – Lucian Bargaoanu Mar 09 '22 at 08:01
  • @LucianBargaoanu Right, this is mapping a model from the client in to a model from the DB, so the purpose is to update an entity in the DB. So I don't want "null" as a fallback value. As far as I understand, if I do the check in the MapFrom then I would need a default value. – oysandvik Mar 09 '22 at 09:55
  • If you want tot try a more generic solution, drop the `PreCondition` and use `ForAllMembers` with a `Condition`. You have acccess to the destination member in a `Condition`. Many people did similar things before. – Lucian Bargaoanu Mar 09 '22 at 10:32
  • Don't I need to specify every destination member still then? Reason I'm trying to make this generic is that the big point of automapper is to write less mapping code. If I have to write the mapping for every destination member then I would have to write more mapping code than if I didnt use automapper. And also, my Optional struct isn't on the destination member, but on the source member. – oysandvik Mar 09 '22 at 13:18
  • No, you don't, a generic mapping will handle that. – Lucian Bargaoanu Mar 09 '22 at 13:20
  • Right, but in .Condition() I only have access to the destination member, and I need to check the source member struct. For example this: ` .ForAllMembers(opts => opts.Condition((src, dest, srcMember) => { if (srcMember is IOptional optional) { return optional.HasValue; } return true; })) ` Does not work because the srcMember is already the resolved value. Sorry for the formatting, difficult to make it nice in these small comments. – oysandvik Mar 09 '22 at 13:46
  • The `MapFrom` is not needed anymore once you have a generic map for `Optional<>`. – Lucian Bargaoanu Mar 09 '22 at 13:57
  • My problem is making a condition on the source member, which is of type `Optional<>`. I can make a generic type converter for `Optional`, so that's not a problem. Just to clarify, when mapping the incoming model to an existing model, I don't want to set a value on the destination member if the source member that has this type `Optional` and it's `.HasValue` property is false. When I use `PreCondition()` I achieve what I want. – oysandvik Mar 09 '22 at 14:16
  • Yes, and I've pointed you in the right direction. It's not that complicated, you just have to put in the work. – Lucian Bargaoanu Mar 09 '22 at 16:37

1 Answers1

0

Think I found a solution I'm happy with:

Catches all optional source types and applies the resolvers.

ForAllPropertyMaps(pm =>
        {
            return pm.SourceType != null && pm.SourceMember != null && pm.SourceType.IsGenericType &&
                   (pm.SourceType.GetGenericTypeDefinition() == typeof(Optional<>));
        }, (pm, c) =>
        {       
            c.MapFrom<AutoMapperExtensions.Resolver, IOptional>(pm.SourceMember.Name);
        });

Resolves based on HasValue:

public class Resolver : IMemberValueResolver<object, object, IOptional, object>
    {
        public object Resolve(object source, object destination, IOptional sourceMember, object destinationMember, ResolutionContext context)
        {
            return sourceMember.HasValue ? sourceMember.Value : destinationMember;
        }
    }

Can be written prettier and the type check should probably be different, but this works as a starting point. Based on this comment: https://github.com/AutoMapper/AutoMapper/issues/2999#issuecomment-472692335

oysandvik
  • 36
  • 5