'Manage host and plugin dll versions in plugin-based application .NET 6

I have application with possibility to load plugins. There is interface IPlugin in PluginBase project which is packed as nuget package. All plugins should implement this interface. DLL versions are set incrementally in CI workflow. Let's say that initially we have application built as 1.0.0.0 version. Then application is published again and all dlls have 1.0.0.1 version. Then I create Plugin1 and do next steps:

  1. Reference this PluginBase package 1.0.0.1.
  2. Add <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> in <PropertyGroup> section of .csproj file.
  3. Add ExcludeAssets="Runtime" for referenced PluginBase package in order not to have it in output folder. Then build and put plugin in app folder.

Everything works fine if to run host app of 1.0.0.1 version. But if to run previously built version of host (1.0.0.0) then there are exceptions as: System.IO.FileNotFoundException: Could not load file or assembly 'PluginBase, Version=1.0.0.1, Culture=neutral, PublicKeyToken=null'. This behavior is clear but my question is how to achieve next requirements:

  1. Ideally I would want all plugins are loaded even when major versions of referenced nuget package don't fit major version of host. Though major version should be changed due to incompatible changes nevetheless it does not mean that particular IPlugin will be changed (I mean incompatible change can be done in other parts of application => plugin interface is not changed and all plugins can still work).
  2. In case of some incompatible changes in plugins put logic in host application, i.e. just hardcode which major version of plugins should be loaded by current version of host. For example if there is incompatibility between version 1 and version 2 then just hardcode that host (with current version 2) should load only plugins with major version >= 2.

I guess that for the second requirement I need to use Assembly.GetReferencedAssemblies method in order to know which version of PluginBase is referenced by plugin and decide if it should/not be loaded?

But I don't have clear vision how to met the first requirement because .net runtime checks dll versions when loading.

Btw, I see there is also EnableDynamicLoading property introduced. Should I use it instead of CopyLocalLockFileAssemblies property?

In future I have plan to inroduce possibility to download plugins from nuget so should I keep in mind any specific in order to have things working as expected?



Solution 1:[1]

Solution was in AssemblyLoadContext public event Func<AssemblyLoadContext, AssemblyName, Assembly?>? Resolving event - "Occurs when the resolution of an assembly fails when attempting to load into this assembly load context." So it's needed to listen this event and decide inside if we can process requested dll or not. In my case code looks in this way:

public static class PluginsRuntimeSettings
{
    /// <summary>
    /// Indicates which minimal version of SmartHomeApi.Utils is supported by current version of SmartHomeApi.
    /// For example if current version is 1.2.0 and MinimalSupportedVersion is 1.1.0 then plugins which refer to
    /// SmartHomeApi.Utils 1.1.0 will work but plugins which refer to SmartHomeApi.Utils 1.0.0 will not.
    /// </summary>
    public static Version MinimalSupportedVersion { get; } = new("1.4.0.0");
}

private Assembly ContextOnResolving(AssemblyLoadContext loadContext, AssemblyName requestedAssemblyName)
{
    //Check only SmartHomeApi assemblies
    if (requestedAssemblyName?.Name == null || !requestedAssemblyName.Name.StartsWith(nameof(SmartHomeApi)) &&
        !requestedAssemblyName.Name.StartsWith($"{nameof(Common)}.{nameof(Common.Utils)}"))
        return null;

    //SmartHomeApi assemblies always have version
    if (requestedAssemblyName.Version == null) return null;

    var minimalSupportedVersion = PluginsRuntimeSettings.MinimalSupportedVersion;

    //Check that current version of SmartHomeApi supports version requested by plugin
    if (requestedAssemblyName.Version < minimalSupportedVersion)
    {
        _logger.Error($"Plugin requires {requestedAssemblyName.Name} of {requestedAssemblyName.Version} version " +
                      $"but minimal supported version is {minimalSupportedVersion}.");
        return null;
    }

    var currentAssembly = AssemblyLoadContext.Default.Assemblies.FirstOrDefault(a => a.GetName().Name == requestedAssemblyName.Name);
    
    if (currentAssembly == null) return null;

    var currentAssemblyVersion = currentAssembly.GetName().Version;

    if (currentAssemblyVersion == null) return null;

    //Don't support future major versions
    if (currentAssemblyVersion.Major < requestedAssemblyName.Version.Major)
    {
        _logger.Error($"Plugin requires {requestedAssemblyName.Name} of {requestedAssemblyName.Version} version " +
                      $"(major version is {requestedAssemblyName.Version.Major}) " +
                      $"but maximum supported major version is {currentAssemblyVersion.Major}.");
        return null;
    }

    return currentAssembly;
}

So final logic is quite simple: when application does not find dll with required version it checks if version is supported. If yes - then take required dll from Default context. Otherwise don't load plugin at all.

Full example of usage can be find in https://github.com/hdimon/SmartHomeApi -> SmartHomeApi.Core -> ItemsPluginsLocator.cs class.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 hdimon