KDR
KDR

Reputation: 488

static constructor not invoked

I'm trying to use the AutoMapper for model-viewmodel mapping and wanted to have the mapping configuration executed once in the static constructor of the type. The static constructor of a type is not invoked when Mapper.Map (AutoMapper) is invoked with that type.

My understanding is that the Mapper.Map would try to access the type, its members through reflection and on the first attempt of the usage the static constructor would be called. This is something basic but challenges my understanding. The code snippet is provided.

class SampleViewModel 
{
    static SampleViewModel()
    {
        Mapper.Initialize(cfg => cfg.CreateMap<Sample, SampleViewModel>().ReverseMap());
    }

    public SampleViewModel()
    {
    }


    public int PropertyA { get; set; }
    public int PropertyB { get; set; }
}


 Sample s = new Sample { PropertyA = 10, PropertyB = 20 };
 var obj = Mapper.Map<SampleViewModel>(s); // fails

Isn't the static constructor called (if provided) when the type and members are accessed through reflection for the first time?

Upvotes: 0

Views: 1078

Answers (3)

baez
baez

Reputation: 3

".. The static constructor of a type is not invoked when Mapper.Map (AutoMapper) is invoked with that type..." I tested your scenario and observed that the static constructor is indeed being called before the first instance of the object is created.

C# Programming Guide: Static Constructors

I also went ahead and modified the sample code by adding a GetMapper function which returns an IMapper. This might seem like an overkill for day-to-day simple mapping, but if we need an object to give us its mapper, perhaps we can get it from a static method.

One can easily move the responsibility of creating the Mapper object to a factory or a DI container which, for the sake of simplicity, I did not include here.

Worth noting that in this example, the static fields are initialized before the static constructor which is called right after the static read-only fields are initialized.

Uses AutoMapper v12.0 and .Net Core 3.1.

public class SimpleObjectToMap
{
    private static readonly MapperConfiguration _simpleObjMapperConfig = new MapperConfiguration(
            config => config.CreateMap<SimpleObjectToMap, ObjectToBeMappedTo>());

    private static readonly IMapper _mapper = new Mapper(_simpleObjMapperConfig);

    private int _propertyA;
    public int PropertyA
    {
        get
        {
            Console.WriteLine("ObjectToMap.PropertyA.Get");
            return _propertyA;
        }

        set { _propertyA = value; }
    }

    static SimpleObjectToMap()
    {
        Console.WriteLine("*** ObjectToMap static ctor called ***");
    }

    public static IMapper GetMapper()
    {
        return _mapper;
    }
}

public class ObjectToBeMappedTo
{
    static ObjectToBeMappedTo()
    {
        Console.WriteLine("*** ObjectToBeMappedTo static ctor called ***");
    }

    private int _propertyA;
    public int PropertyA
    {
        get { return _propertyA; }

        set
        {
            Console.WriteLine("ObjectToBeMappedTo.PropertyA.Set");
            _propertyA = value;
        }
    }
}

public class TestObjectMappingWithStaticCtor
{
    public void TestWithStaticCtor()
    {
        SimpleObjectToMap objToMap = new SimpleObjectToMap { PropertyA = 27 };
        var mappedObject = SimpleObjectToMap.GetMapper().Map<ObjectToBeMappedTo>(objToMap);
    }
}

Upvotes: 0

Luaan
Luaan

Reputation: 63732

You're not accessing any members of SampleViewModel - it's not enough to just reference the type itself.

Mapper.Map only accesses its own internal "dictionary" of mappings - before it could ever get to a point where it deals with a SampleViewModel, it fails. The static constructor never runs, so it cannot add "itself" into the Mapper.

Now, if this didn't involve reflection, you would be right that the static constructor would be called - simply because it would happen during the compilation of the method containing the access, e.g.:

var obj = Mapper.Map<SampleViewModel>(s);
Console.WriteLine(obj.SomeField);

In this case, since the method is referencing a field on SampleViewModel, the static constructor for SampleViewModel will be invoked during JIT compilation of the containing method, and as such, the Mapper.Map<SampleViewModel>(s) line will execute correctly, since the mapping is now present. Needless to say this is not the proper solution to your problem. It would just make the code absolutely horrible to maintain :)

DISCLAIMER: Even though this might fix the problem right now, it depends on a non-contractual behaviour in the current implementation of MS.NET on Windows. The contract specifies that the type initializer is invoked before any access to a member of the type, but that still means that a valid implementation of CIL might only call the type initializer after Mapper.Map, as long as it happens before obj.SomeField - and even then, it might be that obj.SomeField gets optimized away if the compiler can ensure it is safe to do so. The only real way to enforce the call of the type initializer is to call RuntimeHelpers.RunClassConstructor, but by that point, you could just as well add a static Init method or something.

The real problem is that you shouldn't really initialize stuff like this in a static constructor in the first place. Mappings should be set in some kind of deterministic initialization process, say, an explicitly invoked InitMappings method. Otherwise you're opening yourself to a huge can of Heisenbugs, not to mention subtle changes in the CLR breaking your whole application for no apparent reason.

Static constructors just aren't meant for "registration", just the initialization of the type itself - anything else is an abuse, and will cause you (or the .NET compatibility team) trouble.

Upvotes: 4

CodeCaster
CodeCaster

Reputation: 151586

Static constructors run at an implementation-defined time just before the first instance of that class is created, or before any static member of that type is accessed. See When is a static constructor called in C#?.

The mapper tries to find a mapping before doing its work of instantiating the class to map into, and thus can't find a mapping, because the class was never instantiated before that moment.

Just move your mapping initialization code into a AutoMapperBootstrap.cs file or something, and call that in your application initialization.

Upvotes: 3

Related Questions