Enhancing .NET Hot Reload with CreateNewOnMetadataUpdate, MetadataUpdateHandler and MetadataUpdateOriginalType Attributes

With each update to the .NET SDK and Visual Studio the support for Hot Reload improves. Hot Reload is the ability to make changes to your running application without having to restart and it can dramatically improve developer productivity, when it works. In this post we’re going to look at a simple example of where the out of the box hot reload support fails, and a couple of attributes you can use to get improve support for hot reload.

To get started we’re going to define a DataManager class that implements an IDataManager interface, that has a single method, Calculate, that returns an integer.

public interface IDataManager
{
    int Calculate();
}

public class DataManager : IDataManager
{
    private int[] values = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    public int Calculate()
    {
        return (from x in values
                where x % 2 > 0
                select x + 2).Sum();
    }
}

Our application in this scenario is going to be just a console application which has an infinite while loop that creates an instance of the DataManager class, invokes the Calculate method, and prints the value.

while (true)
{
    var d = new DataManager();
    var val = d.Calculate();
    Console.WriteLine($"Value {val}");
    await Task.Delay(3000);
}

Running this application will simply print the value 35 to the screen repeatedly until the application is terminated

Once we’ve started running this application we can make simple changes, for example changing the x + 2, to, x + 5, and the application will apply the change and you’ll see the printed value change from 35 to 50.

But what if we wanted to change the code to extract the x + 2 into a function.

public class DataManager : IDataManager
{
    private int[] values = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    private Func<int, int> calculation => x => x + 2;
    public int Calculate()
    {
        return (from x in values
                where x % 2 > 0
                select calculation(x)).Sum();
    }
}

If you attempt to make this change whilst the application is running you’ll see a prompt stating that “Hot Reload can’t automatically apply your changes. The app needs to be rebuilt to apply updates.

What’s happened is we’ve run into one of the limitations of hot reload where it’s unable to apply the changes whilst the application is running. Specifically, it can’t apply the change to the existing type definition of DataManager. However, what we can do is to apply the CreateNewOnMetadataUpdateAttribute to the DataManager class.

[CreateNewOnMetadataUpdate]
public class DataManager : IDataManager
{
 ...
}

What the CreateNewOnMetadataUpdate attribute does it direct hot reload to replace the entire DataManager class, instead of attempting to modify the existing type. Since it’s not possible to simply replace the actual DataManager class, what happens is a new type, DataManager#1 is created and is made available to the running application.

In order for the application to make use of this new type, we need to use the assembly level MetadataUpdateHandlerAttribute.

[assembly: MetadataUpdateHandler(typeof(MetaHandler))]

The MetadataUpdateHandler attribute specifies a class with either, or both, ClearCache and UpdateApplication methods defined.

public class MetaHandler
{
    public static Dictionary<Type, Type> Map { get; } = new Dictionary<Type, Type>();

    public static void ClearCache(Type[]? updatedTypes)
    {
    }
    public static void UpdateApplication(Type[]? updatedTypes)
    {
        foreach (var t in (updatedTypes ?? Array.Empty<Type>()))
        {
            if (t.GetCustomAttribute<MetadataUpdateOriginalTypeAttribute>() is { } updateAttribute)
            {
                Map[updateAttribute.OriginalType] = t;
            }
        }
    }
}

In this case the UpdateApplication method is being used to update a dictionary of types that have been replaced. For the purposes on this example, the dictionary would only contain a single item with Key being the DataManager type, and the Value being progressively DataManager#1, DataManager#2 etc with each change made to the DataManager class.

The last thing we need to do is to use the dictionary in order to make sure an instance of the new type is created, instead of the original DataManager class. The program code has been modified to use the GetInstance method that first looks up the Map dictionary to determine what type to use, and then call Activator.CreateInstance to create an instance of the type.

while (true)
{
    var d = GetInstance();
    var val = d.Calculate();
    Console.WriteLine($"Value {val}");
    await Task.Delay(3000);
}

IDataManager GetInstance()
{
    var type = MetaHandler.Map.TryGetValue(typeof(DataManager), out var handler) ? handler : typeof(DataManager);
    return Activator.CreateInstance(type) as IDataManager;
}

Now we can make all manner of changes to the DataManager class without running into the limitations of hot reload.

Of course, you’re not going to do this for all the types within an application. In fact, this technique is most likely to be used by framework or component library developers to help improve hot reload support for users of their libraries.

3 thoughts on “Enhancing .NET Hot Reload with CreateNewOnMetadataUpdate, MetadataUpdateHandler and MetadataUpdateOriginalType Attributes”

Leave a comment