Примечание
Функция доступна по запросу в поддержку: support@timetta.com.
Важно
Компонента позволяет кастомизировать логику работы с данными:
Если вам нужно переопределение логики, рекомендуем обратиться в поддержку support@timetta.com для платной консультации.
Основные сценарии применения обработчика:
Обработчик представляет собой класс на C# для каждого типа сущности. В системе он наследует абстрактный класс EntityTypeCustomHooks<TEntity>. Все методы в нём объявлены как virtual с реализацией по умолчанию Task.CompletedTask — переопределять нужно только те методы, логика которых действительно требуется.
В обработчике доступны четыре переопределяемых метода (хука), вызываемых при операциях с сущностью:
| Хук | Когда вызывается | Параметры |
|---|---|---|
| BeforeUpsert | До вставки или обновления сущности. Используется для базовых проверок. | context, detachedEntity |
| AfterUpsert | После вставки или обновления. Предназначен для бизнес-логики: предзаполнение свойств, исходящие интеграции. | context, contextEntity, detachedEntity |
| BeforeDelete | До удаления сущности. | context, detachedEntity |
| AfterDelete | После удаления сущности. | context, contextEntity |
Помимо CRUD-хуков в обработчике доступны три метода, связанные со сменой состояния и запуском воркфлоу:
| Хук | Когда вызывается | Параметры |
|---|---|---|
| BeforeSetState | До смены состояния сущности в жизненном цикле | context, entity, oldStateId, newStateId |
| AfterSetState | После смены состояния | context, entity, oldStateId, newStateId |
| BeforeStartWorkflow | Перед запуском воркфлоу по сущности | context, contextEntity |
Их можно переопределять так же, как остальные хуки (по умолчанию — пустая реализация).
В методы передаются параметры:
detachedEntity.Примечание
Для доступа требуются права на гранулу Настройки менеджера объектов.
Чтобы изменить обработчик:

В настройках генерируется класс с именем CustomHooks, наследующий EntityTypeCustomHooks<ИмяТипаСущности>.
Объект контекста наследует DynamicCodeContext и предоставляет доступ к базовому API системы.
public interface DynamicCodeContext
{
/// Тип операции, для которой вызван обработчик
EntityServiceOperation OperationType { get; }
/// Событие до коммита транзакции
event Func<Task> BeforeCommit;
/// Событие после коммита транзакции
event Func<Task> AfterCommit;
/// Сервис работы с сущностями
IContextEntityService<TEntity> GetEntityService<TEntity>();
/// Логирование (асинхронное, с уровнем)
Task Log(string message, DynamicCodeLogLevel level = DynamicCodeLogLevel.Info);
}
IStatedEntity).Сигнатура метода логирования — асинхронная, с уровнем важности:
Task Log(string message, DynamicCodeLogLevel level = DynamicCodeLogLevel.Info);
Уровни: DynamicCodeLogLevel.Info, DynamicCodeLogLevel.Error. Сообщения записываются в сущность «Лог обработчика» (CustomHookLog) и могут просматриваться в системе. Вызовы пользовательского кода обработчика также оборачиваются системой: в журнал пишутся факт старта и завершения каждого хука, а при исключении — сообщение и стек (уровень Error).
Интерфейс сервиса работы с сущностями в контексте:
public interface IContextEntityService<TEntity> where TEntity : class, IEntity
{
Task<TEntity> InsertAsync(TEntity detachedEntity, bool checkRights = true);
Task<TEntity> UpdateAsync(TEntity detachedEntity, bool checkRights = true);
Task<TEntity> DeleteAsync(Guid entityId, bool checkRights = true);
Task<TEntity> GetAsync(Guid entityId, bool checkRights = true);
IQueryable<TEntity> Get(
Expression<Func<TEntity, bool>> condition = null,
bool checkRights = true
);
Task CheckCanInsertAsync(TEntity entity);
Task CheckCanEditAsync(TEntity entity);
Task CheckCanEditAsync(Guid entityId);
Task CheckCanDeleteAsync(TEntity entity);
Task CheckCanExecuteAsync(Guid entityId, string granular);
Task CheckCanViewAsync(Guid entityId, string granularName = null);
Task<IQueryable<TCollectionType>> GetNavigationAsync<TCollectionType>(
Guid key,
Expression<Func<TCollectionType, Guid>> foreignKeyExpression
) where TCollectionType : Entity;
Task<List<TCollectionType>> UpdateNavigationAsync<TCollectionType>(
Guid key,
Expression<Func<TCollectionType, Guid>> foreignKeyExpression,
ICollection<TCollectionType> collection,
Func<TCollectionType, object> groupByExpression = null
) where TCollectionType : Entity;
}
checkRights определяет, проверять ли права текущего пользователя.Проверка перед сохранением и логирование после создания/обновления/удаления:
public class CustomHooks : EntityTypeCustomHooks<Activity>
{
public override async Task BeforeUpsert(
CustomHooksContext context,
Activity detachedEntity)
{
if (string.IsNullOrWhiteSpace(detachedEntity.Name))
await context.Log("Имя не задано", DynamicCodeLogLevel.Error);
await Task.CompletedTask;
}
public override async Task AfterUpsert(
CustomHooksContext context,
Activity contextEntity,
Activity detachedEntity)
{
await context.Log($"Сущность сохранена: {contextEntity.Id}");
await Task.CompletedTask;
}
public override async Task BeforeDelete(
CustomHooksContext context,
Activity detachedEntity)
{
await context.Log($"Попытка удаления: {detachedEntity.Id}");
await Task.CompletedTask;
}
public override async Task AfterDelete(
CustomHooksContext context,
Activity contextEntity)
{
await context.Log($"Сущность удалена: {contextEntity.Id}");
await Task.CompletedTask;
}
}
Логирование и уведомления при смене состояния проекта:
public class CustomHooks : EntityTypeCustomHooks<Project>
{
public override async Task BeforeSetState(
CustomHooksContext context,
Project entity,
Guid oldStateId,
Guid newStateId)
{
await context.Log($"Смена состояния проекта {entity.Id}: {oldStateId} → {newStateId}");
await Task.CompletedTask;
}
public override async Task AfterSetState(
CustomHooksContext context,
Project entity,
Guid oldStateId,
Guid newStateId)
{
var notificationService = context.GetNotificationService();
await context.Log($"Состояние проекта {entity.Id} обновлено на {newStateId}");
await Task.CompletedTask;
}
public override async Task BeforeStartWorkflow(
CustomHooksContext context,
Project contextEntity)
{
await context.Log($"Запуск воркфлоу для проекта {contextEntity.Id}");
await Task.CompletedTask;
}
}
BeforeSetState вызывается до смены состояния (проверки, логирование «до»). AfterSetState — после сохранения нового состояния (интеграции, уведомления). BeforeStartWorkflow — непосредственно перед стартом воркфлоу по сущности.
public override async Task AfterUpsert(CustomHooksContext context, Activity contextEntity, Activity detachedEntity)
{
const string url = "https://api.timetta.com/odata";
const string token = "***";
using var httClient = new HttpClient();
httClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var newActivity = new { Name = "New Activity" };
var payload = JsonContent.Create(newActivity);
var response = await httClient.PostAsync($"{url}/Activities", payload);
var responseContent = await response.Content.ReadFromJsonAsync<JsonElement>();
var idOfCreatedEntity = responseContent.GetProperty("id").GetGuid();
}
Для HTTP-запросов из обработчика можно также использовать context.GetHttpClient(), который возвращает настроенный клиент.
Перейти на русскую версию?