platform sdk transactions - Genetec/DAP GitHub Wiki
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 = "[email protected]"; // 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 = "[email protected]";
// Total: 1 round-trip to server
});
How Transactions Work
- Start Transaction: SDK prepares to collect entity operations locally
- Accumulate Operations: Entity changes are stored in memory, visible immediately in your application
- Commit Transaction: All accumulated operations are sent to the server in one batch
- 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 = "[email protected]";
// → 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 = "[email protected]"; // 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 = "[email protected]";
});
Asynchronous Usage:
await engine.TransactionManager.ExecuteTransactionAsync(() =>
{
var cardholder = (Cardholder)engine.CreateEntity("Jane Doe", EntityType.Cardholder);
cardholder.FirstName = "Jane";
cardholder.LastName = "Doe";
cardholder.EmailAddress = "[email protected]";
});
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 = "[email protected]";
// 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:
- When using rollBackOnFailure: false (rarely needed)
- 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.IsEngineThreadto check if you're on the correct thread (returns true only when actually on EngineThread) - Automatic Threading:
ExecuteTransactionAsyncautomatically ensures operations run on the EngineThread - Alternative Methods:
QueueUpdateandQueueUpdateAndWaitcan 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
ExecuteTransactionandExecuteTransactionAsyncon any thread - Manual Transactions: Work on any thread, but must stay on the same thread throughout the transaction lifecycle
- Thread Detection:
engine.IsEngineThreadchecks 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
awaitkeywords 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:
-
Global Serialization: Transactions hold locks that prevent other transactions from running. Any slow operation affects your entire application's transaction throughput.
-
Thread Affinity: Manual transactions must complete on the same thread they started on. Async operations can cause thread switching.
-
Performance Impact: Long-running operations inside transactions create bottlenecks for all other transaction operations.
-
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 = "[email protected]";
// 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 = "[email protected]";
// 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 operationsSdkException(SdkError.LicenseCountExceeded)- Operations would exceed license limitsSdkException(SdkError.RecursionViolation)- Operations would cause a recursion violationSdkException(SdkError.UnsufficientPrivilege)- User lacks required privileges for entity or field updateSdkException(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 = "[email protected]";
});
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;
}