Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reflection GetValue of static field with circular dependency returns null

Note: The following code actually works okay, but shows the scenario that is failing in my own solution. See the bottom of this post for more information.

With these classes:

public class MainType {
   public static readonly MainType One = new MainType();
   public static readonly MainType Two = SubType.Two;
}

public sealed class SubType : MainType {
   public new static readonly SubType Two = new SubType();
}

Get fields One and Two:

List<FieldInfo> fieldInfos = typeof(MainType)
   .GetFields(BindingFlags.Static | BindingFlags.Public)
   .Where(f => typeof(MainType).IsAssignableFrom(f.FieldType))
   .ToList();

Finally, get their values:

List<MainType> publicMainTypes = fieldInfos
   .Select(f => (MainType) f.GetValue(null))
   .ToList();

In LinqPad or in a simple unit test class with the above code, everything works okay. But in my solution, where I have some unit tests that want to work with all instances of these fields, GetValue works fine to return fields of the parent type, but where the parent fields are supposed have instances of the subtype, they always instead give null! (If that happened here, the final list would be { One, null } instead of { One, Two }.) The test class is in a different project from the two types (each in their own file), but I've temporarily made everything public. I've dropped a breakpoint in and have examined all I can examine, and have done the equivalent of fieldInfos[1].GetValue(null) in a Watch expression and it does in fact return null, despite the fact that there is a line in my main class exactly like the second one from MainType above.

What is wrong? How do I get all the values of the subtype fields? How is it even possible for them to return null without an error?

On the theory that perhaps for some reason the subtype's class was not being statically constructed due to the access through reflection, I tried

System.Runtime.CompilerServices.RuntimeHelpers
  .RunClassConstructor(typeof(SubType).TypeHandle);

at the top before starting, but it didn't help (where SubType is the actual subtype class in my project).

I'll keep plugging away at trying to reproduce this in a simple case, but I'm out of ideas for the moment.

Additional Information

After a bunch of fiddling, the code started working. Now it is not working again. I am working on reproducing what triggered the code to start working.

Note: Targeting .Net 4.6.1 using C# 6.0 in Visual Studio 2015.

Problem Reproduction Available

You can play with a working (failing) trimmed-down version of my scenario by downloading this somewhat minimal working example of the problem at github.

Debug the unit tests. When the exception occurs, step until you get to line 20 of GlossaryHelper.cs, and can see the return value of GetGlossaryMembers in the Locals tab. You can see that indexes 3 through 12 are null.

like image 469
ErikE Avatar asked Oct 21 '16 17:10

ErikE


1 Answers

Problem

The issue has nothing to do with Reflection but instead the circular dependency between the two static field initializers and the order of their execution.

Consider the following snippet:

var b = MainType.Two;
var a = SubType.Two;
Debug.Assert(a == b); // Success

Now let's exchange the first two lines:

var a = SubType.Two;
var b = MainType.Two;
Debug.Assert(a == b); // Fail! b == null

So what's going on here? Let's see:

  1. Code tries to access SubType.Two static field for the first time.
  2. The static initializer fires and executes the constructor of SubType.
  3. Since SubType inherits from MainType, the MainType constructor also executes and triggers MainType static initialization.
  4. The MainType.Two field static initializer is trying to access SubType.Two. Since static initializers are executed just once, and the one for SubType.Two is already executed (well, not really, it's currently executing, but is considered to be), it simply returns the current field value (null at that moment) which then is stored in MainType.Two and will be returned by further access requests for the field.

In short, the correct working of such design really depends on the order of the external access to the fields, so it's not surprising that it sometimes works, sometimes not. Unfortunately this is something you cannot control.

How to fix

If possible, avoid such static field dependencies. Use static readonly properties instead. They give you full control and also allows you to eliminate the field duplication (currently you have 2 different fields that hold one and the same value).

Here is the equivalent design without such issues (using C#6.0):

public class MainType
{
    public static MainType One { get; } = new MainType();
    public static MainType Two => SubType.Two;
}

public sealed class SubType : MainType
{
    public new static SubType Two { get; } = new SubType();
}

Of course this will require changing your reflection code to use GetProperties instead of GetFields, but I think it's worth it.

Update: Another way to resolve the issue is to move the static fields to a nested abstract container classes:

public class MainType
{
    public abstract class Fields
    {
        public static readonly MainType One = new MainType();
        public static readonly MainType Two = SubType.Fields.Two;
    }
}

public sealed class SubType : MainType
{
    public new abstract class Fields : MainType.Fields
    {
        public new static readonly SubType Two = new SubType();
    }
}

Now both tests complete successfully:

var a = SubType.Fields.Two;
var b = MainType.Fields.Two;
Debug.Assert(a == b); // Success

and

var b = MainType.Fields.Two;
var a = SubType.Fields.Two;
Debug.Assert(a == b); // Success

This is because the container classes, except of being contained in, have no relation to the static field types, hence their static initialization is independent. Also, although they use inheritance, they never get instantiated (because of being abstract), thus have no side effects caused by the base constructor calls.

like image 196
Ivan Stoev Avatar answered Sep 25 '22 23:09

Ivan Stoev