Caliburn-Micro / Caliburn.Micro

A small, yet powerful framework, designed for building applications across all XAML platforms. Its strong support for MV* patterns will enable you to build your solution quickly, without the need to sacrifice code quality or testability.
http://caliburnmicro.com/
MIT License
2.8k stars 778 forks source link

Error while loading view from different AssemblyLoadContext #754

Closed Grendizr closed 3 years ago

Grendizr commented 3 years ago

Hi,

I am currently experiencing problems with Caliburn.Micro v.4.0.173.

My current setup is composed of a main application (host) which is loading ViewModels/Views from different assemblies (plugins). Each plugin is loaded in its own AssemblyLoadContext. Host and plugins are targeting .NET Core 3.1 on Windows. IoC is Autofac 6.1.

In one of these plugins, I am using a XamDataGrid from Infragistics. (taken from the Nuget package Infragistics.WPF.DataGrids.Trial)

When Caliburn.Micro loads the View, it fails to load the Infragistics dll in which the XamDataGrid is defined, with a FileNotFound exception. This is because the dependency is not present in the host application, thus not available in the Default AssemblyLoadContext

The problem seems to be related to contextual reflection. The .NET Core CLR team explain this here.

Unfortunately it will be tricky for me to share a sample project, as I would need to carve out some logic from my company's proprietary library.

I have been able to find a work-around proving that this is indeed the issue, and it might also prove that the View might not be instantiated from the right context by Caliburn. (Although it is not quite clear if it fails under Autofac or Caliburn responsibilities.)

Updating the view constructor like this did the trick:

public PeopleInfoView()
        {
            using (AssemblyLoadContext.GetLoadContext(GetType().Assembly).EnterContextualReflection())
            {
                InitializeComponent();
            }
        }

I think this using block should be part of Caliburn's logic.

What do you think ?

Thank you.

KasperSK commented 3 years ago

How are the plugins assemblies loaded into the host application? Would the logic have to be responsible for loading the infragistics.dll as well?

Grendizr commented 3 years ago

Hi @KasperSK,

Thanks for your reply.

Assemblies are loaded using McMaster NetCore Plugins, a thin wrapper around the AssemblyLoadContext

    var loader = PluginLoader.CreateFromAssemblyFile(
        pluginDll,
        isUnloadable: false, 
        sharedTypes: new[] { typeof(IPluginViewModel), typeof(ILogger), typeof(IEventAggregator)
        }, config =>
        {
            config.LoadInMemory = true;
        }  
    );

    var assembly = loader.LoadDefaultAssembly();
    _logger.Information("Plugin {assemblyName} has been loaded.", assembly.FullName);

Plugin assemblies are loaded and registered in Autofac container builder within the Caliburn bootstrapper. At this stage in the application startup process, I have no knowledge about the controls and dependencies of the Views contained in the plugins, so I am not able to load any additional libraries into a specific AssemblyLoadContext

Also isn't it the .NET framework responsible for loading the control dependencies dll that could be defined in the xaml of a View?

At the point Caliburn initializes the View, it is important to be in the correct AssemblyLoadContext to ensure the dependencies are correctly found when reflection is used. With current behaviour when reflection is used, it is only within the bounds of the Default AssemblyLoadContext.

Having Caliburn creating the View instance within this using block ensures the correct AssemblyLoadContext will be used when reflection occurs. This links explains the problem fully.

KasperSK commented 3 years ago

Could you try the following code in your bootstrapper to see if this fixes you issue?

var oldCreate = ViewLocator.GetOrCreateViewType;
ViewLocator.GetOrCreateViewType = (type) =>
{
    using (AssemblyLoadContext.GetLoadContext(GetType().Assembly).EnterContextualReflection())
    {
        return oldCreate(type);
    }
};

This should surround the view creation with you using statement.

Grendizr commented 3 years ago

Thanks for the update @KasperSK

The provided code is not working, but a tiny change did the trick

var oldCreate = ViewLocator.GetOrCreateViewType;
ViewLocator.GetOrCreateViewType = (type) =>
{
    using (AssemblyLoadContext.GetLoadContext(type.Assembly).EnterContextualReflection())
    {
        return oldCreate(type);
    }
};

Instead of using GetType().Assembly you need to use the Assembly of the target view type via type.Assembly, otherwise, it will always use the Assembly that the bootstrapper is located in, and you will always end-up in the default AssemblyLoadContext.

This is quite neat to have a fix at the boostrapper level ! Thanks for the tip.

As far as I am concerned this is good enough and I will close the issue. Thanks.

KasperSK commented 3 years ago

@Grendizr Glad to help, I am unsure of whether this should be build into Caliburn.Micro. It is a special use case :)