Skip to content

platform sdk transactions

Andre Lafleur edited this page Dec 11, 2025 · 2 revisions

About transactions

Transactions in the Genetec SDK are a local batching mechanism that groups multiple entity operations together and sends them to Security Center as a single request. This provides significant performance benefits and ensures that related operations either all succeed together or all fail together.

Think of transactions like filling an online shopping cart before checking out, rather than buying items one at a time. Each item you add to the cart is stored locally, and only when you "checkout" (commit) does the actual purchase (server communication) happen.

Understanding transactions

SDK transactions work differently from traditional database transactions. They are primarily a performance optimization tool that batches operations:

Without transactions:

// Each operation results in a separate server call
var cardholder = engine.CreateEntity("John Doe", EntityType.Cardholder); // Server call #1
cardholder.FirstName = "John";                                           // Server call #2  
cardholder.LastName = "Doe";                                             // Server call #3
cardholder.EmailAddress = "john@example.com";                            // Server call #4
// Total: 4 round-trips to server

With transactions:

engine.TransactionManager.ExecuteTransaction(() =>
{
    var cardholder = engine.CreateEntity("John Doe", EntityType.Cardholder);
    cardholder.FirstName = "John";
    cardholder.LastName = "Doe";
    cardholder.EmailAddress = "john@example.com";
    // Total: 1 round-trip to server
});

How Transactions Work

  1. Start Transaction: SDK prepares to collect entity operations locally
  2. Accumulate Operations: Entity changes are stored in memory, visible immediately in your application
  3. Commit Transaction: All accumulated operations are sent to the server in one batch
  4. Server Processing: Security Center applies all changes atomically - either all succeed or all fail

Global Serialization

Manual transactions (using CreateTransaction() and CommitTransaction()) are globally serialized across your entire application through a synchronization lock. This means only one manual transaction can be active at any time, regardless of how many threads your application has. When Thread A starts a manual transaction, Thread B attempting to start a manual transaction will block and wait until Thread A completes.

ExecuteTransaction() also uses this same lock internally, but releases it upon completion. Multiple ExecuteTransaction() calls don't necessarily block each other unless they overlap in time.

This serialization prevents data corruption by ensuring that multiple threads cannot create conflicting changes to related entities simultaneously. While this might seem like a performance limitation, it's actually a critical safety feature.

When and Why to Use Transactions

Performance Benefits

The performance impact of transactions is based on the number of requests to the directory server. Each property change that results in a database update equals one request.

Understanding Requests to Directory Server:

Without transactions, every update to an entity is sent immediately to the Directory Server as a separate request:

var cardholder = engine.CreateEntity("John", EntityType.Cardholder); 
// → Sent to Directory Server immediately (Request #1)

cardholder.FirstName = "John";                                       
// → Sent to Directory Server immediately (Request #2)

cardholder.LastName = "Doe";                                         
// → Sent to Directory Server immediately (Request #3)

cardholder.EmailAddress = "john@example.com";                        
// → Sent to Directory Server immediately (Request #4)

partition.AddMember(cardholder);                                     
// → Sent to Directory Server immediately (Request #5)
// Total: 5 requests to Directory Server

With transactions, all updates are collected locally and sent as one batch when you commit:

engine.TransactionManager.ExecuteTransaction(() =>
{
    var cardholder = engine.CreateEntity("John", EntityType.Cardholder); // Stored locally
    cardholder.FirstName = "John";                                       // Stored locally
    cardholder.LastName = "Doe";                                         // Stored locally
    cardholder.EmailAddress = "john@example.com";                        // Stored locally
    partition.AddMember(cardholder);                                     // Stored locally
    // All operations sent together as 1 request when transaction commits
});
// Total: 1 request to Directory Server

Performance Impact:

  • Few requests (1-5): Minor performance improvement
  • Medium requests (10-50): Significant performance improvement
  • Many requests (100+): Major performance improvement

No Transaction Overhead: Creating a transaction is essentially free, it just tells the SDK to start collecting operations in memory instead of sending them immediately. The real work happens when you commit, which sends all the collected operations to the Directory Server in one efficient batch.

Data Consistency

Transactions ensure that related operations either all succeed together or all fail together. This prevents scenarios where partial updates leave your data in an inconsistent state.

Example scenario: Creating a cardholder and their access permissions

  • Without transactions: If creating the access rule fails, you're left with a cardholder who has no access
  • With transactions: If any part fails, no cardholder is created at all

When to Use Transactions

Use transactions when:

  • Creating multiple related entities that should exist together
  • Performing batch operations (creating many entities of the same type)
  • Any scenario where partial success would be problematic
  • You want to optimize performance for multiple entity operations

Transactions are optional: You can create, update, and delete entities without using transactions. The choice depends on your performance and consistency requirements.

Transaction Approaches

ExecuteTransaction Methods (Recommended)

The ExecuteTransaction and ExecuteTransactionAsync methods handle the transaction lifecycle automatically and are recommended for most scenarios.

Basic Synchronous Usage:

engine.TransactionManager.ExecuteTransaction(() =>
{
    var cardholder = (Cardholder)engine.CreateEntity("John Doe", EntityType.Cardholder);
    cardholder.FirstName = "John";
    cardholder.LastName = "Doe";
    cardholder.EmailAddress = "john@example.com";
});

Asynchronous Usage:

await engine.TransactionManager.ExecuteTransactionAsync(() =>
{
    var cardholder = (Cardholder)engine.CreateEntity("Jane Doe", EntityType.Cardholder);
    cardholder.FirstName = "Jane";
    cardholder.LastName = "Doe";
    cardholder.EmailAddress = "jane@example.com";
});

Returning Values:

var cardholderGuid = engine.TransactionManager.ExecuteTransaction(() =>
{
    var cardholder = (Cardholder)engine.CreateEntity("Bob Smith", EntityType.Cardholder);
    cardholder.FirstName = "Bob";
    cardholder.LastName = "Smith";
    return cardholder.Guid;
});

Benefits of ExecuteTransaction:

  • Automatic transaction lifecycle management
  • Built-in exception handling with automatic rollback
  • Thread-safe operation
  • Works correctly in both plugin and external application contexts
  • Prevents common mistakes like forgetting to commit or rollback

Manual Transaction Control

Manual transactions give you explicit control over the transaction lifecycle but require careful error handling and threading considerations.

Basic Manual Transaction:

engine.TransactionManager.CreateTransaction();
try
{
    var cardholder = (Cardholder)engine.CreateEntity("Alice Johnson", EntityType.Cardholder);
    cardholder.FirstName = "Alice";
    cardholder.LastName = "Johnson";
    cardholder.EmailAddress = "alice@example.com";
    
    // Default behavior: rollBackOnFailure is true
    engine.TransactionManager.CommitTransaction();
}
catch (Exception ex)
{
    // With default rollBackOnFailure: true, rollback already happened automatically
    logger.LogError(ex, "Transaction failed: {Message}", ex.Message);
    throw;
}

Understanding rollBackOnFailure Parameter:

// These are equivalent (true is the default)
engine.TransactionManager.CommitTransaction();
engine.TransactionManager.CommitTransaction(rollBackOnFailure: true);

// With rollBackOnFailure: true (default):
// If CommitTransaction throws an exception, RollbackTransaction is called automatically

// With rollBackOnFailure: false:
// You must manually handle rollback in your catch block
engine.TransactionManager.CommitTransaction(rollBackOnFailure: false);

When You Need Manual Rollback: Manual rollback is only needed in two scenarios:

  1. When using rollBackOnFailure: false (rarely needed)
  2. When you want to abort before committing (business logic decision)
engine.TransactionManager.CreateTransaction();
try
{
    var cardholder = (Cardholder)engine.CreateEntity("John Doe", EntityType.Cardholder);
    
    // Check some business condition
    if (SomeBusinessConditionFails())
    {
        // Decide to abort before even attempting commit
        engine.TransactionManager.RollbackTransaction();
        return;
    }
    
    engine.TransactionManager.CommitTransaction(); // Automatic rollback on failure
}
catch (Exception ex)
{
    // Rollback already handled automatically
    throw;
}

Participating Transactions (Not True Nesting)

When you call ExecuteTransaction from within another ExecuteTransaction, the inner call participates in the existing transaction rather than creating a new nested transaction. This is not true nesting like SQL Server supports - it's all one single transaction.

engine.TransactionManager.ExecuteTransaction(() =>
{
    // Outer transaction
    var partition = (Partition)engine.CreateEntity("Engineering", EntityType.Partition);
    
    // This call participates in the same transaction
    CreateUsersInPartition(partition);
    
    // Everything commits together as one atomic operation
});

void CreateUsersInPartition(Partition partition)
{
    // This automatically participates in the outer transaction if one exists
    engine.TransactionManager.ExecuteTransaction(() =>
    {
        var user1 = (Cardholder)engine.CreateEntity("User 1", EntityType.Cardholder);
        var user2 = (Cardholder)engine.CreateEntity("User 2", EntityType.Cardholder);
        
        partition.AddMember(user1);
        partition.AddMember(user2);
    });
}

Threading Model and Application Types

Understanding the threading model is crucial for using transactions correctly, as the behavior differs significantly between plugins and external applications.

Transaction State and Thread Affinity

Per-Thread Transaction State: The IsTransactionActive property returns different values for different threads. Each thread tracks its own transaction state independently.

// Thread A
engine.TransactionManager.CreateTransaction();
Console.WriteLine(engine.TransactionManager.IsTransactionActive); // True on Thread A

// Thread B (simultaneously) 
Console.WriteLine(engine.TransactionManager.IsTransactionActive); // False on Thread B

Global Serialization: Despite per-thread state tracking, manual transactions are globally serialized. Only one manual transaction can be active across the entire application at any time. When Thread A starts a manual transaction, Thread B attempting to start a manual transaction will block until Thread A completes.

Thread Affinity for Manual Transactions: Manual transactions must start and complete on the same thread. Attempting to commit or rollback a transaction from a different thread than the one that started it will result in synchronization exceptions.

Plugin Applications vs External Applications

Plugin Applications (Custom Roles): Plugins run inside the Security Center process and have specific threading requirements:

  • EngineThread Requirement: All entity operations must execute on the EngineThread
  • Thread Detection: Use engine.IsEngineThread to check if you're on the correct thread (returns true only when actually on EngineThread)
  • Automatic Threading: ExecuteTransactionAsync automatically ensures operations run on the EngineThread
  • Alternative Methods: QueueUpdate and QueueUpdateAndWait can execute operations on EngineThread without automatic transaction creation

External Applications (Console, WPF, WinForms, Windows Service, ASP.NET): External applications have more threading flexibility:

  • Any Thread: Can use both ExecuteTransaction and ExecuteTransactionAsync on any thread
  • Manual Transactions: Work on any thread, but must stay on the same thread throughout the transaction lifecycle
  • Thread Detection: engine.IsEngineThread checks if on the proxy thread; in external apps, this depends on the proxy thread registration, but is generally not a concern for threading decisions
  • Full Control: Complete control over threading model

QueueUpdate vs ExecuteTransactionAsync

Understanding when to use these different methods is important for plugin development:

ExecuteTransactionAsync:

  • EngineThread execution + automatic transaction management
  • Use when you want both proper threading and transaction benefits

QueueUpdate:

  • EngineThread execution + no automatic transaction
  • Use when you want EngineThread execution but want to manage transactions manually or don't need transactions

QueueUpdateAndWait:

  • Same as QueueUpdate but blocks until completion
  • Use when you need synchronous execution on EngineThread
// In a plugin - ExecuteTransactionAsync (most common)
await engine.TransactionManager.ExecuteTransactionAsync(() =>
{
    var cardholder = (Cardholder)engine.CreateEntity("Name", EntityType.Cardholder);
});

// In a plugin - QueueUpdate without transaction (for single operations)
engine.TransactionManager.QueueUpdate(() =>
{
    var cardholder = (Cardholder)engine.CreateEntity("Name", EntityType.Cardholder);
    // Single operation sent to server immediately
});

// In a plugin - QueueUpdate with manual transaction (advanced scenarios)
engine.TransactionManager.QueueUpdate(() =>
{
    engine.TransactionManager.CreateTransaction();
    try
    {
        var cardholder1 = (Cardholder)engine.CreateEntity("Name1", EntityType.Cardholder);
        var cardholder2 = (Cardholder)engine.CreateEntity("Name2", EntityType.Cardholder);
        engine.TransactionManager.CommitTransaction();
    }
    catch
    {
        // Rollback handled automatically with default rollBackOnFailure: true
        throw;
    }
});

Rules and Restrictions

What You Can Do Inside Transactions

Entity Operations (Primary Purpose):

  • Create entities: engine.CreateEntity("Name", EntityType.Cardholder)
  • Update entity properties: cardholder.FirstName = "John"
  • Delete entities

Acceptable Supporting Operations:

  • Simple logging for debugging purposes
  • Fast calculations and data transformations
  • Conditional logic and loops for entity processing
  • Memory operations and variable assignments

What You Must Never Do Inside Transactions

I/O Operations:

  • File system access (reading or writing files)
  • Network requests or HTTP calls
  • Database calls to external databases
  • Any operation that accesses external resources

Async Operations:

  • Never use await keywords inside transactions (breaks thread affinity)
  • No Task.Delay() or any delay operations

Blocking Operations:

  • Thread.Sleep() calls

  • Waiting on external events or signals

  • Any operation that could block indefinitely

Query Operations:

  • Never execute queries inside transactions, there is no point in doing so
  • Queries should be executed before starting transactions or after committing them

Why These Restrictions Exist

These restrictions exist because:

  1. Global Serialization: Transactions hold locks that prevent other transactions from running. Any slow operation affects your entire application's transaction throughput.

  2. Thread Affinity: Manual transactions must complete on the same thread they started on. Async operations can cause thread switching.

  3. Performance Impact: Long-running operations inside transactions create bottlenecks for all other transaction operations.

  4. Design Purpose: The transaction system is specifically optimized for fast, local entity operations, not general-purpose operations.

Practical Examples

Creating Multiple Related Entities

engine.TransactionManager.ExecuteTransaction(() =>
{
    // Create a cardholder
    var cardholder = (Cardholder)engine.CreateEntity("John Doe", EntityType.Cardholder);
    cardholder.FirstName = "John";
    cardholder.LastName = "Doe";
    cardholder.EmailAddress = "john@example.com";

    // Create a partition for organization
    var partition = (Partition)engine.CreateEntity("Engineering Team", EntityType.Partition);
    
    // Add cardholder to partition
    partition.AddMember(cardholder);
    
    // All operations succeed together or fail together
});

Batch Creating Multiple Entities

await engine.TransactionManager.ExecuteTransactionAsync(() =>
{
    // Create multiple cardholders in one transaction
    for (int i = 1; i <= 10; i++)
    {
        var cardholder = (Cardholder)engine.CreateEntity($"User {i}", EntityType.Cardholder);
        cardholder.FirstName = "User";
        cardholder.LastName = i.ToString();
        cardholder.EmailAddress = $"user{i}@example.com";
    }
    
    // Much faster than 10 individual operations
});

Conditional Operations Within Transactions

engine.TransactionManager.ExecuteTransaction(() =>
{
    var cardholder = (Cardholder)engine.CreateEntity("Jane Smith", EntityType.Cardholder);
    cardholder.FirstName = "Jane";
    cardholder.LastName = "Smith";
    cardholder.EmailAddress = "jane@example.com";
    
    // Conditional logic based on data is fine
    if (cardholder.EmailAddress.EndsWith("@company.com"))
    {
        var employeePartition = (Partition)engine.CreateEntity("Employees", EntityType.Partition);
        employeePartition.AddMember(cardholder);
        
        logger.LogDebug("Created employee cardholder {Name}", cardholder.Name);
    }
});

Error Handling

Understanding the specific exceptions that can occur during transaction operations helps you build robust applications.

Exceptions from CommitTransaction

Based on the SDK implementation, CommitTransaction() can throw these specific exceptions:

Before attempting commit:

  • SdkException(SdkError.NoActiveTransaction) - No transaction is currently active

During commit (server communication errors):

  • SecurityException - Insufficient privileges for the operations
  • SdkException(SdkError.LicenseCountExceeded) - Operations would exceed license limits
  • SdkException(SdkError.RecursionViolation) - Operations would cause a recursion violation
  • SdkException(SdkError.UnsufficientPrivilege) - User lacks required privileges for entity or field update
  • SdkException(SdkError.TransactionFailed) - General server error (default case)

Example

try
{
    engine.TransactionManager.ExecuteTransaction(() =>
    {
        var cardholder = (Cardholder)engine.CreateEntity("Test User", EntityType.Cardholder);
        cardholder.FirstName = "Test";
        cardholder.LastName = "User";
        cardholder.EmailAddress = "test@example.com";
    });
    
    logger.LogInformation("Transaction completed successfully");
}
catch (SecurityException ex)
{
    logger.LogError(ex, "Insufficient privileges for transaction");
}
catch (SdkException ex)
{
    logger.LogError(ex, "SDK transaction failed: {ErrorMessage}", ex.Message);
    
    switch (ex.ErrorCode)
    {
        case SdkError.LicenseCountExceeded:
            logger.LogError("License limit exceeded");
            break;

        case SdkError.RecursionViolation:
            logger.LogError("Transaction caused a recursion violation");
            break;

        case SdkError.UnsufficientPrivilege:
            logger.LogError("User lacks required privileges");
            break;

        case SdkError.TransactionFailed:
            logger.LogError("General transaction failure");
            break;

        default:
            logger.LogError("Unknown SDK error: {ErrorCode}", ex.ErrorCode);
            break;
    }
}
catch (Exception ex)
{
    logger.LogError(ex, "Unexpected error during transaction");
    throw;
}

Manual Transaction Error Handling

engine.TransactionManager.CreateTransaction();
try
{
    var cardholder = (Cardholder)engine.CreateEntity("Manual Transaction User", EntityType.Cardholder);
    cardholder.FirstName = "Manual";
    cardholder.LastName = "User";
    
    // Default rollBackOnFailure: true means automatic rollback on commit failure
    engine.TransactionManager.CommitTransaction();
}
catch (SdkException ex)
{
    // Rollback already happened automatically due to default rollBackOnFailure: true
    logger.LogError(ex, "Transaction failed and was automatically rolled back");
    throw;
}
catch (Exception ex)
{
    // For non-SDK exceptions, check if rollback is needed
    if (engine.TransactionManager.IsTransactionActive)
    {
        logger.LogWarning("Manual rollback needed for non-SDK exception");
        engine.TransactionManager.RollbackTransaction();
    }
    
    logger.LogError(ex, "Unexpected error in manual transaction");
    throw;
}

Security Center SDK


Macro SDK Developer Guide


Web SDK Developer Guide

  • Getting Started Setup, authentication, and basic configuration for the Web SDK.
  • Referencing Entities Entity discovery, search capabilities, and parameter formats.
  • Entity Operations CRUD operations, multi-value fields, and method execution.
  • Partitions Managing partitions, entity membership, and user access control.
  • Custom Fields Creating, reading, writing, and filtering custom entity fields.
  • Custom Card Formats Managing custom credential card format definitions.
  • Actions Control operations for doors, cameras, macros, and notifications.
  • Events and Alarms Real-time event monitoring, alarm monitoring, and custom events.
  • Incidents Incident management, creation, and attachment handling.
  • Reports Activity reports, entity queries, and historical data retrieval.
  • Performance Guide Optimization tips and best practices for efficient API usage.
  • Reference Entity GUIDs, EntityType enumeration, and EventType enumeration.
  • Under the Hood Technical architecture, query reflection, and SDK internals.
  • Troubleshooting Common error resolution and debugging techniques.

Media Gateway Developer Guide


Web Player Developer Guide

Clone this wiki locally