Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is Automapper Mapper.CreateMap thread-safe?

In our current project we are registering mappings in static constructors of classes which are called by multiple threads. Mappings in static constructors are relevant only for that classes. But still multiple calls of CreateMap can be run in same time. Moreover occasionally (mostly as copy/past issues) same mappings can be registered in static constructors of different classes.

I tried to google whether Mapper.CreateMap is thread safe or not. And I found only following:

In post Is Mapper.Map in AutoMapper thread-safe from 2012 there is note in nemesv's answer that CreateMap is not thread safe and it will never be.

But I found an issue on GitHub Static DynamicMap and CreateMap APIs should be thread-safe from 2014 marked as closed in 3.2 release. Which suggests that CreateMap should be thread safe now.

Can you confirm that CreateMap is thread safe? I ran some tests and it looks like it should be but still if somebody with deeper knowledge could confirm this information it would be fine.

EDIT After some additional testing it seems that CreateMap behavior is very interesting:

I used following code for testing

    public void Test()
    {
        var items = new List<EntityA>();
        for (int i = 0; i < 100000; i++)
        {
            items.Add(new EntityA { FirstName = "A" + i });
        }

        ManualResetEvent stopChangingMappingFunction = new ManualResetEvent(false);

        Thread t1 = new Thread(() =>
        {

            int i = 1;
            while (true)
            {
                if (stopChangingMappingFunction.WaitOne(TimeSpan.Zero))
                    return;

                var i1 = i++;
                Mapper.CreateMap<EntityA, EntityB>().ForMember(x => x.Age, y => y.ResolveUsing(new Func<EntityA, object>(a => i1)));
            }
        });

        Thread t2 = new Thread(() =>
        {
            int i = -1;
            while (true)
            {
                if (stopChangingMappingFunction.WaitOne(TimeSpan.Zero))
                    return;

                var i1 = i--;
                Mapper.CreateMap<EntityA, EntityB>().ForMember(x => x.Age, y => y.ResolveUsing(new Func<EntityA, object>(a => i1)));
            }
        });

        List<int> distinctAges1 = null;
        List<int> distinctAges2 = null;

        Thread t3 = new Thread(() =>
        {
            Thread.Sleep(1000);

            var res = Mapper.Map<IList<EntityA>, IList<EntityB>>(items);
            distinctAges1 = res.Select(x => x.Age).Distinct().ToList();

            Thread.Sleep(1000);

            var res2 = Mapper.Map<IList<EntityA>, IList<EntityB>>(items);
            distinctAges2 = res.Select(x => x.Age).Distinct().ToList();

            stopChangingMappingFunction.Set();
        });

        t1.Start();
        t2.Start();
        t3.Start();

        t1.Join();
        t2.Join();
        t3.Join();

        Console.WriteLine("First Mapping: " + string.Join(", ", distinctAges1.ToArray()));
        Console.WriteLine("Second Mapping: " + string.Join(", ", distinctAges2.ToArray()));
        Console.ReadKey();
    }

public class EntityA
{
    public string FirstName { get; set; }
}

public class EntityB
{
    public string FirstName { get; set; }
    public int Age { get; set; }
}

In all my tests when first Map method is called it means that CreateMap is frozen and no more changes to mapping function can be done (distinctAges1 was always one unique value and same value was in distinctAges2). Changing map function from two threads sometimes lead to increasing alternating values of Age from negative to positive numbers (tests ended with high value of distinct Age). But sometimes behavior was totally different and Age iteration stopped at value of 1 or -1. It seems there is some internal mechanism freezing changes to mapping function if this mapping function is changed from more threads. But this was not happening in 100% cases

like image 965
user2126375 Avatar asked Sep 27 '22 03:09

user2126375


1 Answers

CreateMap is thread-safe. That doesn't mean your code calling CreateMap is. CreateMap should only be called once per AppDomain, the usual way is something like this: https://github.com/jbogard/ContosoUniversity/blob/master/src/ContosoUniversity/Infrastructure/Mapping/AutoMapperBootstrapper.cs

Maps should not use any contextual data that's passed in via closures. The code you have above are bad maps to begin with, you should instead refactor them to use the built-in means for contextual data https://stackoverflow.com/a/31754133/58508

like image 94
Jimmy Bogard Avatar answered Oct 13 '22 01:10

Jimmy Bogard