Добавил систему плагинов
Some checks failed
Create and publish a Docker image / Publish image (push) Failing after 4m3s

This commit is contained in:
2025-09-07 15:47:25 +03:00
parent f15bf4dfe6
commit fd1c033460
8 changed files with 192 additions and 2 deletions

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

View File

@@ -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>

View 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" });
}

View File

@@ -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>

View File

@@ -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

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

View File

@@ -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();

View File

@@ -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>