Bulk Card Operations

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.


Overview

When to Use Bulk Operations

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

How Bulk Operations Work

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:

  • Network overhead and system latency
  • Database transaction count
  • Controller synchronization time

Extend Card Expiration

The ExtendCardExpiry method finds active cards expiring within a date range and extends their expiration to a new date.

Method Signature

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
);

Parameters Explained

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

Usage Pattern

// 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}");

Progress Reporting Example

// 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);
    }
}

Disable Active Cards

The DisableActiveCardsAsync method disables active cards for people matching a MongoDB query. This is ideal for compliance scenarios like COVID screening.

Method Signature

Task<CardUpdateResponse> DisableActiveCardsAsync(
    Instance instance,    // Target instance
    string mongoQuery,    // MongoDB query to select people
    bool trialRun = false // If true, returns count without making changes
);

Common Use Cases

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}

Usage Pattern

// 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}");

Daily Screening Workflow

// 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);
        }
    }
}

Best Practices

Do’s

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

Don’ts

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

Error Handling

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;
}

See Also