Dynamics CRM: Awaiting for Bulk Delete completion
In my previous post I described my method for awaiting of async job completion. I have a similar one for awaiting Bulk Delete operation completion as well. Dealing with Bulk Delete we have to keep in mind the following: initiating the operation through the code, the GUID returned in the BulkDeleteResponse is not the id of the Bulk Delete operation, but the id of some async job; the target Bulk Delete operation will be created a bit later and will be associated with the async job; The code below illustrates the submitting of Bulk Delete request:
... OrganizationServiceProxy proxy = ...; QueryExpression[] queries = ...; var bulkDeleteRequest = new BulkDeleteRequest { JobName = "Bulk Delete " + DateTime.Now, QuerySet = queries, StartDateTime = DateTime.Now.AddDays(-2), /* -2 is to avoid the problem with local and utc times */ ToRecipients = new Guid[] {}, CCRecipients = new Guid[] {}, SendEmailNotification = false, RecurrencePattern = String.Empty }; var bulkDeleteResponse = (BulkDeleteResponse)proxy.Execute(bulkDeleteRequest); // id of the job which will be associated with the Bulk Delete operation Guid jobId = bulkDeleteResponse.JobId;
So, having id of the async job that will be shortly associated with the Bulk Delete operation, the awaiting process supposes two steps:
- wait until the Bulk Delete operation has been created;
- wait until the Bulk Delete operation has completed;
For both steps the RetrieveMultiple is repeatedly called against the bulkdeleteoperation records. Firstly, we try to find a record, the asyncoperationid attribute of which equals the async job id received before. Once the record has been found, it means the Bulk Delete operation has been created. Secondly, then we wait for the Bulk Delete operation completion.
The steps mentioned above have been implemented in the WaitForBulkDeleteCompletion method shown below. Like the WaitForAsyncJobCompletion, the WaitForBulkDeleteCompletion allows adjusting number of retries and sleep interval. It also analyses operation’s current state (and status) and provides actual progress on every iteration of the awaiting process.
public static BulkDeleteState WaitForBulkDeleteCompletion(OrganizationServiceProxy proxy, BulkDeleteAwaiting asyncJobAwaiting) { var state = new BulkDeleteState(); #if WRITE_TRACE_INFO WriteInfo(string.Format(@"Waiting for completion of the bulk delete associated with the Async Job {0}...", asyncJobAwaiting.AsyncJobId)); #endif // Query for bulk delete operation and check for status. var bulkQuery = new QueryByAttribute(BulkDeleteOperation.EntityLogicalName) { ColumnSet = new ColumnSet(true) }; bulkQuery.Attributes.Add("asyncoperationid"); bulkQuery.Values.Add(asyncJobAwaiting.AsyncJobId); state.AsyncJobId = asyncJobAwaiting.AsyncJobId; state.CurrentRetryCount = asyncJobAwaiting.Params.RetryCount; state.CurrentTimeout = asyncJobAwaiting.Params.StartTimeout; do { Thread.Sleep(state.CurrentTimeout); EntityCollection entityCollection = proxy.RetrieveMultiple(bulkQuery); #if WRITE_TRACE_INFO WriteInfo(string.Format("Found {0} entities ...", entityCollection.Entities.Count)); #endif if (entityCollection.Entities.Count > 0) { // Grab the one bulk operation that has been created. var createdBulkDeleteOperation = (BulkDeleteOperation)entityCollection.Entities[0]; state.BulkDeleteOperationId = createdBulkDeleteOperation.BulkDeleteOperationId; state.CurrentState = createdBulkDeleteOperation.StateCode; state.CurrentStatus = createdBulkDeleteOperation.StatusCode != null ? (bulkdeleteoperation_statuscode)createdBulkDeleteOperation.StatusCode.Value : (bulkdeleteoperation_statuscode?) null; state.SuccessCount = createdBulkDeleteOperation.SuccessCount; state.FailureCount = createdBulkDeleteOperation.FailureCount; } #if WRITE_TRACE_INFO if (state.IsCreated) { if (state.IsSuspended) WriteInfo(string.Format("Bulk Delete (Id: {0}) is suspended", state.BulkDeleteOperationId)); if (state.IsPaused) WriteInfo(string.Format("Bulk Delete (Id: {0}) is paused", state.BulkDeleteOperationId)); if (state.CurrentState != null) WriteInfo(string.Format("Bulk Delete (Id: {0}) state is {1} {2}", asyncJobAwaiting.AsyncJobId, state.CurrentState, (state.CurrentStatus != null ? "(Status: " + state.CurrentStatus.Value + ")" : string.Empty))); WriteInfo(string.Format("{0} records were successfully deleted", state.SuccessCount ?? 0)); WriteInfo(string.Format("{0} records failed to be deleted", state.FailureCount ?? 0)); } else WriteInfo(string.Format("Bulk Delete is still not created. Associated Async Job Id:{0})", asyncJobAwaiting.AsyncJobId)); #endif asyncJobAwaiting.FireOnProgress(state); if (asyncJobAwaiting.Params.TimeoutStep != null && asyncJobAwaiting.Params.EndTimeout != null && state.CurrentTimeout < asyncJobAwaiting.Params.EndTimeout) state.CurrentTimeout += asyncJobAwaiting.Params.TimeoutStep.Value; if (asyncJobAwaiting.Params.WaitForever) continue; if (state.IsSuspended && asyncJobAwaiting.Params.DoNotCountIfSuspended) continue; if (state.IsPaused && asyncJobAwaiting.Params.DoNotCountIfPaused) continue; if(!state.IsCreated && asyncJobAwaiting.Params.DoNotCountIfNotCreated) continue; state.CurrentRetryCount--; } while (!state.IsCompleted && state.CurrentRetryCount > 0); return state; }
Classes involved in the method are derived from the same base classes as those used for WaitForAsyncJobCompletion. The diagram below demonstrates dependencies between the base classes and the ones specific for Bulk Delete operations.
The source code of the base classes AsyncOperationState, AsyncOperationAwaitingParams and AsyncOperationAwaiting are shown in the post – Awaiting for async job completion. The rest of used classes are listed below:
/// <summary> /// Represents the awaiting parameters for Bulk Delete operations /// </summary> public class BulkDeleteAwaitingParams : AsyncOperationAwaitingParams { private bool _doNotCountIfNotCreated = true; /// <summary> /// Indicates if an iteration counts when the target Bulk Delete operation /// hasn't been created yet /// </summary> public bool DoNotCountIfNotCreated { get { return _doNotCountIfNotCreated; } set { _doNotCountIfNotCreated = value; } } }
In comparison with the AsyncOperationAwaitingParams, I added one more property specifying whether retries are counted while Bulk Delete operation hasn’t been created yet.
/// <summary> /// Represents parameters for tracking of a Bulk Delete operation /// </summary> public class BulkDeleteAwaiting : AsyncOperationAwaiting<BulkDeleteState, BulkDeleteAwaitingParams> { /// <summary> /// Id of the async job that will be associated with the target Bulk Delete operation /// </summary> public Guid AsyncJobId { get { return _asyncOperationId; } } public BulkDeleteAwaiting(Guid asyncJobId, BulkDeleteAwaitingParams asyncJobAwaitingParams): base(asyncJobId, asyncJobAwaitingParams) { } }
Note that the AsyncJobId here represents an auxiliary async job that is supposed to be associated with the target Bulk Delete operation.
/// <summary> /// Represents current state of a Bulk Delete operation /// </summary> [Serializable] public class BulkDeleteState : AsyncOperationState<BulkDeleteOperationState?, bulkdeleteoperation_statuscode?> { /// <summary> /// Id of the async job associated with the target Bulk Delete operation /// </summary> public Guid AsyncJobId { get; set; } /// <summary> /// Id of the target Bulk Delete operation. Null when the operation isn't created. /// </summary> public Guid? BulkDeleteOperationId { get; set; } /// <summary> /// Current number of successfully deleted records /// </summary> public int? SuccessCount { get; set; } /// <summary> /// Current number of problem records /// </summary> public int? FailureCount { get; set; } /// <summary> /// Indicates if the Bulk Delete is suspended /// </summary> public override bool IsSuspended { get { return CurrentState != null && CurrentState.Value == BulkDeleteOperationState.Suspended; } } /// <summary> /// Indicates if the Bulk Delete is paused /// </summary> public override bool IsPaused { get { return CurrentState != null && CurrentState.Value == BulkDeleteOperationState.Locked && CurrentStatus != null && CurrentStatus.Value == bulkdeleteoperation_statuscode.Pausing; } } /// <summary> /// Indicates if the Bulk Delete is completed /// </summary> public override bool IsCompleted { get { return CurrentState != null && CurrentState.Value == BulkDeleteOperationState.Completed; } } /// <summary> /// Indicates if the number of retries have run out while the Bulk Delete is /// still uncompleted /// </summary> public override bool IsTimedOut { get { return !IsCompleted && CurrentRetryCount <= 0; } } /// <summary> /// Indicates if the Bulk Delete is failed /// </summary> public override bool IsFailed { get { return CurrentStatus != null && CurrentStatus.Value == bulkdeleteoperation_statuscode.Failed; } } /// <summary> /// Indicates if the Bulk Delete is created /// </summary> public bool IsCreated { get { return BulkDeleteOperationId != null && BulkDeleteOperationId.Value != Guid.Empty; } } }
The class represents a current state of the Bulk Delete operation being awaited. The current state on each iteration is supplied by the WaitForBulkDeleteCompletion through the OnProgress event available in the passed BulkDeleteAwaiting instance. In the end the WaitForBulkDeleteCompletion method returns the final state. The bulkdeleteoperation_statuscode enum is defined in the OptionSets.cs at sdk\samplecode\cs\helpercode.
All the source code can be downloaded here – ConnectToCrm4.zip