diff --git a/CPlugin.Net.sln b/CPlugin.Net.sln index 1a63439..7857aed 100644 --- a/CPlugin.Net.sln +++ b/CPlugin.Net.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 -VisualStudioVersion = 18.8.11912.234 insiders +VisualStudioVersion = 18.8.11912.234 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CPlugin.Net.Attributes", "src\Attributes\CPlugin.Net.Attributes.csproj", "{D9737294-F5EA-4EA0-8BF2-2BF5C3954DD9}" EndProject @@ -36,6 +36,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example.PersonPlugin", "sam EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example.ProgramPlugin", "samples\Plugins\ProgramPlugin\Example.ProgramPlugin.csproj", "{BA75A525-7013-4F0D-85FD-416E744098FD}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.GunGamePlugin", "samples\Plugins\GunGamePlugin\Example.GunGamePlugin.csproj", "{1DD684E8-9889-8431-95FB-DE64D8C411F7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.WeaponsPlugin", "samples\Plugins\WeaponsPlugin\Example.WeaponsPlugin.csproj", "{17C8B89A-5ABF-CE24-3948-9ACC366D4E86}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example.Test", "samples\Test\Example.Test.csproj", "{1E64908D-DC48-4B83-BB25-CF36821EB37F}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{7DC4CDB0-B0CD-48B5-91C7-40F05F662FA8}" @@ -53,6 +57,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestProject.WebPlugin", "te EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestProject.HelloPlugin", "tests\Plugins\TestProject.HelloPlugin\TestProject.HelloPlugin.csproj", "{E2373A36-6D3E-44DE-99FF-4CC019E05D06}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject.GunGamePlugin", "tests\Plugins\TestProject.GunGamePlugin\TestProject.GunGamePlugin.csproj", "{2F4A0A4B-B028-A62A-2E6B-BF3503E25BE3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject.WeaponsPlugin", "tests\Plugins\TestProject.WeaponsPlugin\TestProject.WeaponsPlugin.csproj", "{067880FC-D7E2-A3CE-BF10-05C2FB023F58}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestProject.Contracts", "tests\TestProject.Contracts\TestProject.Contracts.csproj", "{FEE1E508-3313-4C7A-A7E6-5EBA8D47D3FD}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestProject.PluginHost", "tests\TestProject.PluginHost\TestProject.PluginHost.csproj", "{0BCD3305-F0D5-43E6-B879-EEF0827558A8}" @@ -124,6 +132,14 @@ Global {BA75A525-7013-4F0D-85FD-416E744098FD}.Debug|Any CPU.Build.0 = Debug|Any CPU {BA75A525-7013-4F0D-85FD-416E744098FD}.Release|Any CPU.ActiveCfg = Release|Any CPU {BA75A525-7013-4F0D-85FD-416E744098FD}.Release|Any CPU.Build.0 = Release|Any CPU + {1DD684E8-9889-8431-95FB-DE64D8C411F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1DD684E8-9889-8431-95FB-DE64D8C411F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1DD684E8-9889-8431-95FB-DE64D8C411F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1DD684E8-9889-8431-95FB-DE64D8C411F7}.Release|Any CPU.Build.0 = Release|Any CPU + {17C8B89A-5ABF-CE24-3948-9ACC366D4E86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17C8B89A-5ABF-CE24-3948-9ACC366D4E86}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17C8B89A-5ABF-CE24-3948-9ACC366D4E86}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17C8B89A-5ABF-CE24-3948-9ACC366D4E86}.Release|Any CPU.Build.0 = Release|Any CPU {1E64908D-DC48-4B83-BB25-CF36821EB37F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1E64908D-DC48-4B83-BB25-CF36821EB37F}.Debug|Any CPU.Build.0 = Debug|Any CPU {1E64908D-DC48-4B83-BB25-CF36821EB37F}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -152,6 +168,14 @@ Global {0BCD3305-F0D5-43E6-B879-EEF0827558A8}.Debug|Any CPU.Build.0 = Debug|Any CPU {0BCD3305-F0D5-43E6-B879-EEF0827558A8}.Release|Any CPU.ActiveCfg = Release|Any CPU {0BCD3305-F0D5-43E6-B879-EEF0827558A8}.Release|Any CPU.Build.0 = Release|Any CPU + {2F4A0A4B-B028-A62A-2E6B-BF3503E25BE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F4A0A4B-B028-A62A-2E6B-BF3503E25BE3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F4A0A4B-B028-A62A-2E6B-BF3503E25BE3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F4A0A4B-B028-A62A-2E6B-BF3503E25BE3}.Release|Any CPU.Build.0 = Release|Any CPU + {067880FC-D7E2-A3CE-BF10-05C2FB023F58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {067880FC-D7E2-A3CE-BF10-05C2FB023F58}.Debug|Any CPU.Build.0 = Debug|Any CPU + {067880FC-D7E2-A3CE-BF10-05C2FB023F58}.Release|Any CPU.ActiveCfg = Release|Any CPU + {067880FC-D7E2-A3CE-BF10-05C2FB023F58}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -169,6 +193,8 @@ Global {2832E5C3-660E-49B9-8E62-03B11A8CC6D7} = {FA19544E-3925-4A1C-A994-CFA72FDC10AC} {18534944-583B-4924-AC5B-E0655FD92AAC} = {FA19544E-3925-4A1C-A994-CFA72FDC10AC} {BA75A525-7013-4F0D-85FD-416E744098FD} = {FA19544E-3925-4A1C-A994-CFA72FDC10AC} + {1DD684E8-9889-8431-95FB-DE64D8C411F7} = {FA19544E-3925-4A1C-A994-CFA72FDC10AC} + {17C8B89A-5ABF-CE24-3948-9ACC366D4E86} = {FA19544E-3925-4A1C-A994-CFA72FDC10AC} {1E64908D-DC48-4B83-BB25-CF36821EB37F} = {A0059E66-B781-496A-AE13-CD83200A4612} {290C410D-33F9-4C82-A334-04709D751E16} = {7DC4CDB0-B0CD-48B5-91C7-40F05F662FA8} {0F27C776-F284-4C94-86C9-0FF089245E13} = {290C410D-33F9-4C82-A334-04709D751E16} @@ -177,6 +203,8 @@ Global {E2373A36-6D3E-44DE-99FF-4CC019E05D06} = {290C410D-33F9-4C82-A334-04709D751E16} {FEE1E508-3313-4C7A-A7E6-5EBA8D47D3FD} = {7DC4CDB0-B0CD-48B5-91C7-40F05F662FA8} {0BCD3305-F0D5-43E6-B879-EEF0827558A8} = {7DC4CDB0-B0CD-48B5-91C7-40F05F662FA8} + {2F4A0A4B-B028-A62A-2E6B-BF3503E25BE3} = {290C410D-33F9-4C82-A334-04709D751E16} + {067880FC-D7E2-A3CE-BF10-05C2FB023F58} = {290C410D-33F9-4C82-A334-04709D751E16} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {67EE578E-EE0A-4E98-AE5A-1771751D8B5F} diff --git a/Directory.Packages.props b/Directory.Packages.props index b9f7f0d..a8bc4de 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,6 +7,7 @@ + diff --git a/samples/Contracts/IPlugin.cs b/samples/Contracts/IPlugin.cs new file mode 100644 index 0000000..481fc6f --- /dev/null +++ b/samples/Contracts/IPlugin.cs @@ -0,0 +1,8 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Example.Contracts; + +public interface IPlugin +{ + void ConfigureServices(IServiceCollection services); +} diff --git a/samples/Contracts/IWeapon.cs b/samples/Contracts/IWeapon.cs new file mode 100644 index 0000000..fc0c8af --- /dev/null +++ b/samples/Contracts/IWeapon.cs @@ -0,0 +1,8 @@ +namespace Example.Contracts; + +public interface IWeapon +{ + string Id { get; } + string Name { get; } + int Slot { get; } +} diff --git a/samples/HostApplications/ConsoleApp/.env b/samples/HostApplications/ConsoleApp/.env index b2a376d..d0f8a19 100644 --- a/samples/HostApplications/ConsoleApp/.env +++ b/samples/HostApplications/ConsoleApp/.env @@ -1 +1,6 @@ -PLUGINS=Example.OldJsonPlugin.dll Example.JsonPlugin.dll \ No newline at end of file +PLUGINS=" +Example.OldJsonPlugin.dll +Example.JsonPlugin.dll +Example.GunGamePlugin.dll +Example.WeaponsPlugin.dll +" \ No newline at end of file diff --git a/samples/HostApplications/ConsoleApp/Example.HostConsoleApp.csproj b/samples/HostApplications/ConsoleApp/Example.HostConsoleApp.csproj index 624312d..cf877d1 100644 --- a/samples/HostApplications/ConsoleApp/Example.HostConsoleApp.csproj +++ b/samples/HostApplications/ConsoleApp/Example.HostConsoleApp.csproj @@ -6,6 +6,7 @@ + diff --git a/samples/HostApplications/ConsoleApp/GlobalUsings.cs b/samples/HostApplications/ConsoleApp/GlobalUsings.cs index 1c38369..b979868 100644 --- a/samples/HostApplications/ConsoleApp/GlobalUsings.cs +++ b/samples/HostApplications/ConsoleApp/GlobalUsings.cs @@ -1,3 +1,4 @@ -global using Example.Contracts; +global using Microsoft.Extensions.DependencyInjection; +global using Example.Contracts; global using CPlugin.Net; global using DotEnv.Core; diff --git a/samples/HostApplications/ConsoleApp/Program.cs b/samples/HostApplications/ConsoleApp/Program.cs index 51f5e94..174e9dd 100644 --- a/samples/HostApplications/ConsoleApp/Program.cs +++ b/samples/HostApplications/ConsoleApp/Program.cs @@ -1,4 +1,5 @@ // See https://aka.ms/new-console-template for more information + new EnvLoader().Load(); Console.WriteLine("PLUGINS=" + EnvReader.Instance["PLUGINS"]); Console.WriteLine(); @@ -6,8 +7,15 @@ var envConfiguration = new CPluginEnvConfiguration(); // Loads the plugins from the .env file. PluginLoader.Load(envConfiguration); -var commands = TypeFinder.FindSubtypesOf(); + +var services = new ServiceCollection(); +var plugins = TypeFinder.FindSubtypesOf(); +foreach (IPlugin plugin in plugins) + plugin.ConfigureServices(services); + +services.AddSubtypesOf(ServiceLifetime.Transient); + +var serviceProvider = services.BuildServiceProvider(); +var commands = serviceProvider.GetServices(); foreach (ICommand command in commands) -{ command.Execute(); -} diff --git a/samples/Plugins/GunGamePlugin/Example.GunGamePlugin.csproj b/samples/Plugins/GunGamePlugin/Example.GunGamePlugin.csproj new file mode 100644 index 0000000..1749034 --- /dev/null +++ b/samples/Plugins/GunGamePlugin/Example.GunGamePlugin.csproj @@ -0,0 +1,7 @@ + + + + $(ConsoleAppProjectDir) + + + diff --git a/samples/Plugins/GunGamePlugin/GlobalUsings.cs b/samples/Plugins/GunGamePlugin/GlobalUsings.cs new file mode 100644 index 0000000..d85f3f0 --- /dev/null +++ b/samples/Plugins/GunGamePlugin/GlobalUsings.cs @@ -0,0 +1,3 @@ +global using CPlugin.Net; +global using Example.GunGamePlugin; +global using Example.Contracts; \ No newline at end of file diff --git a/samples/Plugins/GunGamePlugin/GunGameCommand.cs b/samples/Plugins/GunGamePlugin/GunGameCommand.cs new file mode 100644 index 0000000..532e9b6 --- /dev/null +++ b/samples/Plugins/GunGamePlugin/GunGameCommand.cs @@ -0,0 +1,30 @@ +[assembly: Plugin(typeof(GunGameCommand))] +[assembly: DependsOn("Example.WeaponsPlugin")] + +namespace Example.GunGamePlugin; + +internal class GunGameCommand(IEnumerable weapons) : ICommand +{ + public string Name => "gungame"; + public string Description => "Lists the available weapons."; + public string Version => "1.0.0"; + + public int Execute() + { + Console.WriteLine(); + Console.WriteLine("=== GunGame Plugin ==="); + Console.WriteLine($"Command : {Name}"); + Console.WriteLine($"Description : {Description}"); + Console.WriteLine($"Version : {Version}"); + Console.WriteLine(); + Console.WriteLine("Registered weapons:"); + + foreach (IWeapon weapon in weapons.OrderBy(w => w.Slot)) + { + Console.WriteLine($" [{weapon.Slot}] {weapon.Name} (Id: {weapon.Id})"); + } + + Console.WriteLine(); + return 0; + } +} diff --git a/samples/Plugins/WeaponsPlugin/Ak47.cs b/samples/Plugins/WeaponsPlugin/Ak47.cs new file mode 100644 index 0000000..62adb50 --- /dev/null +++ b/samples/Plugins/WeaponsPlugin/Ak47.cs @@ -0,0 +1,8 @@ +namespace Example.WeaponsPlugin; + +internal class Ak47 : IWeapon +{ + public string Id => "ak47"; + public string Name => "AK-47"; + public int Slot => 2; +} diff --git a/samples/Plugins/WeaponsPlugin/Example.WeaponsPlugin.csproj b/samples/Plugins/WeaponsPlugin/Example.WeaponsPlugin.csproj new file mode 100644 index 0000000..19840bb --- /dev/null +++ b/samples/Plugins/WeaponsPlugin/Example.WeaponsPlugin.csproj @@ -0,0 +1,7 @@ + + + + $(ConsoleAppProjectDir) + + + diff --git a/samples/Plugins/WeaponsPlugin/GlobalUsings.cs b/samples/Plugins/WeaponsPlugin/GlobalUsings.cs new file mode 100644 index 0000000..8f05eee --- /dev/null +++ b/samples/Plugins/WeaponsPlugin/GlobalUsings.cs @@ -0,0 +1,4 @@ +global using Microsoft.Extensions.DependencyInjection; +global using CPlugin.Net; +global using Example.Contracts; +global using Example.WeaponsPlugin; \ No newline at end of file diff --git a/samples/Plugins/WeaponsPlugin/Pistol.cs b/samples/Plugins/WeaponsPlugin/Pistol.cs new file mode 100644 index 0000000..6b27b11 --- /dev/null +++ b/samples/Plugins/WeaponsPlugin/Pistol.cs @@ -0,0 +1,8 @@ +namespace Example.WeaponsPlugin; + +internal class Pistol : IWeapon +{ + public string Id => "pistol"; + public string Name => "Pistol"; + public int Slot => 1; +} diff --git a/samples/Plugins/WeaponsPlugin/WeaponsPlugin.cs b/samples/Plugins/WeaponsPlugin/WeaponsPlugin.cs new file mode 100644 index 0000000..b53c0af --- /dev/null +++ b/samples/Plugins/WeaponsPlugin/WeaponsPlugin.cs @@ -0,0 +1,12 @@ +[assembly: Plugin(typeof(WeaponsPlugin))] + +namespace Example.WeaponsPlugin; + +internal class WeaponsPlugin : IPlugin +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + } +} diff --git a/src/Attributes/DependsOnAttribute.cs b/src/Attributes/DependsOnAttribute.cs new file mode 100644 index 0000000..12a383d --- /dev/null +++ b/src/Attributes/DependsOnAttribute.cs @@ -0,0 +1,46 @@ +using System; + +namespace CPlugin.Net; + +/// +/// Specifies that the current plugin depends on another plugin. +/// +/// +/// Example: +/// +/// If GunGame depends on Weapons, add this line before the namespace declaration: +/// +/// [assembly: DependsOn("Weapons")] +/// +/// During plugin loading, the specified plugin must also be present in the plugin +/// configuration; otherwise, a PluginDependencyException is thrown. +/// +/// +[AttributeUsage(AttributeTargets.Assembly, Inherited = false, AllowMultiple = true)] +public class DependsOnAttribute : Attribute +{ + /// + /// Gets the name of the required plugin. + /// + public string PluginName { get; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The name of the plugin that the current plugin depends on. + /// The .dll extension is optional. + /// + /// + /// pluginName is null. + /// + public DependsOnAttribute(string pluginName) + { + if (pluginName is null) + { + throw new ArgumentNullException(nameof(pluginName)); + } + + PluginName = pluginName; + } +} diff --git a/src/Core/Configuration/CPluginConfigurationBase.cs b/src/Core/Configuration/CPluginConfigurationBase.cs index e570d75..88619e0 100644 --- a/src/Core/Configuration/CPluginConfigurationBase.cs +++ b/src/Core/Configuration/CPluginConfigurationBase.cs @@ -40,11 +40,15 @@ protected static string GetPluginPath(string pluginFile) bool hasDllExtension = Path .GetExtension(pluginFile) .Equals(".dll"); + pluginFile = hasDllExtension ? pluginFile : pluginFile + ".dll"; + // Example: MyPlugin1 var pluginDirectory = Path.GetFileNameWithoutExtension(pluginFile); + // Example: /home/admin/HostApplication/bin/Debug/net7.0/plugins/MyPlugin1 var basePath = Path.Combine(AppContext.BaseDirectory, "plugins", pluginDirectory); + // Example: /home/admin/HostApplication/bin/Debug/net7.0/plugins/MyPlugin1/MyPlugin1.dll var pluginPath = Path.Combine(basePath, pluginFile); return pluginPath; diff --git a/src/Core/Exceptions/PluginDependencyException.cs b/src/Core/Exceptions/PluginDependencyException.cs new file mode 100644 index 0000000..3c1247d --- /dev/null +++ b/src/Core/Exceptions/PluginDependencyException.cs @@ -0,0 +1,19 @@ +using System; + +namespace CPlugin.Net; + +/// +/// The exception that is thrown when a plugin dependency cannot be resolved. +/// +public class PluginDependencyException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// The message that describes the error. + /// + public PluginDependencyException(string message) : base(message) + { + } +} diff --git a/src/Core/PluginLoader.cs b/src/Core/PluginLoader.cs index a5ed262..f0d6de5 100644 --- a/src/Core/PluginLoader.cs +++ b/src/Core/PluginLoader.cs @@ -30,25 +30,51 @@ public static class PluginLoader /// /// configuration is null. /// + /// + /// A plugin declares a dependency that cannot be resolved. + /// public static void Load(CPluginConfigurationBase configuration) { ArgumentNullException.ThrowIfNull(configuration); - var assemblyFiles = configuration.GetPluginFiles(); - foreach (string assemblyFile in assemblyFiles) + + string[] pluginFiles = [.. configuration.GetPluginFiles()]; + HashSet configuredPluginNames = pluginFiles + .Select(Path.GetFileName) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + foreach (string pluginFile in pluginFiles) { - Assembly currentAssembly = FindAssembly(assemblyFile); - if (currentAssembly is null) - LoadAssembly(assemblyFile); + Assembly assembly = FindAssembly(pluginFile); + + if (assembly is not null) + continue; + + assembly = LoadAssembly(pluginFile); + DependsOnAttribute[] dependencies = [.. assembly.GetCustomAttributes()]; + + foreach (DependsOnAttribute dependency in dependencies) + { + var normalizedPluginName = PluginNameNormalizer.Normalize(dependency.PluginName); + if (!configuredPluginNames.Contains(normalizedPluginName)) + { + var message = + $"The plugin '{assembly.GetName().Name}' depends on '{normalizedPluginName}', " + + $"but '{normalizedPluginName}' was not found in the plugin configuration."; + + throw new PluginDependencyException(message); + } + } } } - private static void LoadAssembly(string assemblyFile) + private static Assembly LoadAssembly(string assemblyFile) { var loadContext = new PluginLoadContext(assemblyFile); var assemblyName = AssemblyName.GetAssemblyName(assemblyFile); var currentAssembly = loadContext.LoadFromAssemblyName(assemblyName); s_assemblies.TryAdd(assemblyFile, currentAssembly); PluginLogger.DefaultLogInformation(currentAssembly.GetName().Name, currentAssembly.FullName); + return currentAssembly; } private static Assembly FindAssembly(string assemblyFile) diff --git a/src/Core/PluginNameNormalizer.cs b/src/Core/PluginNameNormalizer.cs new file mode 100644 index 0000000..aa78db9 --- /dev/null +++ b/src/Core/PluginNameNormalizer.cs @@ -0,0 +1,9 @@ +namespace CPlugin.Net; + +internal class PluginNameNormalizer +{ + public static string Normalize(string pluginName) + => pluginName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) + ? pluginName + : $"{pluginName}.dll"; +} diff --git a/tests/CPlugin.Net/CPlugin.Net.Tests.csproj b/tests/CPlugin.Net/CPlugin.Net.Tests.csproj index 74c53f1..52d7f65 100644 --- a/tests/CPlugin.Net/CPlugin.Net.Tests.csproj +++ b/tests/CPlugin.Net/CPlugin.Net.Tests.csproj @@ -32,6 +32,8 @@ + + diff --git a/tests/CPlugin.Net/Core/PluginLoaderTests.cs b/tests/CPlugin.Net/Core/PluginLoaderTests.cs index b7309f2..9198ec3 100644 --- a/tests/CPlugin.Net/Core/PluginLoaderTests.cs +++ b/tests/CPlugin.Net/Core/PluginLoaderTests.cs @@ -79,4 +79,54 @@ public void Load_WhenMethodIsCalledMultipleTimes_ShouldNotLoadSamePluginsIntoMem .Should() .Be(expectedAssemblies); } + + [Test] + public void Load_WhenPluginDependencyIsNotConfigured_ShouldThrowPluginDependencyException() + { + // Arrange + var value = "TestProject.GunGamePlugin.dll"; + Environment.SetEnvironmentVariable("PLUGINS", value); + var configuration = new CPluginEnvConfiguration(); + + // Act + Action act = () => PluginLoader.Load(configuration); + + // Assert + act.Should() + .Throw() + .WithMessage("*TestProject.WeaponsPlugin.dll*"); + } + + [Test] + public void Load_WhenPluginDependencyIsResolved_ShouldLoadPluginsSuccessfully() + { + // Arrange + var value = + """ + TestProject.WeaponsPlugin.dll + TestProject.GunGamePlugin.dll + """; + + Environment.SetEnvironmentVariable("PLUGINS", value); + var configuration = new CPluginEnvConfiguration(); + var services = new ServiceCollection(); + + // Act + PluginLoader.Load(configuration); + + var weaponPlugins = TypeFinder.FindSubtypesOf(); + foreach (IWeaponPlugin weaponPlugin in weaponPlugins) + { + weaponPlugin.ConfigureServices(services); + } + + services.AddSubtypesOf(ServiceLifetime.Transient); + + using ServiceProvider serviceProvider = services.BuildServiceProvider(); + var gameModes = serviceProvider.GetServices().ToArray(); + + // Assert + string weapons = gameModes[0].ExecuteAction(); + weapons.Should().Be("Pistol, AK-47"); + } } diff --git a/tests/Plugins/TestProject.GunGamePlugin/GunGameMode.cs b/tests/Plugins/TestProject.GunGamePlugin/GunGameMode.cs new file mode 100644 index 0000000..e1a9b4c --- /dev/null +++ b/tests/Plugins/TestProject.GunGamePlugin/GunGameMode.cs @@ -0,0 +1,18 @@ +using CPlugin.Net; +using TestProject.Contracts; +using TestProject.GunGamePlugin; + +[assembly: Plugin(typeof(GunGameMode))] +[assembly: DependsOn("TestProject.WeaponsPlugin")] + +namespace TestProject.GunGamePlugin; + +internal class GunGameMode(IEnumerable weapons) : IGameMode +{ + public string Name => nameof(GunGameMode); + + public string ExecuteAction() + { + return string.Join(", ", weapons.Select(w => w.Name)); + } +} diff --git a/tests/Plugins/TestProject.GunGamePlugin/TestProject.GunGamePlugin.csproj b/tests/Plugins/TestProject.GunGamePlugin/TestProject.GunGamePlugin.csproj new file mode 100644 index 0000000..a68f5a0 --- /dev/null +++ b/tests/Plugins/TestProject.GunGamePlugin/TestProject.GunGamePlugin.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/tests/Plugins/TestProject.WeaponsPlugin/Ak47.cs b/tests/Plugins/TestProject.WeaponsPlugin/Ak47.cs new file mode 100644 index 0000000..6183aa1 --- /dev/null +++ b/tests/Plugins/TestProject.WeaponsPlugin/Ak47.cs @@ -0,0 +1,10 @@ +using TestProject.Contracts; + +namespace TestProject.WeaponsPlugin; + +internal class Ak47 : IWeapon +{ + public string Id => "ak47"; + public string Name => "AK-47"; + public int Slot => 2; +} diff --git a/tests/Plugins/TestProject.WeaponsPlugin/Pistol.cs b/tests/Plugins/TestProject.WeaponsPlugin/Pistol.cs new file mode 100644 index 0000000..9bd6b26 --- /dev/null +++ b/tests/Plugins/TestProject.WeaponsPlugin/Pistol.cs @@ -0,0 +1,10 @@ +using TestProject.Contracts; + +namespace TestProject.WeaponsPlugin; + +internal class Pistol : IWeapon +{ + public string Id => "pistol"; + public string Name => "Pistol"; + public int Slot => 1; +} diff --git a/tests/Plugins/TestProject.WeaponsPlugin/TestProject.WeaponsPlugin.csproj b/tests/Plugins/TestProject.WeaponsPlugin/TestProject.WeaponsPlugin.csproj new file mode 100644 index 0000000..a68f5a0 --- /dev/null +++ b/tests/Plugins/TestProject.WeaponsPlugin/TestProject.WeaponsPlugin.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/tests/Plugins/TestProject.WeaponsPlugin/WeaponsPlugin.cs b/tests/Plugins/TestProject.WeaponsPlugin/WeaponsPlugin.cs new file mode 100644 index 0000000..7853db5 --- /dev/null +++ b/tests/Plugins/TestProject.WeaponsPlugin/WeaponsPlugin.cs @@ -0,0 +1,19 @@ +using CPlugin.Net; +using Microsoft.Extensions.DependencyInjection; +using TestProject.Contracts; +using TestProject.WeaponsPlugin; + +[assembly: Plugin(typeof(WeaponsPlugin))] + +namespace TestProject.WeaponsPlugin; + +internal class WeaponsPlugin : IWeaponPlugin +{ + public string Name => nameof(WeaponsPlugin); + + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + } +} diff --git a/tests/TestProject.Contracts/IGameMode.cs b/tests/TestProject.Contracts/IGameMode.cs new file mode 100644 index 0000000..d768047 --- /dev/null +++ b/tests/TestProject.Contracts/IGameMode.cs @@ -0,0 +1,7 @@ +namespace TestProject.Contracts; + +public interface IGameMode +{ + public string Name { get; } + string ExecuteAction(); +} diff --git a/tests/TestProject.Contracts/IWeapon.cs b/tests/TestProject.Contracts/IWeapon.cs new file mode 100644 index 0000000..dff1691 --- /dev/null +++ b/tests/TestProject.Contracts/IWeapon.cs @@ -0,0 +1,8 @@ +namespace TestProject.Contracts; + +public interface IWeapon +{ + string Id { get; } + string Name { get; } + int Slot { get; } +} diff --git a/tests/TestProject.Contracts/IWeaponPlugin.cs b/tests/TestProject.Contracts/IWeaponPlugin.cs new file mode 100644 index 0000000..708df40 --- /dev/null +++ b/tests/TestProject.Contracts/IWeaponPlugin.cs @@ -0,0 +1,8 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace TestProject.Contracts; + +public interface IWeaponPlugin +{ + void ConfigureServices(IServiceCollection services); +}