Follow Microsoft to trigger plugins by Bulk Operations

Microsoft has recently urged us to move all our plugin steps from messages: Create, Update, Delete to instead use: CreateMultiple, UpdateMultiple, and DeleteMultiple.

I like that.
I like that a lot

You may have read about it and just said, “OK, thanks, but that’s not relevant to us; see you never.”

You’re wrong.
I will help you understand why you actually need it, and I’ll try to explain a bit about how it works. So read on!


I was testing a few months ago about this new preview feature, UpdateMultipleRequest (etc.), instead of the old existing request ExecuteMultipleRequest. But in one specific case, it was a bit too hard to use when we may create/update records both one-by-one and in bulk.

I’ve been waiting many years to run my plugins when everything is complete and finished (depending on the Stage), so then I’d trigger the message UpdateMultiple, but when we executed it as a one-by-one, then we also had to have the same code to be triggered by the message Update. The biggest problem is that UpdateMultipleRequest it will trigger both UpdateMultiple and Update messages. That was bad…

Now, these features are officially released! There is one small change that has a big effect: these messages are ‘double-triggering’ even more! It may sound stupid, but it is essential since we can now select which one we will use.


The UpdateMultiple message will now always be triggered, even if only one record is updated. Either by code or by a user in the standard UI. And everything else too.

Why should we go to xMultiple steps?

When we have the possibility to only act the plugin when a full bulk is executed, instead of at each record being executed, that will be awesome at performance.

When it’s possible to do things in bulk, use it as often as we can.


Examples

I have i plugin that triggers when an Account is updating Annual Revenue.
Two features are done in the plugin:

  1. Validate: If we already have a revenue, we will never accept nulling it.
  2. Aggregate: The Primary Contact (=the boss) will get an aggregate of all his companies’ revenues.

Very nice, valuable plugins? No, I know. It’s not used for any of my customers. Promise.

In this case, I’m using a PluginBase from my own code, you can find it here: https://github.com/rappen/Rappen.XTB.Helper and the shared project Rappen.XRM.Helpers.

This is the classic, one-by-one record plugin:

    public class PluginOneRecord : PluginBase
    {
        public override void Execute(PluginBag bag)
        {
            var account = bag.ContextEntity[ContextEntityType.Complete];
            var preimage = bag.ContextEntity[ContextEntityType.PreImage];

            Features.VerifyRevenue(bag.Logger, account, preimage);

            var primaryContact = account.GetAttributeValue("primarycontactid", new EntityReference());
            if (!primaryContact.Id.Equals(Guid.Empty))
            {
                Features.SummarizeBossRevenues(bag, primaryContact);
            }
        }
    }

The ContextEntity is a class that easily returns the Target, the PreImage, the PostImage, and something called Complete. All those four types of the property have type Entity, and it helps sooo much to have something like that. Complete is used to get all info we can find, meaning merge from Target, PostImage and PreImage – if those are available in the IPluginExecutionContext.
Nothing fancy code, but I can’t survive without these. You can find that code here: Rappen.XRM.Helpers/Plugin/ContextEntity.

Here is the general code with the methods for the two features needed:

        internal static void VerifyRevenue(ILogger log, Entity account, Entity preaccount)
        {
            var nowRev = account.GetAttributeValue("revenue", new Money()).Value;
            var preRev = preaccount.GetAttributeValue("revenue", new Money()).Value;
            if (preRev > 0 && nowRev <= 0)
            {
                throw new InvalidPluginExecutionException($"Accounts can't decrease revenue. {account["name"]} is wrong.");
            }
            log.Log($"{account["name"]} looks ok.");
        }

        internal static void SummarizeBossRevenues(IBag bag, EntityReference contactRef)
        {
            bag.Logger.Log($"Summarizing {contactRef.Name ?? (contactRef.LogicalName + " " + contactRef.Id)}");
            var accRevs = bag.RetrieveMultiple(new QueryExpression("account")
            {
                ColumnSet = new ColumnSet("name", "revenue"),
                Criteria =
                {
                    Conditions =
                    {
                        new ConditionExpression("statecode", ConditionOperator.Equal, 0),
                        new ConditionExpression("primarycontactid", ConditionOperator.Equal, contactRef.Id)
                    }
                }
            });

            var sumRev = accRevs.Entities.Sum(e => e.GetAttributeValue("revenue", new Money()).Value);
            var contact = new Entity(contactRef.LogicalName, contactRef.Id);
            contact["description"] = $"Total revenues: {sumRev:0}";
            bag.Update(contact);
        }

Nothing fancy about this code – just so you can see how we use it in this example case.

The VerifyRevenue should be run for every update of every record.

What would really help though, is that the SummarizeBossRevenues method should only be run once, even if many accounts with the same boss…

So here I created a new fancy plugin, trigging only by the UpdateMultiple message:

    public class PluginMultipleRecords : PluginBase
    {
        public override void Execute(PluginBag bag)
        {
            bag.ContextEntityCollection.ToList()
                .ForEach(cec => Features.VerifyRevenue(bag.Logger, cec[ContextEntityType.Complete], cec[ContextEntityType.PreImage]));

            var primaryContacts = bag.ContextEntityCollection
                 .Select(cec => cec[ContextEntityType.Complete].GetAttributeValue("primarycontactid", new EntityReference()))
                 .Where(c => !c.Id.Equals(Guid.Empty))
                 .Distinct();
            primaryContacts.ToList().ForEach(c => Features.SummarizeBossRevenues(bag, c));
        }
    }

Here we can find a bit more advanced of ContextEntity is ContextEntityCollection : IEnumerable<ContextEntity>

Again, nothing fancy here either, but this one bunches them together, all entities from Context.InputParameters["Targets"]. Note on the “s” at the end.

This requires we use IPluginExecutionContext4 (or later), there we see EntityImageCollection[] collecting PreImages and PostImages.

To me, I’m wondering why target and pre/post are more connected to each other in the SDK classes. And when we are now using a collection of targets and pre’s/post’s, we have to find them by the index in these different collections. That’s a bit weird to me.

In the code of the PluginMultipleRecords we see it is very easy to move on to multiple codes. I’ve tested a few times with our “real code”, and it’s still so easy.

Code without any base class

My code above uses a few help features, you may see “raw” code without any help if it helps you more:
Rappen.XRM.Helpers.PluginTest/PluginWithoutBaseClass.cs

My best tip

Just snatch my classes in one file: ContextEntity.cs and use it everywhere in plugin code. (If you don’t have even better classes? Ping me!)

Just do it.

It doesn’t need any of my other file, just add this and your life will be so much easier.


Why should we go to xMultiple steps?


Business Logic

Seems very clear that it would be great to use bulk operations in plugins. But still, this is not supported.

What we can do is Azure Functions, which often is needed because they are “Duration Functions” – they take a lot of time!

Azure Functions loves CreateMultipleRequests. You can also read more about Duration Functions by Lee Baker.


Integration

It’s a no-brain that integration shall use CreateMultipleRequest and UpdateMultipleRequest to (probably) get faster than both one-by-one calls and ExecuteMultipleRequest.

So use it now that it is officially available.


Migration

Migration tools can use CreateMultipleRequest and UpdateMultipleRequest in many cases, for example, KingswaySoft and… well, we at CRMK only use their tool SSIS Integration Toolkit for Microsoft Dynamics 365.

This means another reason to trigger your plugins with multiple calls!


Community Tools

Community developers like to be on the edge, trying the newest features and implementing them usually a long time before Microsoft officially releases them. I know there are some tools that already are using xMultipleRequests.

Like for example, my little tool, Bulk Data Updater – is incredibly useful after migration to fix the migrated data when we realize it should be saved in another format, etc.

Another tool is, of course, SQL 4 CDS by Mark Carrington. This tool creator knows things probably a long time before even Microsoft has any idea about it.


Limitations

I suggest you read through these Limitations docs!

Never use UpdateMultipeRequest in plugin code

It is not supported to use UpdateMultipleRequest (etc.) in your plugin code. I can think of a bunch of cases where that would simplify my life, both from running the code and further plugins to trigger from this action.

It’s quite obvious why that’s not allowed – if a plugin makes a massive update, it’s unfortunately easy to get timeout issues, and everything will crash badly.

From my guess, it might also stretch the performance on the servers by creating big multi-operations.

But anyway, we shall, of course, accept what Microsoft has told us and don’t use it in this case. Period. But please stay tuned for any updates from Microsoft in the future!

What if…

Dreaming a bit more… What if we use the standard bulk edit feature to start to update all these selected records through one request…

Maybe one day…


I don’t talk about API costs…

…but why is one call not always cost one API call?

My thought was that one call would cost one.
Then I realized Microsoft would like to get the bucks they deserve, so I was guessing that if I called an UpdateMultipleRequest to update 100 records, it would cost one call, plus all 100 records that actually are updated, so I have to pay for 101 API calls.
Then someone at Microsoft explained to me, it would cost 100 API calls.

I guess the multiple calls are free, that’s nice, I guess…!

Ok, just accept it and move on


Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.