Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion CPlugin.Net.sln
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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}"
Expand All @@ -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}"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand All @@ -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}
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="7.0.10" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
Expand Down
8 changes: 8 additions & 0 deletions samples/Contracts/IPlugin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Microsoft.Extensions.DependencyInjection;

namespace Example.Contracts;

public interface IPlugin
{
void ConfigureServices(IServiceCollection services);
}
8 changes: 8 additions & 0 deletions samples/Contracts/IWeapon.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Example.Contracts;

public interface IWeapon
{
string Id { get; }
string Name { get; }
int Slot { get; }
}
7 changes: 6 additions & 1 deletion samples/HostApplications/ConsoleApp/.env
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
PLUGINS=Example.OldJsonPlugin.dll Example.JsonPlugin.dll
PLUGINS="
Example.OldJsonPlugin.dll
Example.JsonPlugin.dll
Example.GunGamePlugin.dll
Example.WeaponsPlugin.dll
"
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

<ItemGroup>
<PackageReference Include="DotEnv.Core" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="CopyPluginsToPublishDirectory" />
</ItemGroup>

Expand Down
3 changes: 2 additions & 1 deletion samples/HostApplications/ConsoleApp/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -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;
14 changes: 11 additions & 3 deletions samples/HostApplications/ConsoleApp/Program.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
// See https://aka.ms/new-console-template for more information

new EnvLoader().Load();
Console.WriteLine("PLUGINS=" + EnvReader.Instance["PLUGINS"]);
Console.WriteLine();

var envConfiguration = new CPluginEnvConfiguration();
// Loads the plugins from the .env file.
PluginLoader.Load(envConfiguration);
var commands = TypeFinder.FindSubtypesOf<ICommand>();

var services = new ServiceCollection();
var plugins = TypeFinder.FindSubtypesOf<IPlugin>();
foreach (IPlugin plugin in plugins)
plugin.ConfigureServices(services);

services.AddSubtypesOf<ICommand>(ServiceLifetime.Transient);

var serviceProvider = services.BuildServiceProvider();
var commands = serviceProvider.GetServices<ICommand>();
foreach (ICommand command in commands)
{
command.Execute();
}
7 changes: 7 additions & 0 deletions samples/Plugins/GunGamePlugin/Example.GunGamePlugin.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutDir>$(ConsoleAppProjectDir)</OutDir>
</PropertyGroup>

</Project>
3 changes: 3 additions & 0 deletions samples/Plugins/GunGamePlugin/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
global using CPlugin.Net;
global using Example.GunGamePlugin;
global using Example.Contracts;
30 changes: 30 additions & 0 deletions samples/Plugins/GunGamePlugin/GunGameCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[assembly: Plugin(typeof(GunGameCommand))]
[assembly: DependsOn("Example.WeaponsPlugin")]

namespace Example.GunGamePlugin;

internal class GunGameCommand(IEnumerable<IWeapon> 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;
}
}
8 changes: 8 additions & 0 deletions samples/Plugins/WeaponsPlugin/Ak47.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Example.WeaponsPlugin;

internal class Ak47 : IWeapon
{
public string Id => "ak47";
public string Name => "AK-47";
public int Slot => 2;
}
7 changes: 7 additions & 0 deletions samples/Plugins/WeaponsPlugin/Example.WeaponsPlugin.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutDir>$(ConsoleAppProjectDir)</OutDir>
</PropertyGroup>

</Project>
4 changes: 4 additions & 0 deletions samples/Plugins/WeaponsPlugin/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
global using Microsoft.Extensions.DependencyInjection;
global using CPlugin.Net;
global using Example.Contracts;
global using Example.WeaponsPlugin;
8 changes: 8 additions & 0 deletions samples/Plugins/WeaponsPlugin/Pistol.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Example.WeaponsPlugin;

internal class Pistol : IWeapon
{
public string Id => "pistol";
public string Name => "Pistol";
public int Slot => 1;
}
12 changes: 12 additions & 0 deletions samples/Plugins/WeaponsPlugin/WeaponsPlugin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[assembly: Plugin(typeof(WeaponsPlugin))]

namespace Example.WeaponsPlugin;

internal class WeaponsPlugin : IPlugin
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IWeapon, Pistol>();
services.AddSingleton<IWeapon, Ak47>();
}
}
46 changes: 46 additions & 0 deletions src/Attributes/DependsOnAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System;

namespace CPlugin.Net;

/// <summary>
/// Specifies that the current plugin depends on another plugin.
/// </summary>
/// <remarks>
/// Example:
/// <para>
/// If <c>GunGame</c> depends on <c>Weapons</c>, add this line before the namespace declaration:
/// </para>
/// <c>[assembly: DependsOn("Weapons")]</c>
/// <para>
/// During plugin loading, the specified plugin must also be present in the plugin
/// configuration; otherwise, a <c>PluginDependencyException</c> is thrown.
/// </para>
/// </remarks>
[AttributeUsage(AttributeTargets.Assembly, Inherited = false, AllowMultiple = true)]
public class DependsOnAttribute : Attribute
{
/// <summary>
/// Gets the name of the required plugin.
/// </summary>
public string PluginName { get; }

/// <summary>
/// Initializes a new instance of the <see cref="DependsOnAttribute"/> class.
/// </summary>
/// <param name="pluginName">
/// The name of the plugin that the current plugin depends on.
/// The <c>.dll</c> extension is optional.
/// </param>
/// <exception cref="ArgumentNullException">
/// <c>pluginName</c> is <c>null</c>.
/// </exception>
public DependsOnAttribute(string pluginName)
{
if (pluginName is null)
{
throw new ArgumentNullException(nameof(pluginName));
}

PluginName = pluginName;
}
}
4 changes: 4 additions & 0 deletions src/Core/Configuration/CPluginConfigurationBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
19 changes: 19 additions & 0 deletions src/Core/Exceptions/PluginDependencyException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;

namespace CPlugin.Net;

/// <summary>
/// The exception that is thrown when a plugin dependency cannot be resolved.
/// </summary>
public class PluginDependencyException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="PluginDependencyException"/> class.
/// </summary>
/// <param name="message">
/// The message that describes the error.
/// </param>
public PluginDependencyException(string message) : base(message)
{
}
}
38 changes: 32 additions & 6 deletions src/Core/PluginLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,51 @@ public static class PluginLoader
/// <exception cref="ArgumentNullException">
/// <c>configuration</c> is <c>null</c>.
/// </exception>
/// <exception cref="PluginDependencyException">
/// A plugin declares a dependency that cannot be resolved.
/// </exception>
public static void Load(CPluginConfigurationBase configuration)
{
ArgumentNullException.ThrowIfNull(configuration);
var assemblyFiles = configuration.GetPluginFiles();
foreach (string assemblyFile in assemblyFiles)

string[] pluginFiles = [.. configuration.GetPluginFiles()];
HashSet<string> 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<DependsOnAttribute>()];

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)
Expand Down
9 changes: 9 additions & 0 deletions src/Core/PluginNameNormalizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace CPlugin.Net;

internal class PluginNameNormalizer
{
public static string Normalize(string pluginName)
=> pluginName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)
? pluginName
: $"{pluginName}.dll";
}
Loading
Loading