-
Notifications
You must be signed in to change notification settings - Fork 6
plugin sdk queries
Plugins respond to queries from clients (Security Desk, Config Tool, Web SDK). Understanding the query flow is essential for proper implementation.
sequenceDiagram
participant Client as Client (Security Desk)
participant Directory
participant Host as Plugin Host
participant Plugin
Client->>Directory: Send ReportQuery
Directory->>Host: Distribute to roles with supported report types
Host->>Host: Filter based on SupportedQueries and SupportedCustomReports
Host->>Plugin: OnQueryReceived()
Plugin->>Plugin: Process query
Plugin-->>Directory: Send results
Directory-->>Client: Return results
- Directory distributes queries to roles that registered supported report types
- SDK filters queries using
SupportedQueriesandSupportedCustomReports - Plugin receives only queries it supports
- Plugin must send results and completion
Plugins must explicitly declare which query types they support by overriding SupportedQueries:
public override List<ReportQueryType> SupportedQueries =>
new List<ReportQueryType>
{
ReportQueryType.DoorActivity,
ReportQueryType.AuditTrails,
ReportQueryType.Custom
};Important
- Only queries in
SupportedQueriesare routed to your plugin - The SDK filters queries before calling
OnQueryReceived() - If
SupportedQueriesis empty or not overridden, the plugin receives no queries - Custom queries require filtering by
CustomReportId
The query handler is the entry point for all plugin-side query processing.
This abstract method must be implemented by all plugins:
protected override void OnQueryReceived(ReportQueryReceivedEventArgs args)
{
// Process query and send results
}- A query matching
SupportedQueriesis received - For
Customqueries,CustomReportIdmatchesSupportedCustomReports - Client is waiting for response
- Return quickly or offload long-running work
- Use async work for long-running queries
| Property | Type | Description |
|---|---|---|
Query |
ReportQuery |
The query to process. Cast to specific type (for example, CustomQuery) as needed. |
MessageId |
int |
Message identifier for this query request. Use with QuerySource to uniquely identify a query. |
QuerySource |
Guid |
Unique identifier of the Application that sent the query. Combined with MessageId to uniquely identify the request. |
DispatchedSystems |
List<Guid> |
Systems the query has been dispatched to. |
Note
MessageId alone is not unique across multiple clients. Always use both MessageId and QuerySource together to identify a specific query request.
When OnQueryReceived() is called, you must:
- Don't block - Return quickly or process asynchronously
-
Send results - Use
Engine.ReportManager.SendQueryResult()for each result batch -
Always complete - Must call
SendQueryCompleted()even if no results - Handle errors gracefully - Catch exceptions and report errors
protected override void OnQueryReceived(ReportQueryReceivedEventArgs args)
{
try
{
// Process query...
ProcessQuery(args.Query);
}
finally
{
// ALWAYS send query completed
Engine.ReportManager.SendQueryCompleted(
args.MessageId,
args.QuerySource,
PluginGuid,
true,
ReportError.None,
string.Empty);
}
}If you don't call SendQueryCompleted(), the client waits until timeout.
Send results in one or more batches, then explicitly signal completion so the client can stop waiting.
flowchart TB
A[OnQueryReceived called] --> B[Process query]
B --> C{More results?}
C -->|Yes| D[SendQueryResult<br/>messageId, results]
D --> C
C -->|No| E[SendQueryCompleted<br/>messageId, ...]
Used to send result data to the client:
var results = new ReportQueryResults(ReportQueryType.DoorActivity)
{
Results = dataSet, // DataSet containing result tables
QuerySource = args.QuerySource,
ResultSource = PluginGuid
};
Engine.ReportManager.SendQueryResult(args.MessageId, results);- Can be called multiple times for the same query (streaming results)
- Each call sends a batch of data
- Client receives results incrementally
- Use appropriate
ReportQueryResultstype for query
Signals that no more results will be sent:
Engine.ReportManager.SendQueryCompleted(
args.MessageId,
args.QuerySource,
PluginGuid,
true,
ReportError.None,
string.Empty
);-
successful- true if query succeeded, false if error -
error-ReportErrorenumeration value -
errorDetail- Optional error details for client
For ReportQueryType.Custom, you must filter by CustomReportId:
public override List<Guid> SupportedCustomReports =>
new List<Guid> { CustomReportPageGuid };
protected override void OnQueryReceived(ReportQueryReceivedEventArgs args)
{
if (args.Query is CustomQuery customQuery)
{
if (customQuery.CustomReportId == CustomReportPageGuid)
{
// Handle this custom report
HandleCustomReport(customQuery);
}
}
Engine.ReportManager.SendQueryCompleted(...);
}If the client report defines a custom ReportFilter, the query also includes the serialized custom filter payload in CustomQuery.FilterData. The payload format is application-defined. Use the same format on the client and server.
- Add
ReportQueryType.CustomtoSupportedQueries - Override
SupportedCustomReportswith your report GUIDs - Check
CustomReportIdinOnQueryReceived() - Read
CustomQuery.FilterDataif the report defines a custom filter - Only process queries matching your GUIDs
Clients can cancel queries that are taking too long, or the Directory may cancel queries that timeout:
protected override void OnQueryCancelled(ReportQueryCancelledEventArgs args)
{
// Cancel any ongoing work for this query
// No need to call SendQueryCompleted() for cancellations
}| Property | Type | Description |
|---|---|---|
MessageId |
int |
Message identifier of the original query request. Use with QueryId to locate the query to cancel. |
QueryId |
Guid |
Unique identifier matching ReportQuery.QueryId of the original query. |
SystemsToCancel |
IEnumerable<Guid> |
External systems for which this query should be cancelled. Check if your PluginGuid is in this list. |
- Cancellations are informational - no response required
- May come from clients or from Directory timeouts
- Use to clean up resources for long-running queries
- Check
SystemsToCancelto see if cancellation applies to your plugin
Don't block the query thread - process asynchronously:
protected override void OnQueryReceived(ReportQueryReceivedEventArgs args)
{
Task.Run(() =>
{
try
{
// Long-running query processing
var results = ProcessQuery(args.Query);
Engine.ReportManager.SendQueryResult(args.MessageId, results);
}
catch (Exception ex)
{
Logger.TraceError(ex, "Query processing failed");
}
finally
{
Engine.ReportManager.SendQueryCompleted(
args.MessageId, args.QuerySource, PluginGuid,
true, ReportError.None, string.Empty);
}
});
}Plugins can respond to any ReportQueryType by adding it to SupportedQueries. Common types include:
| Category | Query Types |
|---|---|
| Access Control |
CardholderActivity, DoorActivity, AreaActivity, ElevatorActivity, CredentialActivity, UnitActivity, ZoneActivity, AccessPointActivity
|
| Video |
CameraEvent, VideoMotionEvent, VideoFile, VideoSequence, ArchiverEvent
|
| Intrusion |
IntrusionAreaActivity, IntrusionUnitActivity, IntrusionDetectionActivity
|
| Health |
Health, HealthEvent, HealthStatistics
|
| LPR |
LprReads, LprHits, PatrollerPositions
|
| Custom |
Custom (requires SupportedCustomReports) |
| Other |
AuditTrails, ActivityTrails, EntityState, Thumbnail
|
See the ReportQueryType enum for the complete list.
For large datasets, structure the response so the client can start processing before the full result set is ready.
For large result sets, send data in chunks:
const int ChunkSize = 1000;
var allResults = GetQueryResults(args.Query);
for (int i = 0; i < allResults.Count; i += ChunkSize)
{
var chunk = allResults.Skip(i).Take(ChunkSize).ToList();
var dataSet = CreateDataSet(chunk);
var results = new ReportQueryResults(args.Query.ReportQueryType)
{
Results = dataSet,
QuerySource = args.QuerySource,
ResultSource = PluginGuid
};
Engine.ReportManager.SendQueryResult(args.MessageId, results);
}- 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.
- About transactions: Batching multiple entity operations into one request.
- 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