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

 

Last updated on 1/8/2026

Attachments