Create a handler
Send the drops' data to your own system. A handler is a class that inherits from WebhookHandlerBase, compiled into a DLL that the application loads at startup. You do not have to modify or rebuild the webhook.
1. Create the project
A class library for the same platform as the webhook, referencing its SESARWebHook.Core.NetCore library and the Secure Exchanges SDK, in the version the webhook uses (see SESARWebHook.Core.NetCore.csproj):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0-windows</TargetFramework>
</PropertyGroup>
<ItemGroup>
<!-- Provided by the webhook at run time: do not copy them with the handler -->
<ProjectReference Include="..\SESARWebhookCore\SESARWebHook.Core.NetCore\SESARWebHook.Core.NetCore.csproj" Private="false" />
<PackageReference Include="SecureExchanges.NetCore" Version="26.4.20.2" ExcludeAssets="runtime" />
</ItemGroup>
</Project>
2. Write the class
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using SecureExchangesSDK.Models.Messenging;
using SESARWebHook.Core;
using SESARWebHook.Core.Auth;
using SESARWebHook.Core.Models;
namespace Example.Handlers
{
public class CasesHandler : WebhookHandlerBase
{
// A single HttpClient for the whole life of the application.
private static readonly HttpClient Http = new HttpClient();
private ApiKeyHelper _auth;
private string _endpoint;
public override string HandlerId => "my-cases";
public override string DisplayName => "My cases";
public override void Initialize(Dictionary<string, string> settings)
{
base.Initialize(settings);
_endpoint = GetRequiredSetting("ApiUrl"); // appsettings.json
_auth = ApiKeyHelper.AsBearer(GetRequiredSetting("ApiKey")); // secrets file
}
public override async Task<IntegrationResult> ProcessAsync(StoreManifest manifest, WebhookContext context)
{
var item = new
{
tracking = GetTrackingId(manifest), // stable identifier: used to discard duplicates
subject = GetSubject(manifest),
files = GetFileCount(manifest)
};
using var request = new HttpRequestMessage(HttpMethod.Post, _endpoint)
{
Content = new StringContent(JsonSerializer.Serialize(item), Encoding.UTF8, "application/json")
};
_auth.ApplyTo(request);
using var response = await Http.SendAsync(request);
return response.IsSuccessStatusCode
? IntegrationResult.Ok("Case sent", HandlerId)
: IntegrationResult.Fail("Refused by the target system", $"HTTP {(int)response.StatusCode}", HandlerId);
}
}
}
The webhook keeps a class if it is public, not abstract, implements IWebhookHandler and has a public parameterless constructor. Its HandlerId is part of the address: choose it stable, in lowercase, without spaces.
Members to know
| Member | Role |
|---|---|
HandlerId, DisplayName |
Required. Description and Version are optional. |
Initialize(settings) |
Called once, the first time the handler is used. Call base.Initialize(settings) first. |
ProcessAsync(manifest, context) |
Called for each drop. Return IntegrationResult.Ok or IntegrationResult.Fail. |
ValidateConfigurationAsync, TestConnectionAsync |
Called by POST /api/handlers/{id}/test. Return true by default. |
GetSetting, GetRequiredSetting, GetSettingBool, GetSettingInt |
Read the settings and secrets. GetRequiredSetting throws an exception if the value is missing, without quoting it. |
GetSubject, GetRecipientEmails, GetFileCount, GetTrackingId, IsReply, DecodeBase64 |
Read the manifest. |
The full list is in the .NET reference.
Design rules
- One instance for the whole life of the application. The handler is created once, then reused, including for simultaneous requests: do not keep the state of a drop in a field.
- A light constructor. It runs when the DLL is loaded and on each
GET /api/handlers. Put the setup inInitialize. - Less than 60 seconds per drop, and duplicates accepted: see Configure SESAR.
- No personal information in logs or in the
IntegrationResult, which is returned as is in the HTTP response. Do not usecontext.RawPayloadoutside the processing: it contains the decrypted manifest. - Exceptions thrown in
ProcessAsyncare caught and returned as a failure, with their details. Those fromInitializeare not: the request receives a 500 error.
3. Configure
Settings go in appsettings.json, secrets in the secrets file. The handler receives both, merged, in its Settings.
{
"Handler": {
"my-cases": { "ApiUrl": "https://api.example.com/cases" }
}
}
{
"Handlers": {
"my-cases": { "Secrets": { "ApiKey": "<your API key>" } }
}
}
4. Deploy
- Build the project in Release.
- Copy the handler DLL, and its own dependencies only, to the handlers folder:
Handlers, in the application folder, or the one set byHandlersPath. Subfolders are not read. - Restart the application. A DLL already loaded stays in memory until the restart, and
POST /api/handlers/rescanonly reads the default folder again. - Check that it appears in
GET /api/handlers, then test it withPOST /api/handlers/my-cases/test. - In SESAR, set the address
https://<server>/api/webhook/handler/my-cases.
If two DLLs declare the same HandlerId, the last one loaded wins, without warning.
Warning
A handler runs in the webhook's process, with all its rights, and can read all the application's secrets. Only drop in the handlers folder DLLs whose code you know, and reserve write access to this folder to administrators.
Built-in connector: for those who maintain their own version of the webhook
A built-in connector implements IIntegrationConnector. It is not loaded from a folder: you must add it to the project, then to StartupConfig.InitializeConnectors in Program.cs (ConnectorRegistry.RegisterConnector<T>()), and rebuild the webhook. The SESARWebHook.Connectors.Template.NetCore project serves as a model; its comments still mention Web.config, replaced by appsettings.json.
Prefer a handler: it survives webhook updates without merging code.