Search code examples
c#.net.net-coreautomapperhotchocolate

Generic Automapper Precondition for Struct type with `Value`and `HasValue`


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.


Solution

  • 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