When you need to update hundreds or thousands of cards at once, standard individual API calls become inefficient. This guide covers the bulk card operations designed for high-performance mass updates.
| Scenario | Standard API | Bulk API | Recommendation |
|---|---|---|---|
| Update 1-10 cards | ✅ Fast | ⚠️ Overhead | Use standard API |
| Update 10-500 cards | ⚠️ Slow | ✅ Efficient | Use bulk API |
| Update 500+ cards | ❌ Very slow | ✅ Required | Use bulk API |
| Real-time single updates | ✅ Recommended | ❌ Not suitable | Use standard API |
| Scheduled batch jobs | ⚠️ Possible | ✅ Recommended | Use bulk API |
Ordinarily, the Mercury Communication service receives individual data update requests by monitoring ObjectMod events on the MQTT events stream. This approach keeps the cards collection in the database in sync with Mercury Controllers in real-time.
Bulk operations bypass this event-driven approach and directly update cards in batches of 500 cards per call. This significantly reduces:
The ExtendCardExpiry method finds active cards expiring within a date range and extends their expiration to a new date.
Task<CardUpdateResponse> ExtendCardExpiry(
Instance instance, // Target instance
DateTime expiresBefore, // Upper bound: cards expiring before this date
DateTime expiresAfter, // Lower bound: cards expiring after this date
DateTime newExpiresOn, // New expiration date to set
bool trialRun = false // If true, returns count without making changes
);
| Parameter | Description | Example |
|---|---|---|
instance |
The instance containing the cards | await client.GetCurrentInstanceAsync() |
expiresBefore |
Find cards expiring before this date | DateTime.UtcNow.AddDays(60) |
expiresAfter |
Find cards expiring after this date | DateTime.UtcNow |
newExpiresOn |
The new expiration date to assign | DateTime.UtcNow.AddYears(1) |
trialRun |
Dry run mode - returns count only | true for estimation |
// Scenario: Extend active cards expiring in the next 60 days by 1 year
var instance = await client.GetCurrentInstanceAsync();
// Date boundaries
var expiresBefore = DateTime.UtcNow.AddDays(60); // Cards expiring within 60 days
var expiresAfter = DateTime.UtcNow; // But not already expired
var newExpiration = DateTime.UtcNow.AddYears(1); // Extend by 1 year
// Step 1: Estimate total cards to process (trial run)
var estimate = await client.ExtendCardExpiry(
instance,
expiresBefore,
expiresAfter,
newExpiration,
trialRun: true
);
Console.WriteLine($"Cards to process: {estimate.CardsUpdated}");
// Step 2: Process in batches until complete
int totalProcessed = 0;
int batchCount = 0;
while (true)
{
var result = await client.ExtendCardExpiry(
instance,
expiresBefore,
expiresAfter,
newExpiration,
trialRun: false
);
if (result.CardsUpdated == 0)
break;
totalProcessed += result.CardsUpdated;
batchCount++;
Console.WriteLine($"Batch {batchCount}: Updated {result.CardsUpdated} cards");
}
Console.WriteLine($"Complete! Total cards updated: {totalProcessed}");
// With progress callback for UI integration
public async Task ExtendCardsWithProgress(
Client client,
Instance instance,
DateTime expiresBefore,
DateTime expiresAfter,
DateTime newExpiration,
IProgress<int> progress)
{
// Get total for progress calculation
var estimate = await client.ExtendCardExpiry(
instance, expiresBefore, expiresAfter, newExpiration, true);
int total = estimate.CardsUpdated;
int processed = 0;
while (true)
{
var result = await client.ExtendCardExpiry(
instance, expiresBefore, expiresAfter, newExpiration, false);
if (result.CardsUpdated == 0) break;
processed += result.CardsUpdated;
int percentage = (int)((processed / (double)total) * 100);
progress.Report(percentage);
}
}
The DisableActiveCardsAsync method disables active cards for people matching a MongoDB query. This is ideal for compliance scenarios like COVID screening.
Task<CardUpdateResponse> DisableActiveCardsAsync(
Instance instance, // Target instance
string mongoQuery, // MongoDB query to select people
bool trialRun = false // If true, returns count without making changes
);
| Use Case | MongoDB Query |
|---|---|
| Disable all cards | {"_t":"Person"} |
| Exclude management | {"_t":"Person","Tags":{"$ne":"management"}} |
| Specific department | {"_t":"Person","Metadata.Values.Department":"Engineering"} |
| Exclude contractors | {"_t":"Person","Metadata.Values.EmployeeType":{"$ne":"Contractor"}} |
| By custom field | {"_t":"Person","Metadata.Values.CovidCleared":false} |
// Scenario: COVID daily reset - disable all non-management cards
var instance = await client.GetCurrentInstanceAsync();
// MongoDB query: all people except those tagged "management"
var query = @"{'_t':'Person','Tags':{'$ne':'management'}}";
// Step 1: Estimate total
var estimate = await client.DisableActiveCardsAsync(instance, query, trialRun: true);
Console.WriteLine($"People to process: {estimate.CardsUpdated}");
// Step 2: Process in batches
int totalDisabled = 0;
while (true)
{
var result = await client.DisableActiveCardsAsync(instance, query, trialRun: false);
if (result.CardsUpdated == 0)
break;
totalDisabled += result.CardsUpdated;
Console.WriteLine($"Disabled {result.CardsUpdated} cards this batch");
}
Console.WriteLine($"Complete! Total cards disabled: {totalDisabled}");
// Complete COVID screening workflow
public class CovidScreeningService
{
private readonly Client _client;
// Run at end of shift (e.g., 6 PM) to disable all non-exempt cards
public async Task EndOfDayReset()
{
var instance = await _client.GetCurrentInstanceAsync();
// Disable cards for non-management
var query = @"{'_t':'Person','Tags':{'$ne':'management'}}";
int disabled = 0;
while (true)
{
var result = await _client.DisableActiveCardsAsync(instance, query, false);
if (result.CardsUpdated == 0) break;
disabled += result.CardsUpdated;
}
Console.WriteLine($"End of day: Disabled {disabled} cards");
}
// Called when person completes screening the next morning
public async Task PersonCleared(string personKey, InstanceInfo instance)
{
// Get person by href
var personHref = $"/api/f/{instance.Key}/people/{personKey}";
var person = await _client.GetByHrefAsync<PersonInfo>(personHref);
// Re-enable their active card assignments
foreach (var card in person.CardAssignments.Where(c => c.IsDisabled))
{
card.IsDisabled = false;
await _client.UpdateCardAssignmentAsync(person, card);
}
}
}
| Practice | Reason |
|---|---|
| ✅ Use trial run first | Know what you’re about to change |
| ✅ Run batches sequentially | Parallel calls may cause conflicts |
| ✅ Log batch results | Audit trail and debugging |
| ✅ Test on small dataset | Validate query before bulk operation |
| ✅ Schedule during low-traffic periods | Reduce impact on real-time access |
| Anti-Pattern | Problem |
|---|---|
| ❌ Run batches in parallel | Database contention, potential data corruption |
| ❌ Skip trial run | May affect more cards than expected |
| ❌ Run during peak hours | May impact access control performance |
| ❌ Use overly broad queries | Accidental mass disabling |
try
{
while (true)
{
var result = await client.ExtendCardExpiry(
instance, expiresBefore, expiresAfter, newExpiration, false);
if (result.CardsUpdated == 0) break;
// Success logging
_logger.LogInformation("Batch complete: {Count} cards updated", result.CardsUpdated);
}
}
catch (KeepException ex) when (ex.Code == KeepExceptionCode.PermissionsError)
{
_logger.LogError("Insufficient permissions for bulk card update");
throw;
}
catch (KeepException ex) when (ex.Code == KeepExceptionCode.QueryTooLarge)
{
_logger.LogError("Query matched too many results - narrow your criteria");
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error during bulk card update");
throw;
}