Let's learn about Dataverse - BulkDeleteRequest

BulkDeleteRequestis a feature in Dataverse that we can use to delete data across multiple tables. It is based on a scheduler (optional) and runs in an asynchronous process (as this process runs in the background, meaning this operation will not be performance-friendly). By creating a delete job in the UI, the deletion process will only affect a specific table. Hence, creating this job via code will make more sense to register multiple deletions. So, today we will explore this feature!

Create From UI

To create the job from UI, you can go to Settings > Advance Settings >System Jobs > More Actions > Bulk Delete:

Create Bulk Delete from UI

Create Bulk Delete from UI

Once the popup is shown, you can choose the Table that you want to work > define the query (from new/existing views):

Define Search Criteria

Define Search Criteria

Next, you can define the start time, and a recurrence strategy, and also send an email notification after the deletion is done:

Setting the start time, and a recurrence strategy, and also sending an email notification after the deletion is done

Setting the start time, and a recurrence strategy, and also sending an email notification after the deletion is done

BulkDeleteRequest's Properties

Let's break down one by one of the Properties:

var queryContact = new QueryExpression("contact")
{
    ColumnSet = new ColumnSet("contactid")
};
queryContact.Criteria.AddCondition("contactid", ConditionOperator.Equal, new Guid("65e18bfb-4dd6-ef11-a72f-000d3a59611f"));

var request = new BulkDeleteRequest
{
    JobName = "Bulk Delete Contact",
    QuerySet = [queryContact],
    StartDateTime = DateTime.UtcNow.AddMinutes(2),
    RecurrencePattern = "FREQ=DAILY;INTERVAL=3", // Every 3 Days
    SendEmailNotification = true,
    ToRecipients = [new Guid("4f10e027-051f-ef11-840a-6045bd05b598")],
    CCRecipients = Array.Empty<Guid>(),
};

var response = (BulkDeleteResponse)service.Execute(request);

Console.WriteLine($"Bulk Delete Job ID: {response.JobId}");
  • JobName: It will create a record in the AsyncOperationand BulkDeleteOperationtable with this name.
  • StartDateTime: DateTime to start the job.
  • QuerySet: Array of QueryExpression for the job to pick up and delete. Query limits such as *TOPCount*will not work.
  • RecurrencePattern: If you don't want to make it recurrence, you can give an empty string (""). Otherwise, you can use the pattern that is being mentioned here (or you just can ask AI to generate the command by using RFC2445 iCalendarStandard xxx). The recurrence pattern will only work a minimum "Daily". Otherwise, you will get an error: 'Bulk Delete and Duplicate Detection recurrence must be specified as daily.'.
  • SendEmailNotification: boolean to send a notification after the job is done. If you set it as yes, then you need to fill in the SystemUserId in ToReceipients and CCReceipients.

After execution, it will return JobId which is the AsyncOperationId that we can inspect via SQL4CDS or via UI:

System Jobs generated

System Jobs generated

After the deletion is done, you can see it will trigger an email with the below format (as I'm not set the server-side synchronization, hence the email is stuck in the "Pending Send" state) :

Email notification for succeed deletion

Email notification for successful deletion

Delete Multiple Tables

For this demo, I run the below code:

var queryContact = new QueryExpression("contact")
{
    ColumnSet = new ColumnSet(false)
};
queryContact.Criteria.AddCondition("statecode", ConditionOperator.Equal, 0);

var queryParent = new QueryExpression("tmy_parent")
{
    ColumnSet = new ColumnSet(false)
};
queryParent.Criteria.AddCondition("statecode", ConditionOperator.Equal, 0);

var queryChild = new QueryExpression("tmy_child")
{
    ColumnSet = new ColumnSet(false)
};
queryChild.Criteria.AddCondition("statecode", ConditionOperator.Equal, 0);

var queryBenchmark1 = new QueryExpression("tmy_benchmarktable1")
{
    ColumnSet = new ColumnSet(false)
};
queryBenchmark1.Criteria.AddCondition("statecode", ConditionOperator.Equal, 0);

var queryBenchmark2 = new QueryExpression("tmy_benchmarktable2")
{
    ColumnSet = new ColumnSet(false)
};
queryBenchmark2.Criteria.AddCondition("statecode", ConditionOperator.Equal, 0);

var request = new BulkDeleteRequest
{
    JobName = "Bulk Delete Collection",
    QuerySet = [queryContact, queryParent, queryChild, queryBenchmark1, queryBenchmark2],
    StartDateTime = DateTime.UtcNow.AddMinutes(2),
    RecurrencePattern = "",
    SendEmailNotification = true,
    ToRecipients = [new Guid("f9e7905c-cf7a-ee11-8179-002248201668")],
    CCRecipients = Array.Empty<Guid>(),
};

var response = (BulkDeleteResponse)service.Execute(request);

Console.WriteLine($"Bulk Delete Job ID: {response.JobId}");

With the total records of all the tables equal to 363.502 rows, it took almost 6 hours to delete it (yeah, I know Dataverse deletion is very slow 🥲).

Delete job result

Delete job result

From System Job, you also can see what are the criteria for the deletion:

System Job UI

System Job UI

Conclusion

The below table summarizes the differences between BulkDeleteRequestand ExecuteMultipleRequest (DeleteRequest):

Information BulkDeleteRequest ExecuteMultipleRequest
Multiple Tables Supported via passing QueryExpression Supported. But, we need to populate the DeleteRequestfor each of the rows.
Processing Mode Asynchronous from the Dataverse backend server. Run on the server requesting it.
Bypass Plugins Before the job begins, you need to create a logic to disable the Plugin (there is a risk of other operations bypassing the plugin). On each of DeleteRequest, we can add optional parameters.
Notification OOB sending Email Notification. Need custom code

Happy CRM-ing! 🚀

Leave a comment

Your comment is sent privately to the author and isn't published on the site.