Skip to content

plugin sdk request manager

Andre Lafleur edited this page Jul 10, 2026 · 12 revisions

About plugin request handling

Plugins can handle custom requests from client applications (custom tasks, other plugins) using the RequestManager.

Request/Response Architecture

sequenceDiagram
    participant Client as Client application
    participant ClientRM as SDK RequestManager (client)
    participant Directory
    participant PluginRM as SDK RequestManager (plugin)
    participant Handler as Request handler

    Client->>ClientRM: SendRequest TRequest to TResponse
    ClientRM->>ClientRM: Resolve recipient to role owner
    ClientRM->>Directory: Send request
    Directory->>PluginRM: Dispatch request to role
    PluginRM->>Handler: Invoke registered handler
    Handler-->>PluginRM: RequestCompletion TResponse
    PluginRM-->>Directory: Send response
    Directory-->>ClientRM: Return response
    ClientRM-->>Client: TResponse
Loading

Key concepts:

  • Request/response communication pattern (RPC-style)
  • Type-safe request and response classes
  • Requests target a recipient entity GUID
  • Multiple handlers for different request types
  • Synchronous, asynchronous, and completion-based handlers

Use Cases

Custom tasks:

  • User triggers task in Security Desk
  • Task sends command to plugin
  • Plugin executes operation
  • Returns result to task

Inter-plugin communication:

  • One plugin requests data from another
  • Plugins coordinate actions
  • Share state or resources

Diagnostics and testing:

  • Test connections
  • Query plugin state
  • Trigger operations
  • Get statistics

Request handler types

Synchronous Handler

Simple function that returns response immediately:

protected override void OnPluginLoaded()
{
    // Use Plugin's protected helper method
    AddRequestHandler<GetStatusRequest, StatusResponse>(HandleGetStatus);
}

private StatusResponse HandleGetStatus(GetStatusRequest request)
{
    return new StatusResponse
    {
        IsConnected = m_isConnected,
        DeviceCount = m_devices.Count,
        Uptime = DateTime.UtcNow - m_startTime
    };
}

When to use:

  • Fast operations (< 100ms)
  • No I/O required
  • Simple data retrieval
  • Immediate results available

Asynchronous Handler (Task-based)

Async method that can await operations:

protected override void OnPluginLoaded()
{
    // Use Plugin's protected helper method
    AddAsyncRequestHandler<TestConnectionRequest, TestConnectionResponse>(
        HandleTestConnectionAsync);
}

private async Task<TestConnectionResponse> HandleTestConnectionAsync(
    TestConnectionRequest request,
    RequestManagerContext context)
{
    var startTime = DateTime.UtcNow;

    // A failed or timed-out connection throws; the exception propagates to the caller.
    await TestConnectionToExternalSystemAsync(
        request.Timeout,
        context.RequestCancellationToken);

    return new TestConnectionResponse
    {
        ResponseTime = DateTime.UtcNow - startTime
    };
}

When to use:

  • Slow operations (> 100ms)
  • I/O operations (database, network)
  • Long-running tasks
  • Need async/await
  • Need cancellation support or user context

RequestManagerContext

The RequestManagerContext parameter provides information about the request origin and supports cancellation:

Properties

public class RequestManagerContext
{
    // Cancellation support
    CancellationToken RequestCancellationToken { get; }
    TimeSpan RequestTimeout { get; }
    
    // Request source information
    Guid SourceApplication { get; }     // Application that sent request
    Guid SourceUser { get; }            // User who initiated request
    string SourceUsername { get; }      // Username of requester
    
    // Request metadata
    DateTime ReceptionTimestamp { get; }              // When request was received (UTC)
    IReadOnlyCollection<Guid> DestinationEntities { get; } // Target entities
}

Usage Examples

Cancellation support:

private async Task<Response> HandleRequestAsync(
    Request request,
    RequestManagerContext context)
{
    // Pass the cancellation token to async work; if the caller cancels, the await
    // throws OperationCanceledException, which propagates to the caller.
    var data = await FetchDataAsync(context.RequestCancellationToken);

    return new Response { Data = data };
}

User-specific logic:

private async Task<Response> HandleRequestAsync(Request request, RequestManagerContext context)
{
    var user = Engine.GetEntity<User>(context.SourceUser);

    Logger.TraceInformation($"Request from {context.SourceUsername} received at {context.ReceptionTimestamp}");

    // Reject an unauthorized caller by throwing; the exception propagates to the caller.
    if (!user.HasPrivilege(MyPrivilege))
    {
        throw new UnauthorizedAccessException("Insufficient privileges");
    }

    return await ProcessRequestAsync(request);
}

Timeout handling:

private async Task<Response> HandleRequestAsync(
    Request request,
    RequestManagerContext context)
{
    Logger.TraceDebug($"Request timeout: {context.RequestTimeout}");

    // Enforce the caller's timeout; on expiry the await throws
    // OperationCanceledException, which propagates to the caller.
    using var cts = CancellationTokenSource.CreateLinkedTokenSource(
        context.RequestCancellationToken);
    cts.CancelAfter(context.RequestTimeout);

    return await ProcessWithTimeoutAsync(request, cts.Token);
}

Completion-based Handler

Callback pattern for complex scenarios:

protected override void OnPluginLoaded()
{
    // Use Plugin's protected helper method
    AddRequestHandler<ComplexRequest, ComplexResponse>(HandleComplexRequest);
}

private void HandleComplexRequest(
    ComplexRequest request,
    RequestCompletion<ComplexResponse> completion)
{
    // Process in background
    Task.Run(async () =>
    {
        try
        {
            var result = await ProcessComplexOperationAsync(request);
            completion.SetResponse(new ComplexResponse { Result = result });
        }
        catch (Exception ex)
        {
            completion.SetError(ex);
        }
    });
}

When to use:

  • Need manual control over completion
  • Complex error handling
  • Progress reporting scenarios
  • Legacy async patterns

Registering Handlers

Register in OnPluginLoaded

protected override void OnPluginLoaded()
{
    // Register multiple handlers using Plugin's protected helper methods
    AddRequestHandler<GetStatusRequest, StatusResponse>(HandleGetStatus);
    AddAsyncRequestHandler<TestConnectionRequest, TestConnectionResponse>(
        HandleTestConnectionAsync);
}

Remove in Dispose

protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        // Remove handlers using Plugin's protected helper methods
        RemoveRequestHandler<GetStatusRequest, StatusResponse>(HandleGetStatus);
        RemoveAsyncRequestHandler<TestConnectionRequest, TestConnectionResponse>(
            HandleTestConnectionAsync);
    }
}

Important

  • Register handlers in OnPluginLoaded()
  • Remove handlers in Dispose()
  • Match handler type when removing (sync vs async)

Request Routing

How routing works

Requests are routed by:

  1. Request type - The generic type TRequest
  2. Recipient entity - The GUID passed to SendRequest
  3. Handler registration - Plugin must register handler for type

If the recipient entity is owned by a plugin role, the RequestManager routes the request to the owner role. Multiple plugins can handle the same request type as long as the recipient entity differs.

Sending a request

A client issues a request through the engine's request manager, targeting the recipient entity GUID:

var response = engine.RequestManager.SendRequest<GetStatusRequest, StatusResponse>(
    pluginRoleEntityGuid,
    new GetStatusRequest());

SendRequestAsync<TRequest, TResponse>(recipientId, request) is the async form, and both have an overload that takes a TimeSpan timeout. recipientId is the GUID of the entity the plugin role owns, often the plugin role itself.

Request Classes

Define request and response classes:

[Serializable]
public class GetDeviceListRequest
{
    public bool IncludeOffline { get; set; }
    public DateTime Since { get; set; }
}

[Serializable]
public class DeviceListResponse
{
    public List<DeviceInfo> Devices { get; set; }
    public int TotalCount { get; set; }
}

[Serializable]
public class DeviceInfo
{
    public Guid DeviceGuid { get; set; }
    public string Name { get; set; }
    public bool IsOnline { get; set; }
}

Requirements:

  • Request and response types must be public and serializable. Use [Serializable], or use [DataContract] with [DataMember] on each member that must be sent.
  • The sender and handler must use matching request and response types. Prefer sharing the same classes in a common assembly. If the client and plugin define the classes separately, use the same class name, namespace, and serialized members on both sides.
  • Use simple member types, such as primitive values, strings, GUIDs, dates, lists, dictionaries, and other public serializable classes.

Common request patterns

Test Connection

AddAsyncRequestHandler<TestConnectionRequest, TestConnectionResponse>(
    async (request, context) =>
    {
        var sw = Stopwatch.StartNew();

        // A connection failure or timeout throws; the exception propagates to the caller.
        await ConnectToExternalSystemAsync(
            request.Timeout,
            context.RequestCancellationToken);

        return new TestConnectionResponse
        {
            ResponseTime = sw.Elapsed
        };
    });

Execute Command

AddAsyncRequestHandler<ExecuteCommandRequest, CommandResponse>(
    async (request, context) =>
    {
        try
        {
            var result = await ExecuteCommandOnHardwareAsync(
                request.DeviceGuid,
                request.Command,
                request.Parameters,
                context.RequestCancellationToken);

            return new CommandResponse { Result = result };
        }
        catch (Exception ex)
        {
            // Log on the plugin side, then let the exception propagate to the caller.
            Logger.TraceError(ex, "Command execution failed");
            throw;
        }
    });

Query State

AddRequestHandler<GetDeviceStateRequest, DeviceStateResponse>(request =>
{
    var device = m_devices.FirstOrDefault(d => d.Guid == request.DeviceGuid);

    if (device == null)
    {
        return new DeviceStateResponse { Found = false };
    }

    return new DeviceStateResponse
    {
        Found = true,
        State = device.State,
        LastUpdate = device.LastUpdate,
        Properties = device.GetProperties()
    };
});

Error Handling

When a handler throws, the RequestManager captures the exception and delivers it to the caller, where SendRequest and SendRequestAsync rethrow it as an SdkException.

Throw from the handler

private StatusResponse HandleGetStatus(GetStatusRequest request)
{
    if (!m_initialized)
    {
        throw new InvalidOperationException("Plugin not initialized");
    }

    return new StatusResponse { ... };
}

For a completion-based handler, report failure with completion.SetError(ex) instead of throwing; it reaches the caller the same way.

Catch on the caller

try
{
    var status = engine.RequestManager.SendRequest<GetStatusRequest, StatusResponse>(
        pluginRoleEntityGuid,
        new GetStatusRequest());
}
catch (SdkException ex)
{
    Logger.TraceError(ex, "Status request failed");
}

Guidance:

  • A handler that fails should throw, or call completion.SetError; the failure reaches the caller as an SdkException.
  • To record a failure on the plugin side without swallowing it, catch it, log, and rethrow.
  • Model a legitimate not-found or empty outcome as a value on the response, such as DeviceStateResponse.Found, rather than as an error.

Federated action requests

Plugins can receive requests that cross Security Center federation boundaries using OnSdkFederationActionRequestReceived().

Overview

Federated action requests allow communication between plugins across federated Security Center systems. The request and response use serialized string payloads, giving you flexibility in the data format.

protected virtual SdkFederationActionResponse OnSdkFederationActionRequestReceived(
    SdkFederationActionRequest request)

Returns: null by default. Override to handle requests.

SdkFederationActionRequest

Property Type Description
Request string Serialized request payload
RequestId Guid Unique identifier for this request

SdkFederationActionResponse

Property Type Description
Response string Serialized response payload

Implementation Example

[Serializable]
public class MyFederatedRequest
{
    public string Command { get; set; }
    public Dictionary<string, string> Parameters { get; set; }
}

[Serializable]
public class MyFederatedResponse
{
    public string Result { get; set; }
}

public class MyPlugin : Plugin
{
    protected override SdkFederationActionResponse OnSdkFederationActionRequestReceived(
        SdkFederationActionRequest request)
    {
        try
        {
            // Deserialize the request
            var federatedRequest = JsonConvert.DeserializeObject<MyFederatedRequest>(
                request.Request);

            Logger.TraceInformation(
                $"Received federated request {request.RequestId}: {federatedRequest.Command}");

            // Process the request
            string result = ProcessCommand(federatedRequest.Command, federatedRequest.Parameters);

            // Serialize the result payload
            var response = new MyFederatedResponse { Result = result };

            return new SdkFederationActionResponse(JsonConvert.SerializeObject(response));
        }
        catch (Exception ex)
        {
            // Log on the plugin side, then let the exception propagate to the caller.
            Logger.TraceError(ex, "Failed to process federated request");
            throw;
        }
    }

    private string ProcessCommand(string command, Dictionary<string, string> parameters)
    {
        // Implement command processing logic
        return $"Processed: {command}";
    }
}

Usage notes:

  • Use JSON or another serialization format for request/response payloads
  • Return null if your plugin does not handle federated requests
  • Let a failure throw; the RequestManager delivers it to the caller, so you do not serialize an error into the response payload
  • Works for both federated and non-federated Security Center deployments

See also

Platform SDK

Plugin SDK

Workspace SDK

Media SDK

Macro SDK

Web SDK

Synergis RIO

Media Gateway

Genetec Web Player

Clone this wiki locally