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);
+}