From 09013715186e890d7f1178f6f1210fbd264980d4 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Wed, 29 Jul 2026 10:42:27 -0500 Subject: [PATCH 1/6] chore: add new package reference --- Directory.Packages.props | 1 + 1 file changed, 1 insertion(+) 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 @@ + From 3edb088fc21b614b9e36ca8a410a18ca8ffa99d3 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Wed, 29 Jul 2026 10:44:19 -0500 Subject: [PATCH 2/6] feat(core): add plugin dependency validation Introduce the DependsOnAttribute to declare plugin dependencies and validate them during plugin loading. PluginLoader now throws PluginDependencyException when a declared dependency cannot be resolved from the configured plugin list. --- src/Attributes/DependsOnAttribute.cs | 46 +++++++++++++++++++ .../Exceptions/PluginDependencyException.cs | 19 ++++++++ src/Core/PluginLoader.cs | 39 +++++++++++++--- src/Core/PluginNameNormalizer.cs | 9 ++++ 4 files changed, 107 insertions(+), 6 deletions(-) create mode 100644 src/Attributes/DependsOnAttribute.cs create mode 100644 src/Core/Exceptions/PluginDependencyException.cs create mode 100644 src/Core/PluginNameNormalizer.cs 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/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..b5fa5f4 100644 --- a/src/Core/PluginLoader.cs +++ b/src/Core/PluginLoader.cs @@ -30,25 +30,52 @@ 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) + .Select(PluginNameNormalizer.Normalize) + .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"; +} From 6560e365bd02ca52af042b71001de3cb632a8f3a Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Wed, 29 Jul 2026 10:49:00 -0500 Subject: [PATCH 3/6] docs(samples): add plugin dependency example Add sample plugins demonstrating how to declare plugin dependencies using DependsOnAttribute and how plugins can consume services registered by other plugins. --- CPlugin.Net.sln | 16 +++++++++- samples/Contracts/IPlugin.cs | 8 +++++ samples/Contracts/IWeapon.cs | 8 +++++ samples/HostApplications/ConsoleApp/.env | 7 ++++- .../ConsoleApp/Example.HostConsoleApp.csproj | 1 + .../ConsoleApp/GlobalUsings.cs | 3 +- .../HostApplications/ConsoleApp/Program.cs | 14 +++++++-- .../Example.GunGamePlugin.csproj | 7 +++++ samples/Plugins/GunGamePlugin/GlobalUsings.cs | 3 ++ .../Plugins/GunGamePlugin/GunGameCommand.cs | 30 +++++++++++++++++++ samples/Plugins/WeaponsPlugin/Ak47.cs | 8 +++++ .../Example.WeaponsPlugin.csproj | 7 +++++ samples/Plugins/WeaponsPlugin/GlobalUsings.cs | 4 +++ samples/Plugins/WeaponsPlugin/Pistol.cs | 8 +++++ .../Plugins/WeaponsPlugin/WeaponsPlugin.cs | 12 ++++++++ 15 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 samples/Contracts/IPlugin.cs create mode 100644 samples/Contracts/IWeapon.cs create mode 100644 samples/Plugins/GunGamePlugin/Example.GunGamePlugin.csproj create mode 100644 samples/Plugins/GunGamePlugin/GlobalUsings.cs create mode 100644 samples/Plugins/GunGamePlugin/GunGameCommand.cs create mode 100644 samples/Plugins/WeaponsPlugin/Ak47.cs create mode 100644 samples/Plugins/WeaponsPlugin/Example.WeaponsPlugin.csproj create mode 100644 samples/Plugins/WeaponsPlugin/GlobalUsings.cs create mode 100644 samples/Plugins/WeaponsPlugin/Pistol.cs create mode 100644 samples/Plugins/WeaponsPlugin/WeaponsPlugin.cs diff --git a/CPlugin.Net.sln b/CPlugin.Net.sln index 1a63439..978ac6e 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}" @@ -152,6 +156,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 + {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -177,6 +189,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} + {1DD684E8-9889-8431-95FB-DE64D8C411F7} = {FA19544E-3925-4A1C-A994-CFA72FDC10AC} + {17C8B89A-5ABF-CE24-3948-9ACC366D4E86} = {FA19544E-3925-4A1C-A994-CFA72FDC10AC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {67EE578E-EE0A-4E98-AE5A-1771751D8B5F} 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(); + } +} From f27fe071756fd93bbba4f738e41bb9bd85be9e1a Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Wed, 29 Jul 2026 11:11:03 -0500 Subject: [PATCH 4/6] refactor(core): remove redundant plugin name normalization Remove the redundant normalization of configured plugin names. CPluginConfigurationBase already guarantees that plugin files use the .dll extension, so only dependency names declared with DependsOnAttribute need to be normalized before comparison. --- src/Core/PluginLoader.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/PluginLoader.cs b/src/Core/PluginLoader.cs index b5fa5f4..f0d6de5 100644 --- a/src/Core/PluginLoader.cs +++ b/src/Core/PluginLoader.cs @@ -40,7 +40,6 @@ public static void Load(CPluginConfigurationBase configuration) string[] pluginFiles = [.. configuration.GetPluginFiles()]; HashSet configuredPluginNames = pluginFiles .Select(Path.GetFileName) - .Select(PluginNameNormalizer.Normalize) .ToHashSet(StringComparer.OrdinalIgnoreCase); foreach (string pluginFile in pluginFiles) From a40b62c62aa950496b233c8a82e9db1867a5aaa2 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Wed, 29 Jul 2026 11:13:23 -0500 Subject: [PATCH 5/6] style: apply consistent code formatting --- src/Core/Configuration/CPluginConfigurationBase.cs | 4 ++++ 1 file changed, 4 insertions(+) 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; From 7c5ba394b9dbcb2f526e3a951ea1514eba7c7eb2 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Wed, 29 Jul 2026 14:34:30 -0500 Subject: [PATCH 6/6] test(core): add plugin dependency tests Add tests covering plugin dependency validation. Verify that PluginLoader throws PluginDependencyException when a dependency is missing and successfully loads plugins when all declared dependencies are configured. --- CPlugin.Net.sln | 34 +++++++++---- tests/CPlugin.Net/CPlugin.Net.Tests.csproj | 2 + tests/CPlugin.Net/Core/PluginLoaderTests.cs | 50 +++++++++++++++++++ .../TestProject.GunGamePlugin/GunGameMode.cs | 18 +++++++ .../TestProject.GunGamePlugin.csproj | 7 +++ .../Plugins/TestProject.WeaponsPlugin/Ak47.cs | 10 ++++ .../TestProject.WeaponsPlugin/Pistol.cs | 10 ++++ .../TestProject.WeaponsPlugin.csproj | 7 +++ .../WeaponsPlugin.cs | 19 +++++++ tests/TestProject.Contracts/IGameMode.cs | 7 +++ tests/TestProject.Contracts/IWeapon.cs | 8 +++ tests/TestProject.Contracts/IWeaponPlugin.cs | 8 +++ 12 files changed, 170 insertions(+), 10 deletions(-) create mode 100644 tests/Plugins/TestProject.GunGamePlugin/GunGameMode.cs create mode 100644 tests/Plugins/TestProject.GunGamePlugin/TestProject.GunGamePlugin.csproj create mode 100644 tests/Plugins/TestProject.WeaponsPlugin/Ak47.cs create mode 100644 tests/Plugins/TestProject.WeaponsPlugin/Pistol.cs create mode 100644 tests/Plugins/TestProject.WeaponsPlugin/TestProject.WeaponsPlugin.csproj create mode 100644 tests/Plugins/TestProject.WeaponsPlugin/WeaponsPlugin.cs create mode 100644 tests/TestProject.Contracts/IGameMode.cs create mode 100644 tests/TestProject.Contracts/IWeapon.cs create mode 100644 tests/TestProject.Contracts/IWeaponPlugin.cs diff --git a/CPlugin.Net.sln b/CPlugin.Net.sln index 978ac6e..7857aed 100644 --- a/CPlugin.Net.sln +++ b/CPlugin.Net.sln @@ -57,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}" @@ -128,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 @@ -156,14 +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 - {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 + {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 @@ -181,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} @@ -189,8 +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} - {1DD684E8-9889-8431-95FB-DE64D8C411F7} = {FA19544E-3925-4A1C-A994-CFA72FDC10AC} - {17C8B89A-5ABF-CE24-3948-9ACC366D4E86} = {FA19544E-3925-4A1C-A994-CFA72FDC10AC} + {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/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); +}