.NET 6 Plugin Development - "Unable to load one or more of the requested types" when adding dependency

Jason Ipock 6 Reputation points
2023-02-08T14:57:25.0366667+00:00

I am writing a series of plugins to be able to perform a specific set of actions as needed. I have been following the Microsoft documentation that can be found here: https://learn.microsoft.com/en-us/dotnet/core/tutorials/creating-app-with-plugin-support. The basic examples work as expected, however, a more complex example does not. It should be noted that I copy the contents of the build into a temp folder to read in the plugins. I also believe I have been marking the project files correctly for plugins (excluding assets, enabling dynamic loading)

WORKING SCENARIO

Here is the basic interface that will be used by two plugins at runtime. It will be kept in its own assembly (PluginBase.csproj)

// IBusinessMethod.cs
namespace PluginBase
{
    public interface IBusinessMethod
    {
        string Name { get; }
        string Description { get; }
        bool DoWork();
    }
}

<!-- PluginBase.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

Next is the sample of the code of one of the plugins. It is in an assembly which references only the PluginBase project (for the IBusinessMethod interface). The sample I have has another plugin almost identical to this one, but I am omitting it for brevity.

// CreateNewBusiness.cs
using PluginBase;

namespace CreateNewBusinessPlugin
{
    public class CreateNewBusiness : IBusinessMethod
    {
        public string Name => "Create New Business";        
        public string Description => "Adds new business to multiple data repositories";

        public bool DoWork()
        {
            Console.WriteLine("Business Added to 3 repos");
            return true;
        }        
    }
}

Here is the CreateNewBusinessPlugin.csproj file for reference:

<!-- CreateNewBusinessPlugin.csproj -->
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
	<EnableDynamicLoading>true</EnableDynamicLoading>
  </PropertyGroup>

  <Target Name="PostBuild" AfterTargets="PostBuildEvent">
    <Exec Command="set pluginpath=&quot;C:\temp\plugins\NewBusiness&quot;&#xD;&#xA;rmdir %25pluginpath%25 /S /Q&#xD;&#xA;mkdir %25pluginpath%25&#xD;&#xA;xcopy &quot;$(OutDir)*.*&quot; %25pluginpath%25 /E /Y /C" />
  </Target>

  <ItemGroup>
	  <ProjectReference Include="..\PluginBase\PluginBase.csproj">
		  <Private>false</Private>
		  <ExcludeAssets>runtime</ExcludeAssets>
	  </ProjectReference>
  </ItemGroup>

</Project>

The output from running this is here (successful run).

Loading commands from: C:\Temp\plugins\NewBusiness\CreateNewBusinessPlugin.dll
Loading commands from: C:\Temp\plugins\EvaluateBusiness\EvaluateBusinessPlugin.dll
Commands:
Create New Business      - Adds new business to multiple data repositories
Business Added to 3 repos
Evaluate Business        - Checks business for conflicts
Evaluated 3 businesses

ERROR SCENARIO
I need to add a dependency into the plugin interface project (PluginBase.csproj). Specifically, Microsoft.Extensions.Logging.Abstractions. I am now adding an Init(ILoggerFactory) function. (This is just an example that shows the issue - I am aware of .NET Hosting DI)

using Microsoft.Extensions.Logging;

namespace PluginBase
{
    public interface IBusinessMethod
    {
        string Name { get; }

        string Description { get; }

        bool DoWork();

        void Init(ILoggerFactory loggerFactory);
    }
}
<!-- PluginBase.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

	<ItemGroup>
		<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.3" />
	</ItemGroup>
</Project>

Now here I implement the interface in the CreateNewBusiness.cs and CreateNewBusinessPlugin.csproj files:

using Microsoft.Extensions.Logging;
using PluginBase;

namespace CreateNewBusinessPlugin
{
    public class CreateNewBusiness : IBusinessMethod
    {

        ILogger<CreateNewBusiness> _logger;

        public string Name => "Create New Business";
        
        public string Description => "Adds new business to multiple data repositories";

        public bool DoWork()
        {
            Console.WriteLine("Business Added to 3 repos");
            return true;
        }
        public void Init(ILoggerFactory loggerFactory)
        {
            _logger = loggerFactory.CreateLogger<CreateNewBusiness>();
            return;
        }
    }
}

<!-- CreateNewBusinessPlugin.csproj-->
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
	<EnableDynamicLoading>true</EnableDynamicLoading>
  </PropertyGroup>

     <Target Name="PostBuild" AfterTargets="PostBuildEvent">
	<Exec Command="set pluginpath=&quot;C:\temp\plugins\NewBusiness&quot;&#xD;&#xA;rmdir %25pluginpath%25 /S /Q&#xD;&#xA;mkdir %25pluginpath%25&#xD;&#xA;xcopy &quot;$(OutDir)*.*&quot; %25pluginpath%25 /E /Y /C" />
	  </Target>
	
	<ItemGroup>
		<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.3" />
	</ItemGroup>

	<ItemGroup>
	  <ProjectReference Include="..\PluginBase\PluginBase.csproj">
		  <Private>false</Private>
		  <ExcludeAssets>runtime</ExcludeAssets>
	  </ProjectReference>
  </ItemGroup>

</Project>

The resulting output:

Loading commands from: C:\Temp\plugins\NewBusiness\CreateNewBusinessPlugin.dll
System.Reflection.ReflectionTypeLoadException: Unable to load one or more of the requested types.
Method 'Init' in type 'CreateNewBusinessPlugin.CreateNewBusiness' from assembly 'CreateNewBusinessPlugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' does not have an implementation.
   at System.Reflection.RuntimeModule.GetTypes(RuntimeModule module)
   at System.Reflection.Assembly.GetTypes()
   at MyPluginApp.Program.CreateCommands(Assembly assembly)+MoveNext() in C:\Users\jipoc\source\repos\MyPluginApp\MyPluginApp\Program.cs:line 93
   at System.Collections.Generic.List`1.InsertRange(Int32 index, IEnumerable`1 collection)
   at System.Collections.Generic.List`1.AddRange(IEnumerable`1 collection)
   at System.Linq.Enumerable.SelectManySingleSelectorIterator`2.ToList()
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   at MyPluginApp.Program.Main(String[] args) in C:\Users\jipoc\source\repos\MyPluginApp\MyPluginApp\Program.cs:line 35
System.TypeLoadException: Method 'Init' in type 'CreateNewBusinessPlugin.CreateNewBusiness' from assembly 'CreateNewBusinessPlugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' does not have an implementation.

The error indicates that the new Init function is not implemented. However, going into the folder containing the plugin and using ILSpy, I get this:

// CreateNewBusinessPlugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// CreateNewBusinessPlugin.CreateNewBusiness
using System;
using CreateNewBusinessPlugin;
using Microsoft.Extensions.Logging;
using PluginBase;

public class CreateNewBusiness : IBusinessMethod
{
	private ILogger<CreateNewBusiness> _logger;

	public string Name => "Create New Business";

	public string Description => "Adds new business to multiple data repositories";

	public bool DoWork()
	{
		Console.WriteLine("Business Added to 3 repos");
		return true;
	}

	public void Init(ILoggerFactory loggerFactory)
	{
		_logger = loggerFactory.CreateLogger<CreateNewBusiness>();
	}
}

Clearly, it IS impemented, but the loader does not see it as being implemented. And below is an image of that directory (which also includes the abstractions dll)

User's image

Why can the plug-in loader not recognize the new function?? I can post my whole solution somewhere, if it would help. I don't suspect the actual loader assembly / project, as it functions just fine without the Init function and is mostly a copy from the dotnet repo sample.

Any help would be appreciated.

Thank you!

.NET
.NET
Microsoft Technologies based on the .NET software framework.
3,370 questions
C#
C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
10,238 questions
{count} votes

2 answers

Sort by: Most helpful
  1. Jason Ipock 6 Reputation points
    2023-02-08T16:19:12.2366667+00:00

    Rubber Duck Debugging works again, it would seem.

    For anyone experiencing this issue, I found the problem is that the main Plugin Loader assembly (the one finding the plugin assemblies and loading them via the AssemblyLoadContext or a derived class) could not identify the ILoggerFactory type appropriately, despite the fact the assembly was in both the plugin folder and in the bin folder of the plugin loader class.... Which was the problem!

    Hypothesis:

    I believe that the issue is that the Loader Assembly has a reference to the Microsoft.Extensions.Logger.Abstractions.dll assembly in its bin, which it is what the assembly is looking for.. EXCEPT the actual plugin is using the Microsoft.Extensions.Logger.Abstractions.dll version in its own directory. The names and versions maybe the same to us, but clearly the loading assembly didn't agree.

    Test:

    In order to test this, I did the following steps:

    1. Rebuild all plugins and the loading assembly after cleaning the plugin directories. This is a fresh start.
    2. Ensure that the latest files are in the proper locations, INCLUDING the Microsoft.Extensions.Logger.Abstractions.dll files in the loader bin and plug-in directories.
    3. Run the loading assembly.
    4. Confirm the expected error: "The Init method is not implemented....".
    5. Stop the debug session.
    6. Go into each plug-in directory and delete the Microsoft.Extensions.Logger.Abstractions.dll file from all plugin directories (but NOT the Plug-in loader assembly's bin folder).
    7. Re-run the application (don't rebuild / redeploy the plug-ins)
    8. Expected: the application now successfully loads.
    9. Confirmed!

    Rationale:

    Plugins are loaded using a derived class of AssemblyLoadContext and the AssemblyDependencyResolver it uses has its own resolver for each assembly for isolation. This allows the plugins to use different versions the same .dll / packages. ( like if one plugin uses Newtonsoft.Json 12.0.1 and another uses Newtonsoft.Json 13.0.1). But what happens when the plugin's dependency resolver can't find a dependency it needs? It then looks in the default resolver, which is the plugin loader assembly's resolver. When we remove the Microsoft.Extensions.Logging.Abstractions.dll from the plug-in folder, the plugin resolved that dependency from the default resolver instead of its own resolver. At that point, the loader assembly can match the parameter type of ILoggerFactory<> correctly. If the abstractions.dll remains in the plugin folder, the plugin's resolver has its own distinct reference to the ILoggerFactory<> class and will never match when the loader looks for ILoggerFactory<> and, thus, the "Not Implemented Exception" is thrown.

    This rationale is my $0.02 on why this works and is based upon the following link:

    https://learn.microsoft.com/en-us/dotnet/core/tutorials/creating-app-with-plugin-support#reference-a-plugin-interface-from-a-nuget-package

    This is focused on having the plugin interface being loaded as part of a nuget package. I'm not doing that yet, but the principle is the same: ensuring the types are found appropriately.

    I hope this helps someone else down the road! Shoot me a comment / question / complaint if I have something wrong - I'd love to learn more about this whole process.

    1 person found this answer helpful.

  2. Reza Aghaei 4,936 Reputation points MVP
    2023-02-08T15:39:55.98+00:00

    I think you are looking for AssemblyResolve, and a good folder structure for your plugins. I've shared an example/answer which you may find useful

    If you are loading all the plugin assemblies in the current app domain, then you need to handle the event for AppDomain.CurrentDomain and load the requested assembly in the event handler.

    No matter what folder structure you have for references, what you should do is:

    • Get all assembly files form plugins folder
    • Get all assembly files from references folder (entire hierarchy)
    • Handle AssemblyResolve of AppDomain.CurrentDomain and check if the requested assembly name is available files of reference folder, then load and return assembly.
    • For each assembly file in plugins folder, get all types and if the type implements your plugin interface, instantiate it and call its entry point for example.

    You can download or clone the code: