-
Notifications
You must be signed in to change notification settings - Fork 6
plugin sdk request manager
Plugins can handle custom requests from client applications (custom tasks, config pages, other plugins) using the RequestManager.
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
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
Custom configuration pages:
- Page requests current settings
- Plugin returns configuration
- Page sends updated settings
- Plugin validates and applies
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
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
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)
{
try
{
var startTime = DateTime.UtcNow;
await TestConnectionToExternalSystemAsync(
request.Timeout,
context.RequestCancellationToken);
var elapsed = DateTime.UtcNow - startTime;
return new TestConnectionResponse
{
Success = true,
ResponseTime = elapsed
};
}
catch (Exception ex)
{
return new TestConnectionResponse
{
Success = false,
Error = ex.Message
};
}
}When to use:
- Slow operations (> 100ms)
- I/O operations (database, network)
- Long-running tasks
- Need async/await
- Need cancellation support or user context
The RequestManagerContext parameter provides information about the request origin and supports cancellation:
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
}Cancellation support:
private async Task<Response> HandleRequestAsync(
Request request,
RequestManagerContext context)
{
// Pass cancellation token to async operations
var data = await FetchDataAsync(context.RequestCancellationToken);
// Check if cancelled
if (context.RequestCancellationToken.IsCancellationRequested)
return new Response { Cancelled = true };
return new Response { Data = data };
}User-specific logic:
private Response HandleRequest(Request request, RequestManagerContext context)
{
var user = Engine.GetEntity<User>(context.SourceUser);
Logger.TraceInformation($"Request from {context.SourceUsername} received at {context.ReceptionTimestamp}");
// Apply user-specific permissions or behavior
if (user.HasPrivilege(MyPrivilege))
{
return ProcessRequest(request);
}
return new Response { Error = "Insufficient privileges" };
}Timeout handling:
private async Task<Response> HandleRequestAsync(
Request request,
RequestManagerContext context)
{
Logger.TraceDebug($"Request timeout: {context.RequestTimeout}");
try
{
// Use the timeout from context
using var cts = CancellationTokenSource.CreateLinkedTokenSource(
context.RequestCancellationToken);
cts.CancelAfter(context.RequestTimeout);
return await ProcessWithTimeoutAsync(request, cts.Token);
}
catch (OperationCanceledException)
{
return new Response { Error = "Request timed out" };
}
}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
protected override void OnPluginLoaded()
{
// Register multiple handlers using Plugin's protected helper methods
AddRequestHandler<GetConfigRequest, PluginConfig>(HandleGetConfig);
AddRequestHandler<SetConfigRequest, SetConfigResponse>(HandleSetConfig);
AddAsyncRequestHandler<TestConnectionRequest, TestConnectionResponse>(
HandleTestConnectionAsync);
}protected override void Dispose(bool disposing)
{
if (disposing)
{
// Remove handlers using Plugin's protected helper methods
RemoveRequestHandler<GetConfigRequest, PluginConfig>(HandleGetConfig);
RemoveRequestHandler<SetConfigRequest, SetConfigResponse>(HandleSetConfig);
RemoveAsyncRequestHandler<TestConnectionRequest, TestConnectionResponse>(
HandleTestConnectionAsync);
}
}Important
- Register handlers in
OnPluginLoaded() - Remove handlers in
Dispose() - Match handler type when removing (sync vs async)
Requests are routed by:
-
Request type - The generic type
TRequest -
Recipient entity - The GUID passed to
SendRequest - 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.
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:
- Must be
[Serializable] - Must be able to serialize/deserialize (JSON or binary)
- Keep classes simple (POCOs)
- Avoid complex object graphs
// Get configuration
AddRequestHandler<GetConfigRequest, PluginConfig>(request => LoadConfiguration());
// Set configuration
AddRequestHandler<SetConfigRequest, SetConfigResponse>(request =>
{
try
{
if (!ValidateConfiguration(request.Config, out string error))
{
return new SetConfigResponse
{
Success = false,
Error = error
};
}
UpdateConfiguration(request.Config);
ApplyConfiguration(request.Config);
return new SetConfigResponse { Success = true };
}
catch (Exception ex)
{
Logger.TraceError(ex, "Failed to update configuration");
return new SetConfigResponse
{
Success = false,
Error = ex.Message
};
}
});AddAsyncRequestHandler<TestConnectionRequest, TestConnectionResponse>(
async (request, context) =>
{
var sw = Stopwatch.StartNew();
try
{
await ConnectToExternalSystemAsync(
request.Timeout,
context.RequestCancellationToken);
return new TestConnectionResponse
{
Success = true,
Message = "Connection successful",
ResponseTime = sw.Elapsed
};
}
catch (TimeoutException)
{
return new TestConnectionResponse
{
Success = false,
Message = "Connection timeout",
ResponseTime = sw.Elapsed
};
}
catch (Exception ex)
{
return new TestConnectionResponse
{
Success = false,
Message = ex.Message,
ResponseTime = sw.Elapsed
};
}
});AddAsyncRequestHandler<ExecuteCommandRequest, CommandResponse>(
async (request, context) =>
{
try
{
var result = await ExecuteCommandOnHardwareAsync(
request.DeviceGuid,
request.Command,
request.Parameters,
context.RequestCancellationToken);
return new CommandResponse
{
Success = true,
Result = result
};
}
catch (Exception ex)
{
Logger.TraceError(ex, "Command execution failed");
return new CommandResponse
{
Success = false,
Error = ex.Message
};
}
});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()
};
});private SetConfigResponse HandleSetConfig(SetConfigRequest request)
{
try
{
ValidateAndApplyConfig(request.Config);
return new SetConfigResponse { Success = true };
}
catch (ValidationException ex)
{
return new SetConfigResponse
{
Success = false,
Error = ex.Message,
ValidationErrors = ex.Errors
};
}
catch (Exception ex)
{
Logger.TraceError(ex, "Configuration update failed");
return new SetConfigResponse
{
Success = false,
Error = "Internal error occurred"
};
}
}private StatusResponse HandleGetStatus(GetStatusRequest request)
{
if (!m_initialized)
{
throw new InvalidOperationException("Plugin not initialized");
}
return new StatusResponse { ... };
}Exception handling:
- Exceptions are serialized and sent to client
- Client receives exception details
- Use exceptions for unexpected errors
- Prefer returning error in response for expected errors
Request handlers run on a background thread, not the engine thread.
Use Engine.QueueUpdate() or Engine.QueueUpdateAndWait() when work
needs to update entities or other Engine state.
If handler spawns async work:
private void HandleLongOperation(
LongOperationRequest request,
RequestCompletion<LongOperationResponse> completion)
{
Task.Run(async () =>
{
var result = await DoLongWorkAsync();
// Queue back to engine thread for Engine access
Engine.QueueUpdate(() =>
{
UpdateEntityState(result);
completion.SetResponse(new LongOperationResponse { ... });
});
});
}Plugins can receive requests that cross Security Center federation boundaries using OnSdkFederationActionRequestReceived().
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.
| Property | Type | Description |
|---|---|---|
Request |
string |
Serialized request payload |
RequestId |
Guid |
Unique identifier for this request |
| Property | Type | Description |
|---|---|---|
Response |
string |
Serialized response payload |
[Serializable]
public class MyFederatedRequest
{
public string Command { get; set; }
public Dictionary<string, string> Parameters { get; set; }
}
[Serializable]
public class MyFederatedResponse
{
public bool Success { get; set; }
public string Result { get; set; }
public string Error { 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);
// Create and serialize response
var response = new MyFederatedResponse
{
Success = true,
Result = result
};
return new SdkFederationActionResponse(JsonConvert.SerializeObject(response));
}
catch (Exception ex)
{
Logger.TraceError(ex, "Failed to process federated request");
var errorResponse = new MyFederatedResponse
{
Success = false,
Error = ex.Message
};
return new SdkFederationActionResponse(JsonConvert.SerializeObject(errorResponse));
}
}
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
nullif your plugin does not handle federated requests - Handle exceptions and return error responses rather than throwing
- Works for both federated and non-federated Security Center deployments
- Plugin SDK overview: Plugin architecture, lifecycle, and components.
- Plugin SDK threading: Engine thread, QueueUpdate, and async patterns for plugins.
- Plugin SDK lifecycle: Plugin initialization, startup, and disposal phases.
- Plugin SDK configuration: Role-level configuration storage and update flow.
- About logging: SDK logging configuration and severity levels.
- Overview
- Connecting to Security Center
- SDK certificates
- Referencing SDK assemblies
- SDK compatibility
- Entities
- Entity cache
- Transactions
- Events
- Actions
- Security Desk
- Custom events
- ReportManager
- ReportManager query reference
- DownloadAllRelatedData and StrictResults
- Privileges
- Partitions
- Mobile credentials
- Logging
- Overview
- Certificates
- Lifecycle
- Threading
- State management
- Configuration
- Restricted configuration
- Events
- Queries
- Request manager
- Database
- Entity ownership
- Entity mappings
- Server management
- Custom privileges
- Custom entity types
- Resolving non-SDK assemblies
- Deploying plugins
- .NET 8 support
- Overview
- Certificates
- Creating modules
- Tasks
- Pages
- Components
- Tile extensions
- Services
- Contextual actions
- Options extensions
- Configuration pages
- Monitors
- Shared components
- Commands
- Extending events
- Map extensions
- Timeline providers
- Image extractors
- Credential encoders
- Credential readers
- Cardholder fields extractors
- Badge printers
- Content builders
- Dashboard widgets
- Incidents
- Logon providers
- Pinnable content builders
- Custom report pages
- Overview
- Getting started
- MediaPlayer
- VideoSourceFilter
- MediaExporter
- MediaFile
- G64 converters
- FileCryptingManager
- PlaybackSequenceQuerier
- PlaybackStreamReader
- OverlayFactory
- PtzCoordinatesManager
- AudioTransmitter
- AudioRecorder
- AnalogMonitorController
- Camera blocking
- Overview
- Getting started
- Referencing entities
- Entity operations
- About access control in the Web SDK
- About video in the Web SDK
- Users and user groups
- Partitions
- Custom fields
- Custom card formats
- Actions
- Events and alarms
- Incidents
- Reports
- Tasks
- Macros
- Custom entity types
- System endpoints
- Performance guide
- Reference
- Under the hood
- Troubleshooting