Write plug-ins for CreateMultiple and UpdateMultiple

Note

The CreateMultiple and UpdateMultiple messages are being deployed. All tables that support Create and Update will eventually support CreateMultiple and UpdateMultiple, but some tables may not support them yet. Learn more about bulk operation messages

You should write plug-ins for the CreateMultiple and UpdateMultiple messages with tables where records may need to be created or updated in bulk, or when performance in creating and updating large numbers of records is important. Just about every table that stores business data may need to be created or updated in bulk.

If you have existing plug-ins for the Create and Update messages for tables like these, you should migrate them to use CreateMultiple and UpdateMultiple instead.

Is updating plug-ins required?

You're not required to migrate your plug-ins to use CreateMultiple and UpdateMultiple instead of Create and Update. Your logic continues to be applied when applications use CreateMultiple or UpdateMultiple. There's no requirement to migrate your plug-ins because the Dataverse message processing pipeline merges the logic for plugins written for either the single or multiple version of these messages.

However, only plug-ins written for the multiple version of these messages get a significant performance boost. Over time, as more developers choose to optimize performance by using the CreateMultiple and UpdateMultiple messages, we expect writing plug-ins for multiple operations to become the standard. Plug-ins written for single operations will be the exception.

What's different?

The following are some of the differences you need to manage when you migrate your plug-ins to the the CreateMultiple and UpdateMultiple messages.

Targets instead of Target

The multiple version of these messages has a Targets parameter that's an EntityCollection rather than a Target parameter that's a single Entity. Your plug-in code needs to loop through the entities in the collection and apply logic to each one.

Entity images

Entity images that are configured in the step registration for your plug-ins are an array of EntityImageCollection. These entity images are only available when you use the IPluginExecutionContext4 Interface, which provides the PreEntityImagesCollection and PostEntityImagesCollection properties. These arrays provide access to the same entity images in an array that's synchronized with the EntityCollection.

If you're using the PluginBase class that's the standard when initializing plug-in projects using Power Platform tools, then in the PluginBase.cs file you should replace all instances of IPluginExecutionContext with IPluginExecutionContext4 so that these collections of entity images are available to your plug-in.

Important

When you configure entity images for plug-in steps for CreateMultiple and UpdateMultiple, it's important that you carefully select which column data to include. Don't select the default option of all columns. This data is multiplied by the number of entities passed in the Targets parameter and contributes to the total size of the message that's sent to the sandbox. You may hit the limit on message size.

Attribute filters

For a plug-in registered on Update or UpdateMultiple, you can specify filtering attributes in the step registration.

  • With Update, the plug-in runs only when any of the selected attributes are included with the Target entity that's being updated.
  • With UpdateMultiple, the plug-in runs when any of the selected attributes are included in any of the entities in the Targets parameter.

Important

For UpdateMultiple, you can't assume that every entity in the Targets parameter contains attributes that are used in a filter.

Example

The following examples, one with some basic logic for Update and another with logic for UpdateMultiple, access entity images registered with the step.

This example updates the sample_description attribute with information about whether the sample_name value has changed. It refers to an entity image named example_preimage that was registered with the step.

// Verify input parameters
if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity entity)
{

   // Verify expected entity image from step registration
   if (context.PreEntityImages.TryGetValue("example_preimage", out Entity preImage))
   {

      bool entityContainsSampleName = entity.Contains("sample_name");
      bool entityImageContainsSampleName = preImage.Contains("sample_name");
      bool entityImageContainsSampleDescription = preImage.Contains("sample_description");

      if (entityContainsSampleName && entityImageContainsSampleName && entityImageContainsSampleDescription)
      {
            // Verify that the entity 'sample_name' values are different
            if (entity["sample_name"] != preImage["sample_name"])
            {
               string newName = (string)entity["sample_name"];
               string oldName = (string)preImage["sample_name"];
               string message = $"\\r\\n - 'sample_name' changed from '{oldName}' to '{newName}'.";

               // If the 'sample_description' is included in the update, do not overwrite it, just append to it.
               if (entity.Contains("sample_description"))
               {

                  entity["sample_description"] = entity["sample_description"] += message;

               }
               else // The sample description is not included in the update, overwrite with current value + addition.
               {
                  entity["sample_description"] = preImage["sample_description"] += message;
               }

               // Success:
               localPluginContext.Trace($"Appended to 'sample_description': \"{message}\" ");
            }
            else
            {
               localPluginContext.Trace($"Expected entity and preImage 'sample_name' values to be different. Both are {entity["sample_name"]}");
            }
      }
      else
      {
            if (!entityContainsSampleName)
               localPluginContext.Trace("Expected entity sample_name attribute not found.");
            if (!entityImageContainsSampleName)
               localPluginContext.Trace("Expected preImage entity sample_name attribute not found.");
            if (!entityImageContainsSampleDescription)
               localPluginContext.Trace("Expected preImage entity sample_description attribute not found.");
      }
   }
   else
   {
      localPluginContext.Trace($"Expected PreEntityImage: 'example_preimage' not found.");
   }
}
else
{
   if (!context.InputParameters.Contains("Target"))
      localPluginContext.Trace($"Expected InputParameter: 'Target' not found.");
   if (!(context.InputParameters["Target"] is Entity))
      localPluginContext.Trace($"Expected InputParameter: 'Target' is not Entity.");
}

Handling exceptions

All errors that occur in your plug-ins should be returned using InvalidPluginExecutionException. When your plug-in throws an exception for steps registered on the CreateMultiple and UpdateMultiple messages, it should identify which record caused the plug-in to fail. To capture this information, use one of the following constructors:

These constructors allow you to add values to the InvalidPluginExecutionException.ExceptionDetails property, which can't be set directly.

Use the constructor's Dictionary<String,String> exceptionDetails parameter to include information about the failed record and any other relevant information.

Set exception details

For the UpdateMultiple message, your code iterates through the EntityCollection Targets property and applies logic to each Entity. If a failure occurs, you can pass the Id of the record to the InvalidPluginExecutionException constructor in the following way:

// in plugin code
foreach (Entity entity in Targets)
{
   // [...] When an error occurs:
   var exceptionDetails = new Dictionary<string, string>();
   exceptionDetails.Add("failedRecordId", (string)entity.Id);
   throw new InvalidPluginExecutionException("This is an error message.", exceptionDetails);
}

Add any other information that's relevant to the failure as string key-value pairs to the exceptionDetails parameter.

For CreateMultiple, we recommend that you don't set the primary key value for each record. In most cases, you should allow the system to set the primary key value for you because the values generated by the system are optimized for best performance.

In cases where the primary key value isn't set, if there's no other unique identifier, you may need to return the index of the failed record in the EntityCollection Targets parameter, or some combination of values that uniquely identify the record that fails. For instance, a key named failedRecordIndex indicating the record's place in the EntityCollection, or any other useful unique identifier, can be added to exceptionDetails to help troubleshoot the failure.

Get exception details

When you include details about the failing operation in the InvalidPluginExecutionException.ExceptionDetails property, the client application can get them from the OrganizationServiceFault.ErrorDetails property through the FaultException<OrganizationServiceFault>.Detail property. The following code shows how:


try
{
   // xMultiple request that triggers your plugin
}
catch (FaultException<OrganizationServiceFault> ex)
{
   ex.Detail.ErrorDetails.TryGetValue("failedRecordId", out object failedRecordId);
}

If the client application uses the Web API, it can get more details about errors by setting the Prefer: odata.include-annotations="*" request header.

Replace single operation plug-ins in solutions

When you deploy plug-in step registrations in solutions, there's no way to force a step registration to be disabled or deleted. That makes replacing logic from a single operation to a multiple operation plug-in a challenge.

When you deploy a new plug-in step in a solution for CreateMultiple or UpdateMultiple that replaces a plug-in step for Create or Update, you want to reduce the amount of time when no logic or duplicate logic is applied. You can manually disable the steps for Create or Update before or after you install the solution. If you disable before, there's a period when no logic is applied. If you disable after, there's a period when duplicate logic is applied. In either case, the organization may require scheduled downtime to ensure logic is applied consistently.

To minimize the duration of either of these conditions, we recommend you include logic to disable any steps that are being replaced by deploying the new plug-ins with Package Deployer. Package Deployer provides the capability to execute custom code before, during, and after the package is imported into an environment. Use this code to disable the existing step registrations.

See also

Sample: CreateMultiple and UpdateMultiple plug-ins
Bulk operation messages
Sample: SDK for .NET Use bulk operations
Optimize performance for bulk operations