ResolutionContext creation customization in Automapper

Viewed 4184

guys.

Maybe someone has the same problem.

I have some cached variables in the MemoryCache (standard non-distributed in-memory implementation of Microsoft.Extensions.Caching.Memory.IMemoryCache). So, I also have mappings I use for Response/DTO creation. Some of them use variables from MemoryCache. But now I must always pass it through

opts => 
{
   opts.Items.Add(variableName1, variableValue1);
   opts.Items.Add(variableName2, variableValue2);
   ... 
}

or I need to pass each time MemoryCache the same way.

Is it possible to set up a global configuration of ResolutionContext which allows me to pass all variables from MemoryCache I need in the time of the ResolutionContext creation? Unfortunately, BeforeMap isn't a solution - It has no DI mechanism for IMemoryCache resolving. And as I know It can be only one in the mapping structure - Automapper skips all BeforeMap after the first one.

Thank you.

2 Answers

Instead of using the ResolutionContext, you can implement a custom IMemberValueResolver, which can get the IMemoryCache dependency injected.
By doing so, there's no need to seed theResolutionContext with key/value pairs (being copied from the IMemoryCache).

The FromMemoryCacheResolver below resolves the value for the requested cache key from the injected IMemoryCache.

public class FromMemoryCacheResolver<TDestMember>
    : IMemberValueResolver<object, object, object, TDestMember>
{
    private readonly IMemoryCache _memoryCache;

    public FromMemoryCacheResolver(IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache;
    }

    public TDestMember Resolve(
        object source, object destination, object cacheKey, TDestMember destMember,
        ResolutionContext context
        )
    {
        if (_memoryCache.TryGetValue(cacheKey, out object value)
            && (value != null)
            )
        {
            return (TDestMember)value;
        }

        return default(TDestMember);
    }
}

Example

public class Source
{
    public int Id { get; set; }
}

public class Target
{
    public decimal DecimalValue { get; set; }

    public string StringValue { get; set; } 
}

Given the above Source and Target classes, you can define an AutoMapper mapping that sets a target property to the value bound to a fixed cache key (see DecimalValue rule) or a dynamic cache key (including a property value of the source object, see StringValue rule).

CreateMap<Source, Target>()
    .ForMember(
        o => o.DecimalValue,
        opt => opt.MapFrom<FromMemoryCacheResolver<decimal>, object>(
            _ => "constant-cache-key"
    ))
    .ForMember(
        o => o.StringValue,
        opt => opt.MapFrom<FromMemoryCacheResolver<string>, object>(
            src => $"dynamic-cache-key-{src.Id}"
    ));

You could override the way AutoMapper is registered in your dependency injection container and perform an action just before it is resolved. Assuming you use standard Microsoft's DI:

// Your code adding AutoMapper
services.AddAutoMapper(assembliesOrMarkerTypes);

// Remove just the IMapper
services.RemoveAll(typeof(IMapper));

// Add it again, but with filling the Items dictionary from cache
services.Add(new ServiceDescriptor(
    typeof(IMapper),
    sp =>
    {
        var memoryCache = sp.GetRequiredService<IMemoryCache>();

        var valueFromCache = memoryCache.Get<string>("foo");

        var mapper = new Mapper(sp.GetRequiredService<IConfigurationProvider>(), sp.GetService);

        // Does not work!
        // mapper.DefaultContext.Items.Add("foo", valueFromCache);

        // Use Items from Options:
        mapper.DefaultContext.Options.Items.Add("foo", valueFromCache);

        return mapper;
    },
    ServiceLifetime.Transient)); // <== Default AutoMapper lifetime

There are two downsizes to this:

1) Access Items from resolution context's options, not directly

There is a check made when accessing Items in resolution context preventing from accessing them in a default context, which is a source for creating other contexts used in mapping. Luckily, there is no such check when accessing Items from options:

var items = resolutionContext.Options.Items;

So don't do that:

var items = resolutionContext.Items;

2) Don't use Map() with Action<IMappingOperationOptions>

You can't use any Map() method accepting Action<IMappingOperationOptions> because it will effectively overwrite content of Items dictionary created when initializing the mapper with entries from the mapping operation options, even if none were set. So, you can't do this:

var result = mapper.Map(source, destination, opts => otps.Items["bar"] = "bar");

Final note

Overall it's a bit of a hack and surely this code wouldn't win the beauty contest, so consider encapsulating it in some decent extension method for IServiceCollection.

Related