Добавил систему плагинов
Some checks failed
Create and publish a Docker image / Publish image (push) Failing after 4m3s
Some checks failed
Create and publish a Docker image / Publish image (push) Failing after 4m3s
This commit is contained in:
13
SfeduSchedule.Plugin.Abstractions/IPlugin.cs
Normal file
13
SfeduSchedule.Plugin.Abstractions/IPlugin.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace SfeduSchedule.Plugin.Abstractions;
|
||||
|
||||
// Базовый контракт плагина (общий для хоста и плагинов)
|
||||
public interface IPlugin
|
||||
{
|
||||
string Name { get; }
|
||||
|
||||
// Регистрация сервисов плагина в DI (выполняется до Build())
|
||||
void ConfigureServices(IServiceCollection services);
|
||||
|
||||
// Регистрация маршрутов (Minimal API) плагина после Build()
|
||||
void MapEndpoints(IEndpointRouteBuilder endpoints);
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<OutputType>Library</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
45
SfeduSchedule.Plugin.Sample/Plugin.cs
Normal file
45
SfeduSchedule.Plugin.Sample/Plugin.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SfeduSchedule.Plugin.Abstractions;
|
||||
|
||||
namespace SfeduSchedule.Plugin.Sample;
|
||||
|
||||
// Пример сервиса плагина
|
||||
public interface IGreeter
|
||||
{
|
||||
string Greet(string name);
|
||||
}
|
||||
|
||||
public sealed class Greeter : IGreeter
|
||||
{
|
||||
public string Greet(string name) => $"Hello, {name} from {nameof(Plugin)}!";
|
||||
}
|
||||
|
||||
public sealed class SamplePlugin : IPlugin
|
||||
{
|
||||
public string Name => "Sample";
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
// Регистрируем DI-сервисы плагина
|
||||
services.AddScoped<IGreeter, Greeter>();
|
||||
// Можно регистрировать любые IHostedService, Options и т.д.
|
||||
}
|
||||
|
||||
public void MapEndpoints(IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
// Пример Minimal API эндпоинта
|
||||
endpoints.MapGet("/plugins/sample/hello", (IGreeter greeter) =>
|
||||
{
|
||||
return Results.Ok(new { message = greeter.Greet("world") });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Пример MVC-контроллера из плагина
|
||||
[ApiController]
|
||||
[Route("plugins/sample/[controller]")]
|
||||
public class EchoController : ControllerBase
|
||||
{
|
||||
[HttpGet("{text}")]
|
||||
public IActionResult Get(string text) => Ok(new { echo = text, from = "Plugin.Sample" });
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>SfeduSchedule.Plugin.Sample.plugin</AssemblyName>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<!-- Кладём зависимости плагина рядом со сборкой, чтобы загрузчик их нашёл -->
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<Nullable>enable</Nullable>
|
||||
<OutputType>Library</OutputType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Даёт доступ к Microsoft.AspNetCore.* (ControllerBase и т.п.) -->
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App"/>
|
||||
<ProjectReference Include="..\SfeduSchedule.Plugin.Abstractions\SfeduSchedule.Plugin.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
@@ -2,6 +2,10 @@
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SfeduSchedule", "SfeduSchedule\SfeduSchedule.csproj", "{57B088A7-D7E2-4B5D-82A4-A3070A78A3E4}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SfeduSchedule.Plugin.Abstractions", "SfeduSchedule.Plugin.Abstractions\SfeduSchedule.Plugin.Abstractions.csproj", "{B2E8DAD7-7373-4155-B230-4E53DFC04445}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SfeduSchedule.Plugin.Sample", "SfeduSchedule.Plugin.Sample\SfeduSchedule.Plugin.Sample.csproj", "{B2B6D730-46AE-40ED-815F-81176FB4E545}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -12,5 +16,13 @@ Global
|
||||
{57B088A7-D7E2-4B5D-82A4-A3070A78A3E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{57B088A7-D7E2-4B5D-82A4-A3070A78A3E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{57B088A7-D7E2-4B5D-82A4-A3070A78A3E4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B2E8DAD7-7373-4155-B230-4E53DFC04445}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B2E8DAD7-7373-4155-B230-4E53DFC04445}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B2E8DAD7-7373-4155-B230-4E53DFC04445}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B2E8DAD7-7373-4155-B230-4E53DFC04445}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B2B6D730-46AE-40ED-815F-81176FB4E545}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B2B6D730-46AE-40ED-815F-81176FB4E545}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B2B6D730-46AE-40ED-815F-81176FB4E545}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B2B6D730-46AE-40ED-815F-81176FB4E545}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
64
SfeduSchedule/PluginLoader.cs
Normal file
64
SfeduSchedule/PluginLoader.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using SfeduSchedule.Plugin.Abstractions;
|
||||
|
||||
public sealed record LoadedPlugin(IPlugin Instance, Assembly Assembly, PluginLoadContext Context);
|
||||
|
||||
public static class PluginLoader
|
||||
{
|
||||
// Загружаем все сборки *.plugin.dll из папки плагинов
|
||||
public static IReadOnlyList<LoadedPlugin> LoadPlugins(string pluginsDir)
|
||||
{
|
||||
var result = new List<LoadedPlugin>();
|
||||
|
||||
if (!Directory.Exists(pluginsDir))
|
||||
return result;
|
||||
|
||||
foreach (var file in Directory.EnumerateFiles(pluginsDir, "*.plugin.dll", SearchOption.AllDirectories))
|
||||
{
|
||||
var path = Path.GetFullPath(file);
|
||||
var alc = new PluginLoadContext(path);
|
||||
var asm = alc.LoadFromAssemblyPath(path);
|
||||
|
||||
// Ищем реализацию IPlugin
|
||||
var pluginType = asm
|
||||
.GetTypes()
|
||||
.FirstOrDefault(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface);
|
||||
|
||||
if (pluginType is null)
|
||||
continue;
|
||||
|
||||
var instance = (IPlugin)Activator.CreateInstance(pluginType)!;
|
||||
result.Add(new LoadedPlugin(instance, asm, alc));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Отдельный контекст загрузки для изоляции зависимостей плагина
|
||||
public sealed class PluginLoadContext : AssemblyLoadContext
|
||||
{
|
||||
private readonly AssemblyDependencyResolver _resolver;
|
||||
|
||||
public PluginLoadContext(string pluginMainAssemblyPath)
|
||||
: base(isCollectible: true)
|
||||
{
|
||||
_resolver = new AssemblyDependencyResolver(pluginMainAssemblyPath);
|
||||
}
|
||||
|
||||
// Разрешаем управляемые зависимости плагина из его папки.
|
||||
// Возвращаем null, чтобы отдать решение в Default ALC для общих сборок (например, Plugin.Abstractions).
|
||||
protected override Assembly? Load(AssemblyName assemblyName)
|
||||
{
|
||||
var path = _resolver.ResolveAssemblyToPath(assemblyName);
|
||||
return path is null ? null : LoadFromAssemblyPath(path);
|
||||
}
|
||||
|
||||
// Нативные зависимости (если есть)
|
||||
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
|
||||
{
|
||||
var path = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
|
||||
return path is null ? IntPtr.Zero : LoadUnmanagedDllFromPath(path);
|
||||
}
|
||||
}
|
@@ -6,6 +6,7 @@ using SfeduSchedule.Jobs;
|
||||
using SfeduSchedule.Services;
|
||||
using X.Extensions.Logging.Telegram.Extensions;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -26,7 +27,7 @@ if (!Directory.Exists(dataDirectory))
|
||||
}
|
||||
|
||||
GlobalVariables.JwtFilePath = Path.Combine(dataDirectory, "jwt.txt");
|
||||
|
||||
var pluginsPath = Path.Combine(dataDirectory, "Plugins");
|
||||
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Logging.AddConsole();
|
||||
@@ -45,11 +46,24 @@ if (!string.IsNullOrEmpty(tgChatId) && !string.IsNullOrEmpty(tgToken))
|
||||
};
|
||||
});
|
||||
|
||||
builder.Services.AddControllers();
|
||||
// Включаем MVC контроллеры
|
||||
var mvcBuilder = builder.Services.AddControllers();
|
||||
builder.Services.AddHttpClient<ModeusService>();
|
||||
|
||||
builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration);
|
||||
|
||||
// Загружаем плагины (изолированно), даём им зарегистрировать DI и подключаем контроллеры
|
||||
var loaded = PluginLoader.LoadPlugins(pluginsPath);
|
||||
foreach (var p in loaded)
|
||||
{
|
||||
// DI из плагина
|
||||
p.Instance.ConfigureServices(builder.Services);
|
||||
|
||||
// Подключаем контроллеры плагина
|
||||
mvcBuilder.PartManager.ApplicationParts.Add(new AssemblyPart(p.Assembly));
|
||||
}
|
||||
|
||||
|
||||
var jobKey = new JobKey("UpdateJWTJob");
|
||||
|
||||
if (string.IsNullOrEmpty(preinstalledJwtToken))
|
||||
@@ -175,6 +189,13 @@ app.MapGet("/", async context =>
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
// Маршруты Minimal API из плагинов
|
||||
foreach (var p in loaded)
|
||||
{
|
||||
logger.LogInformation("Mapping endpoints for plugin: {PluginName}", p.Instance.Name);
|
||||
p.Instance.MapEndpoints(app);
|
||||
}
|
||||
|
||||
app.UseRateLimiter();
|
||||
|
||||
app.Run();
|
@@ -18,4 +18,8 @@
|
||||
<PackageReference Include="X.Extensions.Logging.Telegram" Version="2.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SfeduSchedule.Plugin.Abstractions\SfeduSchedule.Plugin.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
Reference in New Issue
Block a user