Search code examples
c#.netdependency-injectioninversion-of-control

Exception 'No service for type 'TestPlugins.Plugin1.Service1' has been registered.' after dynamic assembly loading in .NET7


I have a plugin based solution using C# on .NET 7, using dynamic assembly loading.

Actually, the IoC container says that the service has not been registered, but in fact it has!

Exception 'No service for type 'TestPlugins.Plugin1.Service1' has been registered.'

The solution architecture like:

  • TestPlugins.PluginInterface
    1. IPlugin.cs
  • TestPlugins.Plugin1
    1. Plugin1.cs
    2. Service1.cs
  • TestPlugins.Plugin2
    1. Plugin2.cs
    2. Service2.cs
  • TestPlugins.App
    1. Program.cs
    2. Plugins/TestPlugins.Plugin1.deps.json
    3. Plugins/TestPlugins.Plugin1.dll
    4. Plugins/TestPlugins.Plugin2.deps.json
    5. Plugins/TestPlugins.Plugin2.dll

The interface:

using Microsoft.Extensions.DependencyInjection;

namespace TestPlugins.PluginInterface
{
    public interface IPlugin
    {
        void Init(IServiceCollection services);

        void Run(IServiceProvider serviceProvider);
    }
}

The plugin:

using Microsoft.Extensions.DependencyInjection;
using TestPlugins.PluginInterface;

namespace TestPlugins.Plugin1
{
    public class Plugin1 : IPlugin
    {
        public void Init(IServiceCollection services)
        {
            Console.WriteLine("Plugin1 Initing.");

            services.AddTransient<IService1, Service1>();

            Console.WriteLine("Plugin1 Inited.");
        }

        public void Run(IServiceProvider serviceProvider)
        {
            var service1 = serviceProvider.GetRequiredService<Service1>();
        }
    }
}

The service:

namespace TestPlugins.Plugin1
{
    public interface IService1
    { }

    public class Service1 : IService1
    {
        public Service1()
        {
            Console.WriteLine("Service1 Ctor.");
        }
    }
}

The program:

using Microsoft.Extensions.Hosting;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;
using TestPlugins.PluginInterface;

namespace TestPlugins.App
{
    internal class Program
    {
        private static async Task Main(string[] args)
        {
            Console.WriteLine("TestPlugins.App Starting.");

            using (IHost host = CreateHostBuilder(args).Build())
            {
                foreach (var plugin in _plugins)
                {
                    plugin.Run(host.Services);
                }

                await host.RunAsync().ConfigureAwait(false);
            }

            Console.WriteLine("TestPlugins.App Ending.");
        }

        private static readonly HashSet<IPlugin> _plugins = new();

        private static IHostBuilder CreateHostBuilder(string[] args)
        {
            var assemblies = GetPlugins();

            return Host.CreateDefaultBuilder(args)
                .ConfigureServices((context, services) =>
                {
                    foreach (var assembly in assemblies)
                    {
                        var obj = Activator.CreateInstance(assembly.Value) as IPlugin;
                        if (obj != null)
                        {
                            obj.Init(services);
                            _plugins.Add(obj);
                        }
                    }
                });
        }

        private static Dictionary<Assembly, Type> GetPlugins()
        {
            var ipluginType = typeof(IPlugin);
            var pluginDir = Path.Combine(Environment.CurrentDirectory, "Plugins");
            var pluginsDlls = Directory.GetFiles(pluginDir, "*.dll", SearchOption.AllDirectories).ToList();
            return pluginsDlls.Select(dll =>
            {
                return ReflectionsExcCatch(() =>
                {
                    var alc = new AssemblyLoadContext(dll);
                    var asm = alc.LoadFromAssemblyPath(dll);
                    var types = asm.GetTypes();
                    var ipluginTypes = types.Where(t => ipluginType.IsAssignableFrom(t) && t != ipluginType);
                    return new KeyValuePair<Assembly, Type?>(
                        asm,
                        ipluginTypes.FirstOrDefault());
                });
            })
            .Where(x => x.Value != null)
            .ToDictionary(x => x.Key, y => y.Value)!;
        }

        private static KeyValuePair<Assembly, Type?> ReflectionsExcCatch(Func<KeyValuePair<Assembly, Type?>> action)
        {
            try
            {
                return action();
            }
            catch (ReflectionTypeLoadException ex)
            {
                var sb = new StringBuilder();
                foreach (Exception exSub in ex.LoaderExceptions)
                {
                    sb.AppendLine(exSub.Message);
                    FileNotFoundException exFileNotFound = exSub as FileNotFoundException;
                    if (exFileNotFound != null)
                    {
                        if (!string.IsNullOrEmpty(exFileNotFound.FusionLog))
                        {
                            sb.AppendLine("Fusion Log:");
                            sb.AppendLine(exFileNotFound.FusionLog);
                        }
                    }
                    sb.AppendLine();
                }
                string errorMessage = sb.ToString();
                Debug.WriteLine(errorMessage);
                throw;
            }
        }
    }
}

The exception occurs there:

using (IHost host = CreateHostBuilder(args).Build())
{
  foreach (var plugin in _plugins)
  {
    plugin.Run(host.Services); // Exception  'No service for type 'TestPlugins.Plugin1.Service1' has been registered.'
  }

  await host.RunAsync().ConfigureAwait(false);
}

This is the repo with the solution, you can check it.
Any idea to solve the problem.

Best regards


Solution

  • This has little to do with the fact that you are loading assemblies dynamically. The problem is simply caused by a programming error in Plugin1.

    You are registering:

    services.AddTransient<IService1, Service1>();
    

    But you are resolving:

    serviceProvider.GetRequiredService<Service1>();
    

    In other words, you are registering the IService1 abstraction, but are resolving Service1 implementation. You should change the resolve to:

    serviceProvider.GetRequiredService<IService1>();