Table of Contents


Deleting Records (DELETE Operations)


Summary

This section covers all methods for deleting records from the database, including soft delete (recycle bin), permanent delete, bulk operations, understanding DeleteResult, cascading deletes, and restore operations.

Understanding Delete Behaviour

Magentrix supports two types of delete operations:

Soft Delete (Default):

  • Records are marked as deleted and moved to the recycle bin
  • Records remain in the database with IsDeleted = true
  • Records can be restored within a retention period
  • Related records may be affected based on relationship configuration
  • Soft-deleted records don't appear in standard queries

Permanent Delete:

  • Records are permanently removed from the database
  • Records cannot be restored
  • Use with extreme caution
  • Requires explicit PermanentDelete = true option

 

Note: Soft-delete mechanism only applies to Magentrix native entity records, not records that are mirrored from external CRMs. Refer to the retention policy of those systems when it comes to record deletions.


Single Record Delete

Use Database.Delete() to delete a single record.

Delete by ID:

// Soft delete by ID
Database.Delete(accountId);

Delete by Object:

// Retrieve and delete
var account = Database.Retrieve<Account>(accountId);

if (account != null)
    Database.Delete(account);

Delete with Validation:

public void DeleteAccountSafely(string accountId)
{
    // Retrieve account
    var account = Database.Retrieve<Account>(accountId);
    
    if (account == null)
    {
        SystemInfo.Debug("Account not found");
        return;
    }
    
    // Check if account can be deleted
    var contactCount = Database.Count<Contact>(f => f.AccountId == accountId);
    
    if (contactCount > 0)
    {
        SystemInfo.Debug($"Cannot delete account: {contactCount} related contacts exist");
        return;
    }
    
    // Proceed with delete
    Database.Delete(account);
}

Conditional Delete:

// Delete only if the condition is met
var account = Database.Retrieve<Account>(accountId);

if (account.Status__c == "Inactive" && account.LastActivityDate__c < DateTime.UtcNow.AddYears(-2))
    Database.Delete(account);
else
    SystemInfo.Debug("Account does not meet deletion criteria");

Bulk Delete

Always use bulk delete methods when removing multiple records. Never perform database operations in loops.

Basic Bulk Delete:

// Query records to delete
var accounts = Database.Query<Account>()
    .Where(f => f.Status__c == "Closed" && f.LastActivityDate__c < DateTime.UtcNow.AddYears(-3))
    .ToList();


// Single bulk delete, no need to check if the account List has entries or not
Database.Delete(accounts);

Bulk Delete with Error Handling:

var accountsToDelete = Database.Query<Account>()
    .Where(f => f.Type == "Test" && f.CreatedOn < DateTime.UtcNow.AddDays(-30))
    .ToList();

var failures = new List<string>();

try
{
    Database.Delete(accountsToDelete);
}
catch (DatabaseException ex)
{
    foreach (var err in ex.Errors)
    {
        var account = accountsToDelete[err.Index];
        failures.Add($"{account.Name} (ID: {account.Id}): {err.Message}");
    }
}

if (failures.Count > 0)
{
    SystemInfo.Debug($"Delete completed with {failures.Count} errors:");
    foreach (var failure in failures)
    {
        SystemInfo.Debug($"  {failure}");
    }
}

Bulk Delete Pattern:

// ✅ GOOD: Query once, delete once
var contacts = Database.Query<Contact>()
    .Where(f => f.AccountId == accountId)
    .ToList();

Database.Delete(contacts);

// ❌ BAD: Query and delete in a loop
var contactIds = GetContactIds();

foreach (var contactId in contactIds)
{
    // Multiple queries
    var contact = Database.Retrieve<Contact>(contactId);  
    
    // Multiple deletes
    Database.Delete(contact);  
}

Large Bulk Delete (Batching):

public void BulkDeleteRecords(List<string> recordIds)
{
    int batchSize = 200;
    int totalBatches = (int)Math.Ceiling((double)recordIds.Count / batchSize);
    int totalDeleted = 0;
    int totalErrors = 0;

    for (int i = 0; i < totalBatches; i++)
    {
        var batchIds = recordIds.Skip(i * batchSize).Take(batchSize).ToList();

        // Load the records so Delete can operate on them as a batch.
        var batch = Database.Query<dbObject>()
            .Where(r => batchIds.Contains(r.Id))
            .ToList();

        int errorCount = 0;

        try
        {
            Database.Delete(batch);
        }
        catch (DatabaseException ex)
        {
            errorCount = ex.Errors.Count;
            foreach (var err in ex.Errors)
            {
                var failedRecord = batch[err.Index];
                Logger.Warn($"Delete failed for {failedRecord.GetType().Name} Id={failedRecord.Id}: {err.Message}");
            }
        }

        int successCount = batch.Count - errorCount;
        totalDeleted += successCount;
        totalErrors += errorCount;

        SystemInfo.Debug($"Batch {i + 1}/{totalBatches}: {successCount} deleted, {errorCount} errors");
    }

    SystemInfo.Debug($"Total: {totalDeleted} deleted, {totalErrors} errors");
}

Delete with Related Records:

public void DeleteAccountWithRelatedRecords(string accountId)
{
    // Delete related contacts first
    var contacts = Database.Query<Contact>()
        .Where(f => f.AccountId == accountId)
        .ToList();

    if (contacts.Count > 0)
    {
        int contactErrors = 0;
        try
        {
            Database.Delete(contacts);
        }
        catch (DatabaseException ex)
        {
            contactErrors = ex.Errors.Count;
            foreach (var err in ex.Errors)
            {
                var failed = contacts[err.Index];
                Logger.Warn($"Delete failed for Contact Id={failed.Id}: {err.Message}");
            }
        }
        SystemInfo.Debug($"Deleted {contacts.Count - contactErrors} related contacts");
    }

    // Delete related opportunities
    var opportunities = Database.Query<Opportunity>()
        .Where(f => f.AccountId == accountId)
        .ToList();

    if (opportunities.Count > 0)
    {
        int oppErrors = 0;
        try
        {
            Database.Delete(opportunities);
        }
        catch (DatabaseException ex)
        {
            oppErrors = ex.Errors.Count;
            foreach (var err in ex.Errors)
            {
                var failed = opportunities[err.Index];
                Logger.Warn($"Delete failed for Opportunity Id={failed.Id}: {err.Message}");
            }
        }
        SystemInfo.Debug($"Deleted {opportunities.Count - oppErrors} related opportunities");
    }

    // Finally delete account
    var account = Database.Query<Account>()
        .Where(f => f.Id == accountId)
        .FirstOrDefault();

    if (account != null)
    {
        try
        {
            Database.Delete(new List<Account> { account });
            SystemInfo.Debug("Account and all related records deleted");
        }
        catch (DatabaseException ex)
        {
            foreach (var err in ex.Errors)
            {
                Logger.Warn($"Delete failed for Account Id={account.Id}: {err.Message}");
            }
        }
    }
}

Permanent Delete

Permanent delete removes records completely from the database without moving to recycle bin.

Basic Permanent Delete:

// Permanently delete record (cannot be restored)
var result = Database.Delete(accountId, new DatabaseOptions 
{ 
    PermanentDelete = true
});
Warning: Permanent deletion cannot be undone. Always use with extreme caution.

Permanent Delete with Confirmation:

public void PermanentlyDeleteAccount(string accountId, bool confirmed)
{
    // UI confirmation obtained
    if (!confirmed)
    {
        SystemInfo.Debug("Permanent delete requires confirmation");
        return null;
    }
    
    var account = Database.Retrieve<Account>(accountId);
    
    if (account == null)
    {
        SystemInfo.Debug("Account not found");
        return null;
    }
    
    // Log permanent delete if necessary
    SystemInfo.Debug($"WARNING: Permanently deleting account '{account.Name}' (ID: {accountId})");
    
    Database.Delete(accountId, new DatabaseOptions 
    { 
        PermanentDelete = true,
    });
}

Bulk Permanent Delete:

// Permanently delete test records
var testRecords = Database.Query<Account>()
    .Where(f => f.Type == "Test" && f.CreatedOn < DateTime.UtcNow.AddDays(-90))
    .ToList();


Database.Delete(testRecords, new DatabaseOptions 
{ 
   PermanentDelete = true
});

When to Use Permanent Delete:

  • Removing test data from production
  • Cleaning up duplicate records
  • Complying with data retention policies (GDPR, etc.)
  • Purging old archived data
  • Removing records that should never be restored

When NOT to Use Permanent Delete:

  • Standard record deletion (use soft delete)
  • Records that may need to be restored
  • Records with audit requirements
  • Unless specifically required by business logic

Error Handling

Proper error handling for delete operations.

Exception Mode (default):

try
{
    Database.Delete(accountId);
}
catch (DatabaseException ex)
{
    SystemInfo.Debug($"Delete failed: {ex.Message}");
}

 

Common Delete Errors:

// Record not found
Database.Delete("INVALID_ID");
// Error: Record does not exist

// Insufficient permissions
Database.Delete(recordId);
// Error: User does not have delete permission

// Record already deleted
Database.Delete(deletedRecordId);
// Error: Record is already deleted

Controller Delete Error Handling:

[HttpPost]
public ActionResponse DeleteAccount(string id)
{
    var account = Database.Retrieve<Account>(id);
    
    if (account == null)
    {
        AspxPage.AddErrorMessage("Account not found");
        return View();
    }
    
    // Check dependencies
    var contactCount = Database.Count<Contact>(f => f.AccountId == id);
    
    if (contactCount > 0)
    {
        AspxPage.AddErrorMessage($"Cannot delete account: {contactCount} related contacts must be removed first");
        return View();
    }
    
    // Attempt delete
    Database.Delete(account);
    
    return View();
}

Restore Operations

Restore soft-deleted records from the recycle bin.

Basic Restore:

// Restore deleted record
Database.Restore(accountId);
SystemInfo.Debug("Account restored from recycle bin");

Query and Restore:

// Find deleted record
var deletedAccount = Database.QueryAll<Account>()
    .Where(f => f.Email__c == "restore@example.com" && f.IsDeleted == true)
    .FirstOrDefault();

if (deletedAccount != null)
{
    Database.Restore(deletedAccount.Id);
    SystemInfo.Debug($"Restored account: {deletedAccount.Name}");
}
else
    SystemInfo.Debug("No deleted account found with that email");

Bulk Restore:

// Find all deleted accounts from a specific date
var deletedAccounts = Database.QueryAll<Account>()
    .Where(f => 
        f.IsDeleted == true && 
        f.DeletedOn >= DateTime.UtcNow.AddDays(-7)
    )
    .ToList();

foreach (var account in deletedAccounts)
{
    Database.Restore(account.Id);
    SystemInfo.Debug($"Restored: {account.Name}");
}

SystemInfo.Debug($"Restored {deletedAccounts.Count} accounts");

Conditional Restore:

public void RestoreRecentlyDeletedAccounts()
{
    // Restore accounts deleted in last 24 hours
    var yesterday = DateTime.UtcNow.AddDays(-1);
    
    var recentlyDeleted = Database.QueryAll<Account>()
        .Where(f => f.IsDeleted == true && f.DeletedOn >= yesterday)
        .ToList();
    
    foreach (var account in recentlyDeleted)
    {
        // Check if restore is appropriate
        if (account.Type == "Partner" || account.Type == "Customer")
        {
            Database.Restore(account.Id);
            SystemInfo.Debug($"Restored: {account.Name}");
        }
    }
}

Restore with Related Records:

public void RestoreAccountWithContacts(string accountId)
{
    // Restore account first
    Database.Restore(accountId);
    SystemInfo.Debug("Account restored");
    
    // Find and restore related contacts
    var deletedContacts = Database.QueryAll<Contact>()
        .Where(f => f.AccountId == accountId && f.IsDeleted == true)
        .ToList();
    
    foreach (var contact in deletedContacts)
    {
        Database.Restore(contact.Id);
        SystemInfo.Debug($"Restored contact: {contact.Name}");
    }
    
    SystemInfo.Debug($"Restored account and {deletedContacts.Count} related contacts");
}

Restore Error Handling:

public void RestoreSafely(string recordId)
{
    try
    {
        // Check if record exists and is deleted
        var record = Database.Retrieve(recordId);
        
        if (record == null)
        {
            // Try to find in deleted records
            var deletedRecord = Database.QueryAll<dbObject>()
                .Where(f => f.Id == recordId && f.IsDeleted == true)
                .FirstOrDefault();
            
            if (deletedRecord != null)
            {
                Database.Restore(recordId);
                SystemInfo.Debug("Record restored successfully");
            }
            else
                SystemInfo.Debug("Record not found in recycle bin");
        }
        else
            SystemInfo.Debug("Record is not deleted");
    }
    catch (DatabaseException ex)
    {
        SystemInfo.Debug($"Restore failed: {ex.Message}");
    }
}

Delete Options (DatabaseOptions)

DatabaseOptions controls the delete operation behaviour.

// Throw exceptions on error (default) 
try
{ 
   Database.Delete(accountId);
}
catch (DatabaseException ex) 
{ 
   // Handle exception
}


// Return errors in result
var result = Database.Delete(accountId, new DatabaseOptions { IsApiMode = true });

if (result.HasError)
{
    // Handle errors in result
}

PermanentDelete:

// Soft delete (default) - moves to recycle bin
Database.Delete(accountId);

// Permanent delete - removes completely
Database.Delete(accountId, new DatabaseOptions 
{ 
    PermanentDelete = true
});

SystemMode:

// Delete with system privileges
Database.Delete(accountId, new DatabaseOptions 
{ 
    SystemMode = true
});
Warning: Always document why SystemMode = true is required.

AllOrNone (Transactional Mode):

// All records must succeed or all fail
var results = Database.Delete(accounts, new DatabaseOptions 
{ 
    AllOrNone = true
});

Combined Options:

var results = Database.Delete(accounts, new DatabaseOptions
{
    IsApiMode = true,           // Return errors in results
    AllOrNone = true,           // All succeed or all fail
    PermanentDelete = false,    // Soft delete (recycle bin)
    SystemMode = false          // Respect user permissions
});

Delete Operation Examples

Cleanup Old Test Data:

private void CleanupTestData()
{
    // Find test accounts older than 90 days
    var testAccounts = Database.Query<Account>()
        .Where(f => 
            f.Type == "Test" && 
            f.CreatedOn < DateTime.UtcNow.AddDays(-90)
        )
        .ToList();
    
    if (testAccounts.Count == 0)
    {
        SystemInfo.Debug("No test accounts to clean up");
        return;
    }
    
    SystemInfo.Debug($"Cleaning up {testAccounts.Count} test accounts");
    
    // Permanently delete (no restore needed for test data)
    Database.Delete(testAccounts, new DatabaseOptions 
    { 
        PermanentDelete = true
    });
}

Archive Old Records:

private void ArchiveOldRecords()
{
    // Find records older than 5 years
    var cutoffDate = DateTime.UtcNow.AddYears(-5);
    
    var oldRecords = Database.Query<Account>()
        .Where(f => 
            f.Status__c == "Inactive" && 
            f.LastActivityDate__c < cutoffDate
        )
        .ToList();
    
    if (oldRecords.Count == 0)
    {
        SystemInfo.Debug("No records to archive");
        return;
    }
    
    SystemInfo.Debug($"Archiving {oldRecords.Count} old records");
    
    // Soft delete (can be restored if needed)
    Database.Delete(oldRecords);
}

Cascade Delete Pattern:

private void CascadeDeleteAccount(string accountId)
{
    var account = Database.Retrieve<Account>(accountId);

    if (account == null)
    {
        SystemInfo.Debug("Account not found");
        return;
    }

    // Delete all related records in correct order

    // 1. Delete tasks
    var tasks = Database.Query<Task>()
        .Where(f => f.WhatId == accountId)
        .ToList();

    if (tasks.Count > 0)
    {
        int failed = 0;
        try
        {
            Database.Delete(tasks);
        }
        catch (DatabaseException ex)
        {
            failed = ex.Errors.Count;
            foreach (var err in ex.Errors)
            {
                SystemInfo.Debug($"Delete failed for Task Id={tasks[err.Index].Id}: {err.Message}");
            }
        }
        SystemInfo.Debug($"Deleted {tasks.Count - failed} of {tasks.Count} tasks");
    }

    // 2. Delete opportunities
    var opportunities = Database.Query<Opportunity>()
        .Where(f => f.AccountId == accountId)
        .ToList();

    if (opportunities.Count > 0)
    {
        int failed = 0;
        try
        {
            Database.Delete(opportunities);
        }
        catch (DatabaseException ex)
        {
            failed = ex.Errors.Count;
            foreach (var err in ex.Errors)
            {
                SystemInfo.Debug($"Delete failed for Opportunity Id={opportunities[err.Index].Id}: {err.Message}");
            }
        }
        SystemInfo.Debug($"Deleted {opportunities.Count - failed} of {opportunities.Count} opportunities");
    }

    // 3. Delete contacts
    var contacts = Database.Query<Contact>()
        .Where(f => f.AccountId == accountId)
        .ToList();

    if (contacts.Count > 0)
    {
        int failed = 0;
        try
        {
            Database.Delete(contacts);
        }
        catch (DatabaseException ex)
        {
            failed = ex.Errors.Count;
            foreach (var err in ex.Errors)
            {
                SystemInfo.Debug($"Delete failed for Contact Id={contacts[err.Index].Id}: {err.Message}");
            }
        }
        SystemInfo.Debug($"Deleted {contacts.Count - failed} of {contacts.Count} contacts");
    }

    // 4. Finally delete account
    try
    {
        Database.Delete(new List<Account> { account });
        SystemInfo.Debug("Account and all related records deleted");
    }
    catch (DatabaseException ex)
    {
        foreach (var err in ex.Errors)
        {
            SystemInfo.Debug($"Delete failed for Account Id={account.Id}: {err.Message}");
        }
    }
}

Scheduled Cleanup:

public void DailyCleanupJob()
{
    // Delete spam contacts
    var spamContacts = Database.Query<Contact>()
        .Where(f => f.IsSpam__c == true
            && f.CreatedOn < DateTime.UtcNow.AddDays(-30)
        )
        .ToList();

    if (spamContacts.Count > 0)
    {
        int failed = 0;
        try
        {
            Database.Delete(spamContacts, new DatabaseOptions
            {
                PermanentDelete = true
            });
        }
        catch (DatabaseException ex)
        {
            failed = ex.Errors.Count;
            foreach (var err in ex.Errors)
            {
                SystemInfo.Debug($"Delete failed for Contact Id={spamContacts[err.Index].Id}: {err.Message}");
            }
        }
        SystemInfo.Debug($"Cleaned up {spamContacts.Count - failed} spam contacts");
    }

    // Soft delete expired opportunities
    var expiredOpps = Database.Query<Opportunity>()
        .Where(f => f.CloseDate < DateTime.UtcNow.AddYears(-2)
            && f.IsClosed == true
        )
        .ToList();

    if (expiredOpps.Count > 0)
    {
        int failed = 0;
        try
        {
            Database.Delete(expiredOpps);
        }
        catch (DatabaseException ex)
        {
            failed = ex.Errors.Count;
            foreach (var err in ex.Errors)
            {
                SystemInfo.Debug($"Delete failed for Opportunity Id={expiredOpps[err.Index].Id}: {err.Message}");
            }
        }
        SystemInfo.Debug($"Archived {expiredOpps.Count - failed} expired opportunities");
    }
}

Async Variant: DeleteAsync

Use Database.DeleteAsync in any AspxAsyncController action. Three overloads mirror the sync surface: by model, by id, or bulk by collection. 

public class ContactApiController: AspxAsyncController
{
    [Authorize]
    public async Task<ActionResponse> Delete(string id)
    {
        if (string.IsNullOrEmpty(id))
            throw new ModelValidationException("id is required.");

        await Database.DeleteAsync(id);

        return IrisData(new { id });
    }
}

Bulk async delete:

var records = await Database.Query<Contact>()
    .Where(c => c.AccountId == accountId)
    .ToListAsync();

await Database.DeleteAsync(records);

Best Practices for Delete Operations

Use soft delete by default (can be restored)

// ✅ GOOD - Soft delete (default)
Database.Delete(accountId);

// ❌ USE WITH CAUTION - Permanent delete
Database.Delete(accountId, new DatabaseOptions 
{ 
    PermanentDelete = true
});

Check for dependencies before deleting

var contactCount = Database.Count<Contact>(f => f.AccountId == accountId);

if (contactCount > 0)
{
    SystemInfo.Debug("Cannot delete: related contacts exist");
    return;
}

Database.Delete(accountId);

Use bulk operations for multiple records

// ✅ GOOD
Database.Delete(accounts);

// ❌ BAD
foreach (var account in accounts)
{
    Database.Delete(account);
}

Consider cascade deletes for related records

// Delete related records first, then parent
var contacts = Database.Query<Contact>()
    .Where(f => f.AccountId == accountId)
    .ToList();

Database.Delete(contacts);
Database.Delete(accountId);

Never delete in loops

// ❌ VERY BAD
foreach (var id in accountIds)
{
    Database.Delete(id);
}

// ✅ GOOD
var accounts = Database.Query<Account>()
    .Where(f => accountIds.Contains(f.Id))
    .ToList();

Database.Delete(accounts, new DatabaseOptions { IsApiMode = true });

Don't use permanent delete unless absolutely necessary

// ❌ BAD - Unnecessary permanent delete
Database.Delete(accountId, new DatabaseOptions 
{ 
    PermanentDelete = true
});

// ✅ GOOD - Soft delete allows recovery
Database.Delete(accountId);

 

Last updated on 5/26/2026

Attachments