Добавил систему плагинов
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
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SfeduSchedule", "SfeduSchedule\SfeduSchedule.csproj", "{57B088A7-D7E2-4B5D-82A4-A3070A78A3E4}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SfeduSchedule", "SfeduSchedule\SfeduSchedule.csproj", "{57B088A7-D7E2-4B5D-82A4-A3070A78A3E4}"
|
||||||
EndProject
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{57B088A7-D7E2-4B5D-82A4-A3070A78A3E4}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
EndGlobal
|
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 SfeduSchedule.Services;
|
||||||
using X.Extensions.Logging.Telegram.Extensions;
|
using X.Extensions.Logging.Telegram.Extensions;
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -26,7 +27,7 @@ if (!Directory.Exists(dataDirectory))
|
|||||||
}
|
}
|
||||||
|
|
||||||
GlobalVariables.JwtFilePath = Path.Combine(dataDirectory, "jwt.txt");
|
GlobalVariables.JwtFilePath = Path.Combine(dataDirectory, "jwt.txt");
|
||||||
|
var pluginsPath = Path.Combine(dataDirectory, "Plugins");
|
||||||
|
|
||||||
builder.Logging.ClearProviders();
|
builder.Logging.ClearProviders();
|
||||||
builder.Logging.AddConsole();
|
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.AddHttpClient<ModeusService>();
|
||||||
|
|
||||||
builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration);
|
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");
|
var jobKey = new JobKey("UpdateJWTJob");
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(preinstalledJwtToken))
|
if (string.IsNullOrEmpty(preinstalledJwtToken))
|
||||||
@@ -175,6 +189,13 @@ app.MapGet("/", async context =>
|
|||||||
|
|
||||||
app.MapControllers();
|
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.UseRateLimiter();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
@@ -18,4 +18,8 @@
|
|||||||
<PackageReference Include="X.Extensions.Logging.Telegram" Version="2.0.2" />
|
<PackageReference Include="X.Extensions.Logging.Telegram" Version="2.0.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\SfeduSchedule.Plugin.Abstractions\SfeduSchedule.Plugin.Abstractions.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
Reference in New Issue
Block a user