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 Behavior
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
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 condition is met
var account = Database.Retrieve<Account>(accountId);
if (account.Status__c == "Inactive" && account.LastActivityDate__c < DateTime.UtcNow.AddYears(-2))
var result = 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();
if (accounts.Count > 0)
{
// Single bulk delete
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 results = Database.Delete(accountsToDelete, new DatabaseOptions { IsApiMode = true });
// Track failures
var failures = new List<string>();
for (int i = 0; i < results.Count(); i++)
{
var result = results.ElementAt(i);
if (result.HasError)
{
var account = accountsToDelete[i];
failures.Add($"{account.Name} (ID: {account.Id}): {result.Errors[0].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 loop
var contactIds = GetContactIds();
foreach (var contactId in contactIds)
{
var contact = Database.Retrieve<Contact>(contactId); // Multiple queries
Database.Delete(contact); // Multiple deletes
}
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();
// Delete batch by IDs
var results = new List<DeleteResult>();
foreach (var id in batchIds)
{
results.Add(Database.Delete(id, new DatabaseOptions { IsApiMode = true }));
}
int successCount = results.Count(r => !r.HasError);
int errorCount = results.Count(r => r.HasError);
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)
{
var contactResults = Database.Delete(contacts, new DatabaseOptions { IsApiMode = true });
int deletedContacts = contactResults.Count(r => !r.HasError);
SystemInfo.Debug($"Deleted {deletedContacts} related contacts");
}
// Delete related opportunities
var opportunities = Database.Query<Opportunity>()
.Where(f => f.AccountId == accountId)
.ToList();
if (opportunities.Count > 0)
{
var oppResults = Database.Delete(opportunities, new DatabaseOptions { IsApiMode = true });
int deletedOpps = oppResults.Count(r => !r.HasError);
SystemInfo.Debug($"Deleted {deletedOpps} related opportunities");
}
// Finally delete account
var accountResult = Database.Delete(accountId, new DatabaseOptions { IsApiMode = true });
if (!accountResult.HasError)
{
SystemInfo.Debug("Account and all related records deleted");
}
}
Understanding DeleteResult
The DeleteResult class contains detailed information about delete operations.
DeleteResult Properties:
public class DeleteResult
{
public string Id { get; set; } // ID of deleted record
public bool HasError { get; set; } // True if operation failed
public List<Error> Errors { get; set; } // Error details
}
Checking for Success:
var result = Database.Delete(accountId, new DatabaseOptions { IsApiMode = true });
if (!result.HasError)
{
SystemInfo.Debug($"Successfully deleted record: {result.Id}");
}
else
{
SystemInfo.Debug("Delete failed");
}
Accessing Error Details:
var result = Database.Delete(accountId, new DatabaseOptions { IsApiMode = true });
if (result.HasError)
{
SystemInfo.Debug($"Delete failed with {result.Errors.Count} errors:");
foreach (var error in result.Errors)
{
SystemInfo.Debug($"Message: {error.Message}");
if (!string.IsNullOrEmpty(error.Code))
{
SystemInfo.Debug($"Code: {error.Code}");
}
}
}
Bulk Delete Results Processing:
var accounts = Database.Query<Account>()
.Where(f => f.Type == "Test")
.ToList();
var results = Database.Delete(accounts, new DatabaseOptions { IsApiMode = true });
// Separate successful and failed deletes
var successfulDeletes = new List<string>();
var failedDeletes = new List<string>();
for (int i = 0; i < results.Count(); i++)
{
var result = results.ElementAt(i);
var account = accounts[i];
if (result.HasError)
{
failedDeletes.Add($"{account.Name}: {result.Errors[0].Message}");
}
else
{
successfulDeletes.Add(result.Id);
}
}
SystemInfo.Debug($"Deleted: {successfulDeletes.Count}");
SystemInfo.Debug($"Failed: {failedDeletes.Count}");
foreach (var failure in failedDeletes)
{
SystemInfo.Debug($"Error: {failure}");
}
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 delete cannot be undone. Always use with extreme caution.
Permanent Delete with Confirmation:
public DeleteResult PermanentlyDeleteAccount(string accountId, bool confirmed)
{
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
SystemInfo.Debug($"WARNING: Permanently deleting account '{account.Name}' (ID: {accountId})");
return Database.Delete(accountId, new DatabaseOptions
{
PermanentDelete = true,
IsApiMode = 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();
if (testRecords.Count > 0)
{
SystemInfo.Debug($"WARNING: Permanently deleting {testRecords.Count} test records");
var results = 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
{
var result = Database.Delete(accountId);
}
catch (Exception ex)
{
SystemInfo.Debug($"Delete failed: {ex.Message}");
}
Result Mode (recommended):
var result = Database.Delete(accountId, new DatabaseOptions { IsApiMode = true });
if (result.HasError)
{
foreach (var error in result.Errors)
{
SystemInfo.Debug($"Delete error: {error.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 RedirectToAction("Index");
}
// 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 RedirectToAction("Details", new { id = id });
}
// Attempt delete
Database.Delete(account);
return RedirectToAction("Index");
}
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 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 (Exception ex)
{
SystemInfo.Debug($"Restore failed: {ex.Message}");
}
}
Delete Options (DatabaseOptions)
DatabaseOptions controls delete operation behavior.
IsApiMode:
// Return errors in result (recommended)
var result = Database.Delete(accountId, new DatabaseOptions { IsApiMode = true });
if (result.HasError)
{
// Handle errors in result
}
// Throw exceptions on error (default)
try
{
Database.Delete(accountId);
}
catch (Exception ex)
{
// Handle exception
}
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:
public 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:
public 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:
public 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)
{
Database.Delete(tasks, new DatabaseOptions { IsApiMode = true });
SystemInfo.Debug($"Deleted {tasks.Count} tasks");
}
// 2. Delete opportunities
var opportunities = Database.Query<Opportunity>()
.Where(f => f.AccountId == accountId)
.ToList();
if (opportunities.Count > 0)
{
Database.Delete(opportunities, new DatabaseOptions { IsApiMode = true });
SystemInfo.Debug($"Deleted {opportunities.Count} opportunities");
}
// 3. Delete contacts
var contacts = Database.Query<Contact>()
.Where(f => f.AccountId == accountId)
.ToList();
if (contacts.Count > 0)
{
Database.Delete(contacts, new DatabaseOptions { IsApiMode = true });
SystemInfo.Debug($"Deleted {contacts.Count} contacts");
}
// 4. Finally delete account
var result = Database.Delete(account, new DatabaseOptions { IsApiMode = true });
if (!result.HasError)
{
SystemInfo.Debug("Account and all related records deleted");
}
}
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)
{
var results = Database.Delete(spamContacts, new DatabaseOptions
{
PermanentDelete = true,
IsApiMode = true
});
SystemInfo.Debug($"Cleaned up {results.Count(r => !r.HasError)} 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)
{
var results = Database.Delete(expiredOpps, new DatabaseOptions { IsApiMode = true });
SystemInfo.Debug($"Archived {results.Count(r => !r.HasError)} expired opportunities");
}
}
Best Practices for Delete Operations
✅ Use soft delete by default (can be restored)
// ✅ GOOD - Soft delete (default)
var result = Database.Delete(accountId, new DatabaseOptions { IsApiMode = true });
// ❌ USE WITH CAUTION - Permanent delete
var result = Database.Delete(accountId, new DatabaseOptions
{
PermanentDelete = true,
IsApiMode = true
});
✅ Check for dependencies before delete
var contactCount = Database.Count<Contact>(f => f.AccountId == accountId);
if (contactCount > 0)
{
SystemInfo.Debug("Cannot delete: related contacts exist");
return;
}
var result = Database.Delete(accountId, new DatabaseOptions { IsApiMode = true });
✅ Use bulk operations for multiple records
// ✅ GOOD
Database.Delete(accounts);
// ❌ BAD
foreach (var account in accounts)
{
Database.Delete(account);
}
✅ Log delete operations for audit trail
var account = Database.Retrieve<Account>(accountId);
SystemInfo.Debug($"Deleting account: {account.Name} (ID: {accountId}) by user: {User.Current.Id}");
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);