Instruction: "How to create plugin project"


Для розробки знадобиться IDE з підтримкою написання коду на мові програмування C#. Для написання даної інструкції використовувалась VisualStudio.

Також необхідна наявність dotnet CLI

Якщо Ви використовували вже плагіни платформи, інструкція щодо створення плагінів на базі iBackendPlugin доступна за посиланням

Implementation

Платформа MEF.DEV підтримує архітектурний стиль розробки REST, де Ви можете створювати методи задля підтримки Ваших вимог - важливо тільки дотримуватись мінімальних правил. Реалізація методу повинна бути позначена атрибутами які є підкласом HttpMethodAttribute та опційно атрибутом RouteAttribute у разі якщо методів одного типу є декілька.

Важливо розуміти, що платформа надає спрощений доступ до частини HttpContext через надання можливості реалізації загально відомих свойств, а саме HttpRequest та HttpResponse, які надають доступу до headers, query параметрів та інше. Також імплементуючи властивість IApiContext ми отримуємо доступ до контексту платформи, отримуючи інформацію щодо конфігурації плагіна, користувача, тенанта та інших доступних у тенанті сервісів (відправка повідомлень, тощо)

Для того щоб платформа MEF.DEV знала який клас має використовуватись для десералізації запитів використовується атрибут ExportAttribute, який позначає клас як експортований (для прикладу restresource нижче).

Важливо зазначити, що у процесі реєстрації плагіна у платформі використовується значення назви проєкту, а самє Assembly name (для прикладу TestPlugin нижче).

Плагін можна створювати використовуючи цю інструкцію, або скориставшись уже готовим прикладом за посилання backend-template.


Project Creation

Для початку, потрібно створити новий проект. Вибираємо Create a new project

Етап 1

Також, вибрати Class library та мова програмування С#

Етап 2

Потрібно налаштувати конфігурацію нового проекту:

  • Вказати Project Name
  • Шлях, куди буде збережено Location
  • Та поставити галочку у Checkbox для того, щоб зберігати solution у тій же папці що і проект
Етап 3

У додатковій інформації, рекомендовано вибрати версію .NET(Long Term Support)

Етап 4

Creating the Plugin Class

Додавання Class

Реалізація плагіну може включати декілька класів, які експортуються, тим самим надаючи окремі ендпоінти шодо зовнішніх http запитів, для прикладу нижче RestResource

namespace TestPlugin;
public class RestResource
{

}

Додавання NuGet залежності та атрибуту Export.

Шукаємо розширення MEF.DEV.Common.Plugin та інсталюємо.

Етап 5
using System.Composition;
using UCP.Common.Plugin;

namespace TestPlugin;

[Export("restresource", typeof(IControllerPlugin))]
public class RestResource

Імплементація членів інтерфейсу

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

public class RestResource : IControllerPlugin
{
    private HttpRequest _request;
    private HttpResponse _response;
    private IApiContext _apiContext;

    public HttpRequest Request
    {
        get => _request;
        set => _request = value;
    }
    public HttpResponse Response
    {
        get => _response;
        set => _response = value;
    }
    public IApiContext ApiContext
    {
        get => _apiContext;
        set => _apiContext = value;
    }
}

Створення методу create-item

Важливо звернути увагу, що реалізація моделі запиту та відповіді можє бути довільна

[HttpGet, Route("{id}")]
public DataResponseModel GetItem([FromRoute] long id)
{
    // How to get service configuration
    var configProvider = _apiContext.ServiceProvider().GetService<IControllerPluginConfigProvider>();
    var configuration = configProvider!.Get<IConfigurationRoot>();

    return new DataResponseModel
    {
        Id = id,
        Name = configuration?.GetSection("myurl").Value,
        IsComplete = true
    };
}

[HttpPost, Route("create-item")]
public object CreateItem(
    [FromHeader] string lastModified,
    [FromQuery] string lang,
    [FromBody] DataRequestModel model
    )
{
    return new List<DataResponseModel>
    {
        new()
        {
            Name = model.Name,
            LastModified = lastModified,
            Lang = lang
        }
    };
}

public class DataRequestModel
{
    public long Id
    {
        get; set;
    }
    public string Name
    {
        get; set;
    }
}

public class DataResponseModel
{
    public long Id
    {
        get; set;
    }
    public string Name
    {
        get; set;
    }
    public bool IsComplete
    {
        get; set;
    }
    public string LastModified
    {
        get; set;
    }
    public string Lang 
    {
        get; set; 
    }
}

Populating the Swagger Specification

Цей етап є необов'язковим, але він потрібен для генерацію опису документації за допомогою Swagger

Активація Documentation file чекбокс

Втановіть галочку у чекбокс , щоб згенерувати файл, який міститиме API документацію

Етап 6

Або за допомогою коду:

<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

Заповнення основної інформації плагіну

Етап 7

Або ж за допомогою коду:

<PropertyGroup>
    <Title>Todo title</Title>
    <Version>1.0.0.1</Version>
    <Company>Author</Company>
    <Product>Todo API</Product>
    <Description>Todo description</Description>
</PropertyGroup>

Заповнення інформації щодо методів

Щоб зв'язати моделі із відповідями методів додайте атрибути «Consumes» та «Produces»

[ProducesResponseType(typeof(List<DataRequestModel>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(DataResponseModel), StatusCodes.Status500InternalServerError)]
// [Consumes("application/json")] // http REQUEST content-type, application/json by default
// [Produces("application/json")] // http RESPONSE content-type, application/json by default

Додавання опису метода та прикладів використовуючи XML коментарі

Документація, яка відображатиметься при генерації

/// <summary>
///     Create Todo item short description
/// </summary>
/// <remarks>
///     Create Todo item long description
/// </remarks>
/// <param name="id"></param>
/// <param name='model'
///     examples='{
///         "UNKNOWN_CONTEXT": {
///             "summary": "Unknown Service Context",
///             "description": "The request failed completely due to an unknown service context value",
///             "value": {
///                "cause": "CHARGING_FAILED",
///                "title": "Incomplete or erroneous session or subscriber information",
///                "invalidParams": [
///                     {
///                         "param": "/serviceRating/0/serviceContextId",
///                         "reason": "unknown context"
///                     }
///                 ]
///             }
///         },
///         "UNKNOWN_RESPONSE_CODE": {
///             "summary": "Unknown Response Code",
///             "description": "Internal Error",
///             "value": "405"
///         }
///     }'
/// >
///     DataRequestModel model for Create item
/// </param>
/// <param name="lastModified"></param>
/// <param name="lang"></param>
/// <response code='200'
///     example='{
///         "id": 1,
///         "name": "walk dog",
///         "isComplete": true
///     }'
/// >
///     Success
/// </response>
/// <response code='500' 
///     examples='{
///         "UNKNOWN_CONTEXT": {
///             "summary": "Unknown Service Context",
///             "description": "The request failed completely due to an unknown service context value",
///             "value": {
///                "cause": "CHARGING_FAILED",
///                "title": "Incomplete or erroneous session or subscriber information",
///                "invalidParams": [
///                     {
///                         "param": "/serviceRating/0/serviceContextId",
///                         "reason": "unknown context"
///                     }
///                 ]
///             }
///         },
///         "UNKNOWN_RESPONSE_CODE": {
///             "summary": "Unknown Response Code",
///             "description": "Internal Error",
///             "value": "405"
///         }
///     }'
///     headers='{
///         "Last-Modified":{
///             "description": "",
///             "schema": {
///                 "type": "string",
///                 "example": "2019-06-09T15:56:13.8498013Z"
///             }
///         }
///     }'
/// >
///     Error
/// </response>

Plugin Configuration

Цей етап є необов'язковим, але він потрібен для отримання дінамичної (в залежності від тенанту) конфігурації плагіна - Ви маєте реалізувати клас IPluginConfig, наприклад:

using System.Composition;
using UCP.Common.Plugin.Models.Config;

namespace TestPlugin;
[Export("config", typeof(IPluginConfig))]
public class ConfigPlugin : IPluginConfig
{
    public static class Keys
    {
        internal static string Connection = "Connection";
        internal static string UiParameters = "UiParameters";
        internal static string Logic = "Logic";
        internal static string Report = "Report";
    }

private readonly Dictionary<string, IEnumerable<PluginConfigSetting>> _configDictionary = new()
    {
        { Keys.Connection, GetConnectionSection() }
    };

public IDictionary<string, IEnumerable<PluginConfigSetting>> Get()
    {
        return _configDictionary;
    }

public IDictionary<string, IEnumerable<PluginConfigSetting>> Set(IDictionary<string, IEnumerable<PluginConfigSetting>> config)
    {
        throw new NotImplementedException();
    }

private static IEnumerable<PluginConfigSetting> GetConnectionSection()
    {
        yield return new PluginConfigSetting()
        {
            SettingType = PluginConfigSettingType.LongText,
                Name = "ExampleName",
                Value = @"{
            ""ConnectionStrings"": {
            ""ConnectionString"": ""Server=sqlserver;Database=database;User ID=userid;Password=password;Trusted_Connection=No"",
        },
            ""DebugLevel"" : ""Trace""}"
    };
    yield break;
}
}

Package Build

Робимо portable збірку та публікацію пакету із ключем self-contained як показано на скріншоті нижче або використовуючи приклад команди нижче:

publish package
dotnet publish -c Release -r portable -p:PublishDir=bin\Publish\net6.0 --no-self-contained

Package Registration

Переходимо на сторінку створення плагіну (якщо це первічне створення плагіну) або переходимо до наступного пункту.

Cторінкa створення Етап 8

Вона знаходиться в пункті меню Плагіни. Після чого, на сторінку створення плагінів.

Аліас - назва предметної області плагіну. Ім'я - назва плагіну. Вводимо ці дані, для нашого прикладу це значення test для аліасу та portal-test у якості назви. Далі переходимо до вибору типу. У платформі існує 4 основних типи плагінів. Про відмінності між ними написано у блоці допомоги. Зараз нас цікавить тип Service- плагін, що містить лише API складову, без користувацької конфігурації. Вибираємо його.

Після вибору у нас активувався Backend блок. Він містить лише одне поле PluginMefName. Це є назва нашого проекту. Вводимо назву з репозиторію, у цьому прикладі це TestPlugin, після чого нажимаємо кнопку Зберегти.

Uploading Package Version

Для завантаження готового ZIP-архіву плагіну на платформу mef.dev technical preview, необхідно, на сторінці конфігерації в блоці Backend натиснути кнопку Завантажити нову версію.

Cторінкa створення Етап 9

Після завантаження, в дропдауні необхідно вибрати необхідну версію та натиснути кнопку Зберегти

Також, альтернативою є завантаження плагіна за допомогою API-методу publish, який надає платформа:

curl --location 'https://preview.mef.dev/api/v2/plugins/<alias>/<PluginMefName>/publish' \
--header 'Authorization: Basic userpass' \
--form 'file=@"/local-path/to/file"' \
--form 'updateVersion="true"' \
--form 'updateTenantVersion="true"'

Важливо зауважити, що якщо ви використовуєте метод API publish, Вам потрібно додати обов’язковий файл metadata.json із прикладом вмісту нижче:

{
   "name":"TestPlugin Name",
   "serviceType":"API",
   "description":"This TestPlugin is used for tutorial goals under MIT license.",
   "dependencies":[],
   "config":{
      "routesUI":[]
   },
   "externalUrl":"https://opensource.org/license/mit",
   "configuration":null
}

Існує кілька додаткових файлів для цього способу публікації, які відповідають вимогам маркетплейсу платформи (https://preview.mef.dev/store), а саме:

  • description.html зрозумілий опис вашого плагіна
  • small.png зображення вашого плагіна у візуалізації залежностей
  • standard.png квадратне зображення вашого плагіна на маркетплейсі платформи

Ці файли потрібно додати до проекту з додатковою властивістю copy to output directory = copy if never

copy_if_never

Package Dry run

Провіряти роботу API плагінів можна любою програмою-сніфером. В данному випадку використовуватиметься Postman.

Для відправлення запитів потрібно авторизуватися – зазвичай використовується схема авторизації із токеном користувача в платформі, алє для тестування ми застосуємо Basic Auth. Необхідну пару логін-пароль доступу до API можна створити в розділі SETTINGS \ CREDENTIALS свого профілю, куди можна потрапити натиснувши на іконку користувача у верхньому правому кутку і вибравши пункт меню SETTINGS. Після натискання на кнопку ADD Ви зможете задати логін користувача і пароль для авторизації Basic Auth

Basic Health Check

В межах платформи існує endpoint для перевірки працездатності плагіна:

https://preview.mef.dev/api/v2/<alias>/plugins/<PluginMefName>/version.json?detaillevel=detailed
detaillevel=detailed Етап 10

У випадку схожого результату, важ плагін успішно завантажений на платформу, та готовий до роботи.

Sending Requests to the Plugin

Надсилання запитів до плагіна продемонструємо на прикладі GET запитів.

Для надсилання запитів використовується шаблон:

https://preview.mef.dev/api/v2/<alias>/<Export Name>

При цьому в тіло запиту можна добавляти будь-які параметри\хедери\поля, проте варто відобразити їх у вхідній моделі плагіна. Приклад запиту

curl --location --request GET 'https://preview.mef.dev/api/v2/test/restresource/1' \
--header 'authorization: Basic userpass' \
--header 'Content-Type: application/json'

curl --location --request POST 'https://preview.mef.dev/api/v2/test/restresource/create-item' \
--header 'authorization: Basic userpass' \
--header 'Content-Type: application/json' \
--data '{
    "name": "walk dog"
}'
detaillevel=detailed Етап 11

Useful Links:

Посилання на GitHub репозиторій із backend-plugin-example: https://github.com/mef-dev/tutorial-backend-plugin

Реалізацію використання бекенд плагіну із Front-плагіна можна побачити в Ангуляр-аплікаці tutorial-ui-plugin.