SDK (Portal.WebPart.Sdk)
C# SDK для серверного рендеринга WebPart (hybrid SSR).
Настройка SDK: скачайте Extension SDK с korport.ru/developers (версия = версия Portal). Для разработки в монорепозитории Portal см.
backend/src/Portal.WebPart.Sdk/IWebPart.cs.
Подробное руководство с примерами UI, data-wp-action и работы с API: Интерфейс, контролы и API.
Интерфейс WebPart
Заголовок раздела «Интерфейс WebPart»using Portal.Contracts.Lists;using Portal.WebPart.Sdk;
public sealed class TasksWidgetWebPart : IWebPart{ public async Task<WebPartResult> RenderAsync(IWebPartContext ctx, CancellationToken cancellationToken = default) { var listId = ctx.Properties.GetGuid("listId");
// Фильтрованная выборка (push-down в SQL) — предпочтительный способ var items = await ctx.Lists.QueryItemsAsync(listId, ListItemQueryBuilder.ForList(listId) .WhereField("status", f => f.Eq("Новая")) .OrderBy("__created_at", desc: true) .Take(ctx.Properties.GetInt32("pageSize", 10)) .Build(), cancellationToken);
var html = "<ul>" + string.Join("", items.Select(i => $"<li>{i.GetProperty("id")}</li>")) + "</ul>"; return new WebPartResult(html); }
public Task<WebPartResult> HandleActionAsync(string action, JsonElement data, IWebPartContext ctx, CancellationToken ct = default) => Task.FromResult(new WebPartResult(""));}IWebPart / WebPartResult
Заголовок раздела «IWebPart / WebPartResult»| Член | Описание |
|---|---|
RenderAsync(ctx, ct) | Серверный рендер HTML |
HandleActionAsync(action, data, ctx, ct) | Обработка data-wp-action |
WebPartResult(Html, Error?) | Результат: HTML и опциональная ошибка |
Чтение data action — WebPartActionData
Заголовок раздела «Чтение data action — WebPartActionData»Не копируйте локальный GetDataString. В SDK:
var form = data.AsActionData();var title = form.GetString("title"); // wpTitle / title / titlevar ok = form.GetBoolean("isActive"); // checkbox "true"/"on"var id = form.GetGuid("itemId");var assignee = form.GetJson("assigneeJson"); // hidden JSON (person)Ключи wp*, snake_case и camelCase считаются одним именем. Подробнее: Интерфейс и API §2.1.
IWebPartContext
Заголовок раздела «IWebPartContext»| Свойство | Тип | Описание |
|---|---|---|
User | PortalUserInfo? | Текущий пользователь (или null для анонима) |
NodeId | Guid? | Узел текущей страницы/формы (то же, что Host.NodeId) |
Host | IWebPartHost | Контекст страницы или формы: список, элемент, файл |
Properties | IReadOnlyDictionary<string, JsonElement> | Свойства из Property Pane |
Lists | IWebPartListsApi | Списки и элементы |
Libraries | IWebPartLibrariesApi | Библиотеки и файлы |
Nodes | IWebPartNodesApi | Узлы |
Routes | IWebPartRoutesApi | Построение ссылок |
Groups | IWebPartGroupsApi | Группы доступа |
Users | IWebPartUsersApi | Профиль сотрудника и оргструктура |
Permissions | IWebPartPermissionsApi | Проверка и назначение ACL |
Kedo | IWebPartKedoApi | Отправка в КЭДО (нужен админ КЭДО) — инструкция |
Fields | IWebPartFieldsFormatter | Форматирование значений полей |
IWebPartHost (ctx.Host)
Заголовок раздела «IWebPartHost (ctx.Host)»Платформа передаёт host при render/action: на форме списка — listId, itemRef, formMode; на странице — nodeId / pageId. Ссылки сами по себе запросов не делают; Get* загружают данные с проверкой прав.
// Форма списка /items/12/editif (ctx.Host.IsListForm && ctx.Host.HasList){ var item = await ctx.Host.GetOrNewListItemAsync(ct); // edit → #12; new → пустой var title = item.GetString("title");}
// Только ссылкиvar listId = ctx.Host.ListId;var itemRef = ctx.Host.ItemRef; // null на созданииvar mode = ctx.Host.FormMode; // New / View / Edit
// Ленивая загрузкаvar node = await ctx.Host.GetNodeAsync(ct);var list = await ctx.Host.GetListAsync(ct);var rawItem = await ctx.Host.GetItemAsync(ct); // JsonElement?var portalItem = await ctx.Host.GetListItemAsync(ct); // null на newvar library = await ctx.Host.GetLibraryAsync(ct);var file = await ctx.Host.GetFileAsync(ct);| Свойство / метод | Описание |
|---|---|
Kind | None / Page / ListForm / LibraryFile |
NodeId / PageId | Узел и страница (на обычной странице) |
ListId / ItemRef | Список и элемент формы (ItemRef — номер или GUID-строка) |
LibraryId / FileRef | Библиотека и файл (если host передан) |
FormMode | New / View / Edit на форме списка |
HasList / HasItem / HasLibrary / HasFile | Есть ли соответствующая ссылка |
IsListForm / IsNewForm / IsViewForm / IsEditForm | Удобные флаги |
GetNodeAsync / GetListAsync / GetItemAsync | Сырой JSON или null |
GetListItemAsync | PortalListItem? (null на создании) |
GetOrNewListItemAsync | Элемент для формы: load или NewListItemAsync |
GetLibraryAsync / GetFileAsync | Библиотека / файл |
Формы create/edit: Кастомная форма.
PortalUserInfo
Заголовок раздела «PortalUserInfo»| Свойство | Тип | Описание |
|---|---|---|
Id | Guid | UUID пользователя |
Login | string | Логин |
DisplayName | string? | Отображаемое имя |
IsPortalAdmin | bool | Администратор портала |
GroupIds | IReadOnlyList<Guid> | Группы, в которых состоит пользователь |
Properties (WebPartPropertiesExtensions)
Заголовок раздела «Properties (WebPartPropertiesExtensions)»Справочник параметров и сценариев (список текущего / другого узла, dependsOn): Параметры WebPart. Методы чтения — §7.
| Метод | Описание |
|---|---|
GetString(key, defaultValue = "") | Строка (числа и bool приводятся к тексту) |
GetGuid(key) | GUID; Guid.Empty, если ключ отсутствует или невалиден |
GetInt32(key, defaultValue = 0) | Целое число |
GetBoolean(key, defaultValue = false) | Булево (true/false или строка) |
Lists (IWebPartListsApi)
Заголовок раздела «Lists (IWebPartListsApi)»| Метод | Описание |
|---|---|
GetListItemAsync / NewListItemAsync / QueryListItemsAsync / GetListItemsAsync | SharePoint-подобный PortalListItem: item["title"], CreateAsync / UpdateAsync (рекомендуется) |
GetAsync | Метаданные списка |
FindBySlugAsync(nodeId, slug) / FindByTitleAsync(nodeId, title) | Найти список → PortalListRef (Id, Slug, …) |
GetFieldsAsync | Схема полей |
GetItemsAsync / QueryItemsAsync | Выборка элементов (сырой JSON) |
GetItemAsync | Один элемент, сырой JSON (itemRef — номер или guid) |
GetItemVersionsAsync | История версий (versioningEnabled, versions[] со снимками fieldValues, fields[]) |
GetItemVersionAsync | Одна версия по номеру со снимком fieldValues |
CreateItemAsync / UpdateItemAsync | Низкоуровневое создание / обновление (fieldValues: UUID или internal_name) |
Libraries (IWebPartLibrariesApi)
Заголовок раздела «Libraries (IWebPartLibrariesApi)»| Метод | Описание |
|---|---|
GetAsync | Метаданные библиотеки |
FindBySlugAsync(nodeId, slug) / FindByTitleAsync(nodeId, title) | Найти библиотеку → PortalLibraryRef |
GetFilesAsync / QueryFilesAsync | Выборка файлов |
GetFileAsync(libraryId, fileRef) | Один файл/папка (номер или GUID) |
DownloadUrl(fileId) | URL скачивания файла |
Nodes (IWebPartNodesApi)
Заголовок раздела «Nodes (IWebPartNodesApi)»| Метод | Описание |
|---|---|
GetAsync | Узел |
GetChildrenAsync | Дочерние узлы |
FindBySlugAsync(slug, parentId?) | Узел по slug (parentId: null — корень) → PortalNodeRef |
Routes (IWebPartRoutesApi)
Заголовок раздела «Routes (IWebPartRoutesApi)»| Метод | Описание |
|---|---|
NodeHrefAsync(nodeId) | Ссылка на узел |
ListItemHrefAsync(nodeId, listId, itemNumber) | Ссылка на элемент списка |
ResolvePathAsync(pathOrUrl) | Разбор path/URL → PortalResolvedRoute (NodeId, ListId, ItemNumber, …) |
Groups (IWebPartGroupsApi)
Заголовок раздела «Groups (IWebPartGroupsApi)»| Метод | Описание |
|---|---|
GetAsync(groupId) | Группа → PortalGroupInfo (GroupType, IsFromDirectory) |
FindByNameAsync(name) | Группа по точному имени |
SearchAsync(query, limit?) | Поиск по подстроке |
IsMemberAsync(groupId, userId) | Состоит ли пользователь в группе |
GetMembersAsync(groupId) | Участники группы (WebPartGroupMember: Id, Login, DisplayName) |
Откуда брать id для прав и ACL: Права доступа из кода §1.
Permissions (IWebPartPermissionsApi)
Заголовок раздела «Permissions (IWebPartPermissionsApi)»Проверка эффективных прав текущего пользователя и назначение ACL. Типы: PortalPermission, PortalAclEntry, PermissionResource, PermissionLevel (Portal.Contracts.Permissions).
Гайд: Права доступа из кода. REST: API: права.
using Portal.Contracts.Permissions;
var node = await ctx.Permissions.ForCurrentNodeAsync(ct);if (!node.CanView) return new WebPartResult("", "Нет доступа");
var list = await ctx.Permissions.ForListAsync(listId, ct);var item = await ctx.Permissions.ForListItemAsync(listId, "12", ct); // номер или UUIDif (item.CanEdit) { /* … */ }
var editors = await ctx.Groups.FindByNameAsync("Редакторы HR", ct) ?? throw new InvalidOperationException("Группа не найдена");await ctx.Permissions.GrantToGroupAsync( PermissionResource.ListItem, "12", editors.Id, PermissionLevel.Edit, listId: listId, ct: ct);
var acl = await ctx.Permissions.ListAsync(PermissionResource.ListItem, "12", listId: listId, ct: ct);await ctx.Permissions.RevokeAsync(acl[0].Id, ct);| Метод | Описание |
|---|---|
ForCurrentNodeAsync / ForNodeAsync / ForListAsync / ForListItemAsync / ForLibraryAsync / ForFileAsync / ForPageAsync / ForSurveyAsync | Эффективные права → PortalPermission (CanView…CanDelete) |
CanAsync(resourceType, resourceId, required, …) | Достаточно ли уровня |
GrantToUserAsync / GrantToGroupAsync / GrantAsync | Upsert ACL |
ListAsync | Явные назначения на ресурс |
RevokeAsync(permissionId) | Удалить запись ACL |
Users (IWebPartUsersApi)
Заголовок раздела «Users (IWebPartUsersApi)»| Метод | Описание |
|---|---|
GetProfileAsync(userId) | Карточка: ФИО, email, card_fields (meta по настройке ad-sync), HasAvatar, ManagerUserId (UserPublicProfile) |
GetOrgChainAsync(userId) | Цепочка руководителей от корня до пользователя (UserOrgChain.Chain) |
GetOrgChildrenAsync(userId) | Прямые подчинённые для lazy-раскрытия (UserOrgChildren.Children) |
AvatarUrl(userId) | URL аватара: /api/v1/users/{id}/avatar |
var profile = await ctx.Users.GetProfileAsync(userId, ct);var avatar = profile.HasAvatar ? $"<img src=\"{ctx.Users.AvatarUrl(userId)}\" alt=\"\" />" : "";var chain = await ctx.Users.GetOrgChainAsync(userId, ct);Типы — Portal.Contracts.Users: UserPublicProfile, UserOrgNode, UserOrgChain, UserOrgChildren.
Fields (IWebPartFieldsFormatter)
Заголовок раздела «Fields (IWebPartFieldsFormatter)»| Метод | Описание |
|---|---|
FormatValue(field, value) | Человекочитаемое представление значения поля |
Запрос элементов списка
Заголовок раздела «Запрос элементов списка»| Метод | Когда использовать |
|---|---|
QueryListItemsAsync + ListItemQueryBuilder | Фильтр, сортировка, поиск → PortalListItem — рекомендуется |
QueryItemsAsync(listId, ListItemQuery, ct) | То же, сырой JSON |
GetItemsAsync / GetListItemsAsync | Sugar: первая страница представления без явного фильтра |
Сортировка: .OrderBy("column", desc: true) в builder. Типы — Portal.Contracts.Lists: ListItemQuery, ListItemQueryBuilder, ListItemFilterBuilder, ViewFilter.
Частые кейсы (статус, даты, person [Я], lookup, пагинация, Timer Job):
→ Выборка элементов списка (Query / LINQ-стиль)
// По internal_name поля (аналог SharePoint FieldRef Name='Title')var settings = await ctx.Lists.QueryListItemsAsync(listId, ListItemQueryBuilder.ForList(listId) .WhereField("title", f => f.Eq("GlobalSettings")) .Take(1) .Build(), ct);
// С представлением + JSON-фильтромvar query = new ListItemQuery{ ViewId = viewId, Filter = ViewFilterJson.Parse("""{"logic":"and","conditions":[{"column":"title","operator":"eq","value":"A"}]}"""), Limit = 50,};var items = await ctx.Lists.QueryItemsAsync(listId, query, ct);Права проверяются на сервере при каждом вызове API.
Запрос файлов библиотеки
Заголовок раздела «Запрос файлов библиотеки»| Метод | Когда использовать |
|---|---|
QueryFilesAsync(libraryId, LibraryFileQuery, ct) | Фильтр, сортировка, поиск — основной API |
GetFilesAsync(libraryId, limit?, ct) | Sugar: первая страница корня без явного фильтра |
Сортировка: .OrderBy("updated_at", desc: true) в builder или orderBy / sort в opts.
Типы запроса — Portal.Contracts.Libraries: LibraryFileQuery, LibraryFileQueryBuilder, LibraryFileFieldFilterBuilder.
using Portal.Contracts.Libraries;
var pdfs = await ctx.Libraries.QueryFilesAsync(libraryId, LibraryFileQueryBuilder.ForLibrary(libraryId) .Where(b => b.Eq("item_type", "file").Eq("mime_type", "application/pdf")) .OrderBy("updated_at", desc: true) .Take(20) .Build(), ct);Чтение и обновление значений полей
Заголовок раздела «Чтение и обновление значений полей»Рекомендуется SharePoint-подобный API (гайд):
var item = await ctx.Lists.GetListItemAsync(listId, "12", ct); // PortalListItemvar status = item.GetString("status"); // string?var due = item.GetDate("due_at"); // string? "YYYY-MM-DD"var room = item.GetLookup("room"); // PortalLookupValue?item["status"] = "Готово";await item.UpdateAsync(ct);
var created = await ctx.Lists.NewListItemAsync(listId, ct);created["title"] = "Новая задача";await created.CreateAsync(ct);
var found = await ctx.Lists.QueryListItemsAsync(listId, ListItemQueryBuilder.ForList(listId) .WhereField("status", f => f.Eq("Новая")) .Take(50) .Build(), ct); // IReadOnlyList<PortalListItem>Таблица возвращаемых типов геттеров: Кастомная форма §1.
| Метод | Описание |
|---|---|
GetListItemAsync / NewListItemAsync / QueryListItemsAsync / GetListItemsAsync | PortalListItem по internal_name |
GetFieldsAsync(listId, ct) | Схема полей (id, internal_name, field_type) |
GetItemAsync(listId, itemRef, ct) | Сырой JSON (itemRef — номер или guid) |
GetItemVersionsAsync / GetItemVersionAsync | Версии |
CreateItemAsync / UpdateItemAsync | Низкоуровневая запись (fieldValues: UUID или internal_name) |
Низкоуровневый пример (UUID, для версий / сырого JSON):
var fields = await ctx.Lists.GetFieldsAsync(listId, ct);var statusFieldId = fields .First(f => f.GetProperty("internal_name").GetString() == "status") .GetProperty("id").GetString()!;
var item = await ctx.Lists.GetItemAsync(listId, "12", ct);var status = item.GetProperty("field_values").GetProperty(statusFieldId).GetString();
await ctx.Lists.UpdateItemAsync(listId, "12", new Dictionary<string, object?>{ [statusFieldId] = "Готово", // или ["status"] = "Готово"}, ct);
// Значения полей в конкретной версииvar v2 = await ctx.Lists.GetItemVersionAsync(listId, "12", versionNumber: 2, ct);var statusThen = PortalItemJson.GetFieldString(v2, statusFieldId);
// Вся историяvar history = await ctx.Lists.GetItemVersionsAsync(listId, "12", ct);foreach (var version in PortalItemJson.EnumerateVersions(history)){ var n = PortalItemJson.GetVersionNumber(version); var statusAt = PortalItemJson.GetFieldString(version, statusFieldId);}Полные примеры (REST, JS, Event Receiver, Timer Job): Значения полей списков.
HTML-шаблоны
Заголовок раздела «HTML-шаблоны»WebPartTemplate загружает embedded .html из сборки WebPart и подставляет плейсхолдеры {{key}}:
| Метод | Назначение |
|---|---|
Load(assembly, resourceName) | Загрузить шаблон (с кэшированием) |
Load<T>(resourceName) | То же из сборки типа T |
LoadAndApply(assembly, resourceName, values) | Загрузить и подставить плейсхолдеры |
Apply(template, values) | Подставить плейсхолдеры в уже загруженную строку |
// Статический шаблон без плейсхолдеровvar legend = WebPartTemplate.Load(typeof(MyWebPart).Assembly, "Templates.legend.html");// или: WebPartTemplate.Load<MyWebPart>("Templates.legend.html");
// Шаблон с подстановкойvar html = WebPartTemplate.LoadAndApply( typeof(MyWebPart).Assembly, "Templates.widget.html", new Dictionary<string, string> { ["title"] = WpHtml.Escape(title) });Подробнее: Интерфейс, контролы и API — HTML-шаблоны.
Шаблон dotnet new portal-webpart создаёт Templates/widget.html и пример в MyWebPartHandler.cs.
PortalItemJson
Заголовок раздела «PortalItemJson»Хелперы для разбора JSON элементов списка из API:
| Метод | Описание |
|---|---|
GetItemGuid(item) | Внутренний GUID (guid или legacy id-строка) |
GetItemNumber(item) | Публичный номер элемента (id как number) |
GetItemRef(item) | Ref для path API: номер или GUID-строка |
TryGetFieldValues(itemOrVersion) | Объект field_values / fieldValues |
TryGetFieldValue(itemOrVersion, fieldId) | Значение поля по UUID |
GetFieldString(itemOrVersion, fieldId) | Строковое значение поля |
EnumerateVersions(versionsResponse) | Массив версий из GetItemVersionsAsync |
TryFindVersion(versionsResponse, versionNumber) | Версия по номеру |
GetVersionNumber(version) / IsVersioningEnabled(...) | Метаданные истории |
ResolveLookupGuid(lookupVal, numberToGuid?) | GUID из lookup-значения (itemId / item_id) |
ReadLookupGuid(item, fieldId, numberToGuid?) | Lookup GUID из field_values |
ParseDateTimeLocal(raw) | Datetime из ISO/UTC в локальное время процесса |
ReadDateTimeLocal(item, fieldId) | Datetime поля из field_values в локальном времени |
WebPartResponseCache
Заголовок раздела «WebPartResponseCache»In-memory кэш ответов внешних API (общий на процесс backend):
| Член | Описание |
|---|---|
DefaultTtl | TTL по умолчанию (1 час) |
GetOrCreateAsync(key, factory, ct, ttl?, shouldCache?) | Вернуть из кэша или вычислить и сохранить |
var data = await WebPartResponseCache.GetOrCreateAsync( "ext:weather:" + city, ct => FetchWeatherAsync(city, ct), ct, ttl: TimeSpan.FromMinutes(15));Шаблон проекта
Заголовок раздела «Шаблон проекта»# Скачайте portal-sdk-X.Y.Z.zip с https://korport.ru/developersdotnet nuget add source https://korport.ru/api/nuget/v3/index.json -n korportdotnet new install ./portal-sdk-1.0.0/templates/portal-webpartdotnet new portal-webpart -n MyWidget -o ./my-widgetПример в SDK ZIP: examples/hello-widget. См. Extension SDK.
Разработка в монорепозитории Portal (вендор)
cd backenddotnet new install ./templates/portal-webpartdotnet new portal-webpart -n MyWidget -o ../../packages/my-widgetСм. manifest.md — поле runtime: "dotnet", entry, entryType.