Пример расширений для упоминания пользователей в обсуждении¶
С помощью расширений можно дополнять логику выполнения упоминания пользователей в обсуждении. В каждом сценарии расширения используется объект context (IForumUserMentionExtensionContext), в котором, в зависимости от сценария, заполняются различные свойства. Для реализации логики упоминания пользователей предназначен метод IForumUserMentionExtension.ProcessMention.
Расширения на упоминание пользователей в обсуждениях могут быть зарегистрированы для определенных пакетов. Пакеты позволяют объединять различные наборы данных (например, типы карточек) и операций (например, расширений) в единую сущность и идентифицируются уникальным именем. При этом один тип карточки может входить только в один пакет.
Задача для реализации¶
Необходимо создать тип карточки, который содержит обсуждения и ведет в таблице подсчет количества упоминаний пользователей в обсуждениях этой карточки.
Для этого нужно создать две таблицы в схеме, которые необходимы для реализации типа карточки и подсчета количества упоминаний.
Таблица AbDiscussion - строковая, следующей структуры:
| Имя колонки | Тип колонки | Описание |
|---|---|---|
ID |
Reference(Typified) Not Null ссылка на Instances |
Идентификатор карточки |
Name |
String(Max) Not Null |
Имя карточки |
Таблица AbMentionStat - коллекционная, следующей структуры:
| Имя колонки | Тип колонки | Описание |
|---|---|---|
ID |
Reference(Typified) Not Null ссылка на Instances |
Идентификатор карточки |
RowID |
Guid Not Null |
Идентификатор записи в таблице |
UserID |
Reference(Typified) Not Null ссылка на PersonalRoles |
Идентификатор упомянутого пользователя |
UserName |
String(128) Not Null |
Имя упомянутого пользователя |
Count |
Int32 Not Null |
Количество упоминаний пользователя |
В таблице AbMentionStat в разделе Индексы необходимо добавить индекс на колонки ID и UserID.
Далее необходимо создать тип карточки AbDiscussion в группе AbTest.
В раздел Секции нужно включить таблицы AbDiscussion и AbMentionStat со всеми полями. На рисунке ниже представлен раздел Секции:

В разделе Вкладки нужно добавить один блок, в который включить следующие контролы:
- Название - контрол строка, который ссылается на поле
AbDiscussion.Name. - Обсуждение - контрол обсуждения с типом по умолчанию (Default).
- Количество упоминаний - контрол таблица, которая ссылается на секцию
AbMentionStat. Для этой таблицы необходимо установить флаг Только для чтения. В разделе Колонки и форма вкладка Форма не требует изменений, так как пользователи не должны редактировать таблицу вручную. На вкладке Колонки нужно добавить две колонки:- Пользователь - колонка, ссылающаяся на поле
AbMentionStat.User. - Количество упоминаний - колонка, ссылающаяся на поле
AbMentionStat.Count.
- Пользователь - колонка, ссылающаяся на поле
На рисунках ниже представлен результат создания типа карточки и контролов в нем:


Important
Для работы процесса упоминания пользователей необходимо, чтобы тип карточки был включен в типовое решение и в настройках правил доступа был установлен флаг Упоминание новых участников.
Реализация подсчета упоминаний пользователей¶
Подсчет упоминаний пользователей будет реализован с помощью запросов к базе данных в методе расширения BeforeRequest. Реализация такого расширения представлена ниже:
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using LinqToDB;
using Tessa.Forums.Mentions;
using Tessa.Platform.Data;
namespace Tessa.Extensions.Default.Server.AbTest
{
public class CountStatMentionExtension : ForumUserMentionExtension
{
/// <summary>
/// Регулярное выражение для поиска упоминания в сообщении.
/// </summary>
private static readonly Regex mentionRegex = new("data-mention-id=\"(?<userID>[a-fA-F0-9]{8}[-]?([a-fA-F0-9]{4}[-]?){3}[a-fA-F0-9]{12})\"", RegexOptions.Compiled);
/// <inheritdoc/>
public override async Task BeforeRequest(IForumUserMentionExtensionContext context)
{
if (!context.ValidationResult.IsSuccessful())
{
return;
}
var matches = mentionRegex.Matches(context.MessageInfo.HtmlText!)
.Select(x => Guid.Parse(x.Groups["userID"].Value))
.ToArray();
var mentionsMessageCount = new Dictionary<Guid, int>();
foreach (var userID in context.UserIDs)
{
mentionsMessageCount[userID] = matches.Count(x => x == userID);
}
await UpsertStatAsync(context.CardID, mentionsMessageCount, context.DbScope, context.CancellationToken);
}
private static async Task UpsertStatAsync(
Guid cardID,
Dictionary<Guid, int> mentionsCount,
IDbScope dbScope,
CancellationToken cancellationToken = default)
{
await using var _ = dbScope.Create();
var db = dbScope.Db;
var factory = await dbScope.GetBuilderFactoryAsync(cancellationToken);
var query = factory.Cached(nameof(CountStatMentionExtension), "UpsertStat",
b => factory.Dbms switch
{
Dbms.PostgreSql => GenerateUpsertQueryPg(factory),
Dbms.SqlServer => GenerateUpsertQueryMs(factory),
_ => throw new InvalidOperationException($"{nameof(CountStatMentionExtension)}. Unknown Dbms while generating upsert query.")
});
var cardIDParam = db.Parameter("cardID", cardID);
var rowIDParam = db.Parameter("rowID", DataType.Guid);
var userIDParam = db.Parameter("userID", DataType.Guid);
var countParam = db.Parameter("count", DataType.Int32);
foreach (var mention in mentionsCount)
{
rowIDParam.Value = Guid.NewGuid();
userIDParam.Value = mention.Key;
countParam.Value = mention.Value;
db.SetCommand(query, cardIDParam, rowIDParam, userIDParam, countParam);
await db.ExecuteNonQueryAsync(cancellationToken);
}
}
private static string GenerateUpsertQueryPg(IQueryBuilderFactory builder) =>
builder.InsertInto("AbMentionStat", "ID", "RowID", "UserID", "UserName", "Count")
.Select()
.P("cardID")
.P("rowID")
.C("PersonalRoles", "ID", "Name")
.P("count")
.From("PersonalRoles")
.Where()
.C("ID").Equals().P("userID")
.AppendLine().Append("ON CONFLICT (").C("ID").C("UserID").Append(") DO")
.AppendLine().Append("UPDATE SET").AppendLine()
.C("Count").Assign().E(e => e.C("AbMentionStat", "Count").Add(b => b.P("count")))
.Where()
.C("AbMentionStat", "UserID").Equals().P("userID")
.And().C("AbMentionStat", "ID").Equals().P("cardID")
.Build();
private static string GenerateUpsertQueryMs(IQueryBuilderFactory builder) =>
builder.Update("AbMentionStat")
.C("Count").Assign().E(e => e.C("AbMentionStat", "Count").Add(b => b.P("count")))
.Where()
.C("AbMentionStat", "UserID").Equals().P("userID")
.And().C("AbMentionStat", "ID").Equals().P("cardID")
.AppendLine().Append("IF @@ROWCOUNT = 0")
.InsertInto("AbMentionStat", "ID", "RowID", "UserID", "UserName", "Count")
.Select()
.P("cardID")
.P("rowID")
.C("PersonalRoles", "ID", "Name")
.P("count")
.From("PersonalRoles")
.Where()
.C("ID").Equals().P("userID")
.Build();
}
}
Пример регистрации типа карточки и расширения в пакете¶
Вначале нужно создать константу для идентификатора реализованного типа карточки во вспомогательном классе:
public static class AbCardTypes
{
/// <summary>
/// Card type identifier for "AbDiscussion": {369FE1A5-213C-49CD-B2CD-B37BE4D736BC}.
/// </summary>
public static readonly Guid AbDiscussionTypeID = new Guid(0x369fe1a5, 0x213c, 0x49cd, 0xb2, 0xcd, 0xb3, 0x7b, 0xe4, 0xd7, 0x36, 0xbc);
/// <summary>
/// Card type name for "AbDiscussion".
/// </summary>
public const string AbDiscussionTypeName = "AbDiscussion";
}
Для регистрации типа карточки в пакете необходимо получить зависимость ICardBundleRegistry и вызвать метод Register, в который передать идентификатор типа карточки и имя пакета.
Note
Если имя пакета не указано, то метод удаляет регистрацию для указанного типа карточки.
Для того, чтобы расширение выполнялось только для типов карточек, включенных в определенный пакет, его нужно зарегистрировать с помощью метода WhenBundles (см. Расширения на упоминание пользователей в обсуждениях).
Ниже приведен пример регистрации:
using Tessa.Cards;
using Tessa.Extensions.Default.Shared.AbTest;
using Tessa.Forums.Mentions;
using Unity;
using Unity.Lifetime;
namespace Tessa.Extensions.Default.Server.AbTest
{
[Registrator]
public sealed class Registrator : RegistratorBase
{
public override void RegisterUnity()
{
this.UnityContainer
.RegisterType<CountStatMentionExtension>(new ContainerControlledLifetimeManager());
}
public override void RegisterExtensions(IExtensionContainer extensionContainer)
{
extensionContainer
.RegisterExtension<IForumUserMentionExtension, CountStatMentionExtension>(x => x
.WithOrder(ExtensionStage.AfterPlatform)
.WhenBundles("AbTest")
.WhenCardTypes(AbCardTypes.AbDiscussionTypeID))
;
}
public override void FinalizeRegistration()
{
this.UnityContainer
.Resolve<ICardBundleRegistry>()
.Register(AbCardTypes.AbDiscussionTypeID, "AbTest");
}
}
}
Этот код регистрирует созданный тип карточки в пакете с именем AbTest в методе FinalizeRegistration. В методе RegisterExtensions выполняется регистрация расширения для подсчета упоминаний пользователей в пакете AbTest с помощью метода WhenBundles.
Результат работы расширения¶
Реализованное расширение будет обновлять таблицу, в которой содержится статистика упоминаний пользователей, при каждом сообщении, в котором есть хотя бы один упомянутый пользователь.
После нескольких сообщений в обсуждении карточка будет выглядеть следующим образом:

В таблице Количество упоминаний содержится общая статистическая информация по упоминаниям пользователей в сообщениях.