Table of Contents

Создание плагина для веб-сервера

Для веб-сервера ПОЛИНОМ:MDM предусмотрена возможность создания плагинов. Плагин расширяет WEB API собственными методами, регистрирует свои сервисы и использует ядро ПОЛИНОМ:MDM через сессию Polynom.Api.ISession.

Общая информация

Плагин представляет собой отдельную сборку с суффиксом .Plugin.dll (тип «библиотека классов»). При старте хост сканирует каталог модулей веб-сервера и каталог расширений {CommonAppData}/ASCON/Polynom/WebServer/Plugins, находит сборки по маске *.Plugin.dll и подключает их автоматически.

При подключении плагина хост:

  1. загружает сборку и находит класс, реализующий IPluginServiceRegistrar;
  2. создаёт его через Activator.CreateInstance и вызывает RegisterServices(IServiceCollection);
  3. добавляет сборку в маршрутизацию MVC — методы плагина становятся доступны по адресам /api/v1/{controller}/{method};
  4. подключает XML-документацию *.Plugin.xml к Swagger.

Плагин подключается к SDK одной ссылкой на метапакет Ascon.Polynom.Web.Api.Plugins. Метапакет агрегирует транзитивные пакеты Plugins.Contracts, Plugins.Core, Plugins.AspNetCore и Plugins.Kernel.

Подключение SDK

Добавьте в файл проекта одну ссылку на метапакет:

<ItemGroup>
  <PackageReference Include="Ascon.Polynom.Web.Api.Plugins" Version="24.1.*" />
</ItemGroup>

Пакет публикуется во внутренний NuGet-фид. В репозитории SDK также поставляется локальный фид SDK/Web/Packages/dotnet. Пример конфигурации nuget.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <!-- Подключите корпоративный NuGet-фид с пакетами SDK. -->
    <add key="sdk-local-feed" value="../../Packages/dotnet" />
  </packageSources>
</configuration>

Прикладные зависимости (FluentValidation, FluentValidation.DependencyInjectionExtensions, Asp.Versioning.Mvc) плагин объявляет самостоятельно.

Структура проекта

Ascon.Polynom.{Feature}.Plugin/
├── Ascon.Polynom.{Feature}.Plugin.csproj
├── ServiceRegistrar.cs                     // IPluginServiceRegistrar
├── Controllers/                            // контроллеры : WebApiPluginController
├── Interfaces/
│   ├── Requests/                           // I{Action}{Entity}Request
│   └── Responses/                          // I{Action}{Entity}Response
├── Models/
│   ├── Requests/                           // {Action}{Entity}Request
│   └── Responses/                          // {Action}{Entity}Response
├── Services/                               // бизнес-логика плагина
└── Validation/
    └── Requests/                           // FluentValidation-валидаторы

Обязательно. Имя сборки (AssemblyName) должно оканчиваться на .Plugin, иначе хост не обнаружит плагин. XML-документацию включают через DocumentationFile — она попадает в Swagger.

Файл проекта

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Library</OutputType>
    </PropertyGroup>

    <PropertyGroup>
        <RootNamespace>Ascon.Polynom.Feature</RootNamespace>
        <AssemblyName>Ascon.Polynom.Feature.Plugin</AssemblyName>
        <DocumentationFile>$(AssemblyName).xml</DocumentationFile>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Ascon.Polynom.Web.Api.Plugins" Version="24.1.*" />
        <PackageReference Include="Asp.Versioning.Mvc" Version="8.*" />
        <PackageReference Include="FluentValidation" Version="12.*" />
        <PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.*" />
    </ItemGroup>

</Project>

Контракты запроса и ответа

Для каждого метода объявляются интерфейс и класс. В интерфейсе задаётся полиморфизм System.Text.Json, иначе десериализация контрактов не работает.

Интерфейс запроса:

using System.Text.Json.Serialization;

using Ascon.Polynom.Web.Api.Plugins.Contracts.Interfaces.Requests.Base;

using Ascon.Polynom.Feature.Models.Requests;

namespace Ascon.Polynom.Feature.Interfaces.Requests;

[JsonPolymorphic(
    UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType),
 JsonDerivedType(typeof(GetStatisticsRequest))]
public interface IGetStatisticsRequest : IWebApiRequest
{
    string? ReferenceName { get; set; }
}

Класс запроса наследует WebApiRequest:

using System.Text.Json.Serialization;

using Ascon.Polynom.Web.Api.Plugins.Contracts.Requests.Base;

namespace Ascon.Polynom.Feature.Models.Requests;

[JsonUnmappedMemberHandling(JsonUnmappedMemberHandling.Disallow)]
public class GetStatisticsRequest : WebApiRequest, IGetStatisticsRequest
{
    public string? ReferenceName { get; set; }
}

Интерфейс ответа описывает возвращаемые данные, конкретный класс их реализует. Имена типов строятся по шаблону {Action}{Entity}Request/{Action}{Entity}Response.

Контроллер

Контроллер наследует WebApiPluginController (Ascon.Polynom.Web.Api.Plugins.AspNetCore). Базовый класс предоставляет маршрут api/v{version:apiVersion}/[controller] и [ApiController] — дублировать их не нужно. Атрибуты [Authorize], [ApiVersion("1")] и [RequestValidation] контроллер задаёт явно: плагинный базовый класс их не наследует.

using Ascon.Polynom.Web.Api.AspNetCore;          // [RequestValidation] (поставляет SDK)
using Ascon.Polynom.Web.Api.Plugins.AspNetCore;  // WebApiPluginController
using Ascon.Polynom.Web.Api.Plugins.Core;        // IPluginAuthorizationService
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Ascon.Polynom.Feature.Controllers;

[ApiVersion("1")]
[Authorize]
[RequestValidation]
public class SampleController : WebApiPluginController
{
    private readonly StatisticsService _statisticsService;

    public SampleController(
        IPluginAuthorizationService authorizationService,
        StatisticsService statisticsService,
        IServiceProvider services) : base(authorizationService, services)
    {
        _statisticsService = statisticsService;
    }

    [HttpPost(nameof(GetStatistics))]
    public async Task<ActionResult<IGetStatisticsResponse>> GetStatistics(
        IGetStatisticsRequest request,
        CancellationToken cancellationToken)
    {
        CheckAuthorization();

        var session = await GetPolynomApiSession(cancellationToken);
        if (session is null)
            return Unauthorized();

        var result = await _statisticsService.GetStatisticsAsync(session, request, cancellationToken);

        return new JsonResult(result);
    }
}

Имя действия задаётся в атрибуте [HttpPost(nameof(Метод))] — используется Action-Based JSON RPC: действие совпадает с именем метода.

Сервис и работа с сессией

Бизнес-логика выносится в сервис. Сервис получает открытую сессию Polynom.Api.ISession параметром и не создаёт её самостоятельно.

using Ascon.Polynom.Api;

namespace Ascon.Polynom.Feature.Services;

public class StatisticsService
{
    public Task<IGetStatisticsResponse> GetStatisticsAsync(
        ISession session,
        IGetStatisticsRequest request,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(session);

        // session.Objects.AllReferences, session.Objects.RootConcepts и т. д.
        throw new NotImplementedException();
    }
}

Регистрация сервисов

DI-регистрация выполняется классом, реализующим IPluginServiceRegistrar. Хост создаёт его через Activator.CreateInstance, поэтому у класса должен быть конструктор без параметров.

using Ascon.Polynom.Web.Api.Plugins.Core;
using FluentValidation;
using Microsoft.Extensions.DependencyInjection;

namespace Ascon.Polynom.Feature;

public class ServiceRegistrar : IPluginServiceRegistrar
{
    public void RegisterServices(IServiceCollection services)
    {
        services.AddValidatorsFromAssembly(typeof(ServiceRegistrar).Assembly);
        services.AddScoped<StatisticsService>();
    }
}

Валидация

Валидаторы FluentValidation наследуют AbstractValidator<I{Action}{Entity}Request> и подключаются автоматически через AddValidatorsFromAssembly. Валидация выполняется, если на контроллере задан атрибут [RequestValidation].

using FluentValidation;

namespace Ascon.Polynom.Feature.Validation.Requests;

public class GetStatisticsRequestValidator : AbstractValidator<IGetStatisticsRequest>
{
    public GetStatisticsRequestValidator()
    {
        RuleFor(x => x.ReferenceName).MaximumLength(200);
    }
}

Авторизация

Доступ к методам ограничивается атрибутом [Authorize]. В начале каждого метода вызывается CheckAuthorization(). Сессия ядра для текущего пользователя возвращается методом GetPolynomApiSession(cancellationToken); если сессия не получена, метод возвращает ответ 401 (или бросает UnauthorizedException, если подключена библиотека Polynom.Web.Exceptions).

Сборка и подключение

  1. Соберите проект:

    dotnet build -c Release
    
  2. Разместите полученные файлы Ascon.Polynom.{Feature}.Plugin.dll и Ascon.Polynom.{Feature}.Plugin.xml в каталоге плагинов хоста (каталог модулей веб-сервера или {CommonAppData}/ASCON/Polynom/WebServer/Plugins).

  3. Перезапустите веб-сервер — плагин подключится автоматически, его методы появятся в Swagger.

Пример из SDK

Полный разобранный пример плагина приведён в статье Разработка плагинов для веб-сервера (пример). Исходный код примера расположен по пути SDK/Web/Samples/Server/Samples.WebApi.Plugin.

Возможные ошибки

Симптом Причина Решение
Плагин не обнаружен имя сборки без суффикса .Plugin задайте AssemblyName с суффиксом .Plugin
ServiceRegistrar не создан конструктор с параметрами сделайте конструктор без параметров
Метод не появился в Swagger нет файла *.Plugin.xml рядом с DLL включите DocumentationFile и разместите XML рядом со сборкой
Ошибка десериализации запроса у контракта нет [JsonDerivedType] добавьте полиморфизм System.Text.Json на интерфейс
Валидаторы не срабатывают нет [RequestValidation] или AddValidatorsFromAssembly добавьте атрибут и регистрацию валидаторов
401 при вызове метода не вызван CheckAuthorization() или пустая сессия приведите контроллер к шаблону выше
Ошибка компиляции при подключении SDK ссылка на внутренний проект вместо пакета подключите метапакет Ascon.Polynom.Web.Api.Plugins

См. также