OnAfter table event subscriber patterns and antipatterns

The purpose of events is to simplify business logic customization while not impeding upgradeability and general extensibility. However, there is one particular class of events that may cause troubles: OnAfter* table events. There are four of them: OnAfterInsert, OnAfterModify, OnAfterDelete, and OnAfterRename.

If you need them, you must be careful.

First of all, a common misconception about those events is that they execute inside the same write operation as code that we’d normally write inside the matching table triggers. For example, you might expect that the code that you put into OnAfterModify event is executing in the same operation as the code in the OnModify trigger inside the table.

If you read the documentation carefully, you’ll realize that it’s not the case: https://msdn.microsoft.com/en-us/library/mt299406(v=nav.90).aspx#DatabaseEvents

Here’s a simple code to prove that:

image

As the code here shows, the code executes on the same instance of Rec variable that OnInsert trigger uses, but it’s not in the same operation.

The point is – if you don’t persist your changes inside your event subscriber yourself, then those changes will be lost.

In the end, it translates to this: you cannot simply take your customizations out of an OnInsert trigger (or other table triggers) and put them into OnAfterInsert event subscriber (or matching ones) one on one.

In other words, this:

image

… does not translate to this:

image

In my opinion, it’s unfortunate that it does not. But this his how things are, no matter my personal opinion.

I do not want to argue (yet) whether this is a good behavior (or even if it was an intended behavior), but to give some suggestions around how to address this behavior properly.

Unfortunately, to persist our same-table changes from within OnAfter* triggers, we must explicitly persist them. You may not have an opinion about this Smile Thus:

image

I said “you may not have an opinion about this” because there’s not much to argue here. If you want to persist changes, you must – well – persist them.

The problem with that Rec.MODIFY(FALSE) is multiple:

  1. We never had to do that in the “old” code
  2. We may not want to trigger modification from insertion trigger
  3. Modifications can cascade

Regarding point 1, this is a weak argument. “Old” and “New” ways do not necessarily have to map one-on-one.

Regarding point 2, it is something (for Microsoft) to consider. Why should we need to trigger explicit modification from insertion code? Why (unnecessarily) causing an extra database operation? Or, in case of multiple event subscribers (from multiple add-ins for example) causing multiple extra database operations?

And here we come to point 3. Not only it results in extra database operations, it can also cascade. Consider this:

image

So, you end in an endless loop, and eventually (in a couple of milliseconds) in a service tier crash. Which is on you, to make it absolutely clear!

Obviously, you must take care of this in your event subscribers, and it’s your responsibility to make sure endless trigger loops don’t happen. Yes, you never had to do it before, because you did make sure no triggers would execute by calling MODIFY with FALSE.

In the event-driven world, there is a similar way to make sure of this. Your table event subscribers come with a particular RunTrigger parameter, so take advantage of it:

image

A complaint that you may put here is “yes, fine, I can do it for my code, but how do I make sure 3rd party add-ins do the same”?

The answer is – you don’t. And you don’t have to.

An add-in that introduces the cascade will also suffer from the cascade, and whoever wrote it will have to resort to the IF NOT RunTrigger THEN EXIT pattern to solve their problem before it comes anywhere near becoming your problem.

Yes – I hear you – a cascade does not have to be direct, it can be circular. Like, modification of Vendor triggers modification of Item, and modification of Item triggers modification of Vendor.

Still, nothing to worry about. A single IF NOT RunTrigger THEN EXIT (yours) will take care of breaking the cascade

And yes – I hear you – it’s okay if you own the code, so you put this RunTrigger check; but there may be two separate add-ins that introduce circular cascade, and you don’t control the code. For example:

image

If these two come from two separate add-ins, then indeed, there is nothing you can do. If this ends up in your database, you’re toast.

However, it’s still not an excuse for you to not do the RunTrigger check in the code you write. You must do it, if for no other reason, then simply because that’s the real “translation” of the “old” in-place table trigger customization into the new event-driven architecture.

Think of it like this: in the old days it was as easy to cause circular database updates by calling MODIFY(TRUE) on any record from OnModify trigger of any table. And while you could make sure to call MODIFY(FALSE), you could not make sure that two separate add-ins did not accidentally do MODIFY(TRUE). So – this is not a new problem, it’s an old problem, just in different outfit.

The same way MODIFY(TRUE) was a bad practice in the old days, not checking for RunTrigger is a bad practice in these days. Remember it. And then apply it.

Yes, MODIFY(TRUE) did have it’s place. As much as there were valid reasons when you would certainly want to write MODIFY(TRUE) there are similar valid reasons when you’d not want to check for RunTrigger and simply unconditionally execute it. However, just as much as you had to be careful about not causing any unintended cascades every time you wrote MODIFY(TRUE) yesterday, you will have to be careful about it every time you intentionally decide to ignore the RunTrigger parameter tomorrow.

Now that I have explained the correct pattern, let me say this: I personally believe this is bad behavior. No matter how much we can get around it, it is still bad behavior. OnAfter table triggers are designed for customization purposes, so we could customize business logic while not changing standard code. An event that happens outside the write operation (while it certainly has its own place in the great scheme of things) is not a correct replacement for the matching table trigger that executes inside the write operation. And that’s all there is to it. It’s just not the same thing.

However, opinion is like a**hole. Everyone’s got one (and everyone thinks everyone else’s stinks).

Vjeko

Vjeko has been writing code for living since 1995, and he has shared his knowledge and experience in presentations, articles, blogs, and elsewhere since 2002. Hopelessly curious, passionate about technology, avid language learner no matter human or computer.

This Post Has 18 Comments

  1. Gary Winter

    However one may or may not be opinionated about this platform behavior, the event does what it says it does: It is executed OnAfter(!)Insert and not OnInsert. This is also how the process is described on msdn help: the database action kicks in first, then the OnAfterInsert gets executed.

    This behavior makes sure that your extension based on this event trigger works on an already successfully inserted record and is responsive to GETs or FINDs on this record, which may also be regarded as a benefit.

    If you want to hook into the OnInsert code before the record gets inserted, i.e. practically supplementing the insert trigger code, you can subscribe to the global event OnAfterOnDatabaseInsert which is raised in Codeunit 1.

    Since I use a lot of integration and business events in my products, I try to maintain a very clear distinction on when an event gets raised. If there is a function called DoSomething and there is an event called OnAfterDoSomething, you can be sure that it is only raised after DoSomething has been completely executed. If the event is, however, called OnDoSomething, you can be sure that it is being raised inside of the DoSomething function, supplementing its code.

    I suggest to be very careful on naming events. In that respect the OnAfterOnDatabaseInsert event should more aptly be called OnOnDatabaseInsert, but that would sound somewhat odd. The reason for that is, of course, that this is an event which is already raised by another event; naming gets a little tough in these situations.

    1. Vjeko

      Gary, I totally agree that the event does exactly what it says it does. And my opinion may stink 🙂 However, for extensibility purpose, an event that’s raised in the context of a specific table but *before* the database operation takes place would be far more useful than an event which occurs right *after* the database operation occurred. Subscribing to OnAfterOnDatabaseInsert may achieve that, but only with a big fat CASE RecRef.NUMBER OF to trigger specific table operation subscribers (or local functions) so it’s not a clean design. Another argument (or a stinky opinion ;)) I have here is that if you have fifteen extensions, all fifteen will necessarily have to call MODIFY (because none can rely on any other doing the modification) causing fifteen extra database write operations, each in turn potentially triggering unnecessary C/AL logic.

      However, the goal of my article was to indicate the correct pattern. Only the alternative goal was to share my (stinky) opinion about us needing an actual event that happens *before* database write operation, but *after* On[Insert/Modify/Delete/Rename] table trigger. Whatever we may want to call that event, I believe the current architecture lacks it, and this is causing us to write unnecessarily cluttered code, or cause unnecessary cascading calls in C/AL.

      And last argument here is (and I’d like to have your answer on that) – considering two things: 1) that events were purportedly designed to help us write clean customizations, and replace old “dirty” in-place code with clean event subscriber code; and 2) that OnAfter* table event occurs *after* database write operation – if we had an additional table event that happens *after* On* table trigger but *before* actual database operation, how much of the “old” code would get written in this missing event subscribers, and how much would still go into that subscriber which we currently have that executes *after* database operation?

      My answer to this question is: exactly none of the old code would get into the *after* trigger, and exactly all of the old code would get into the *before* trigger. Please let me know if your answer is (substantially) different.

  2. Natalie

    Very good to point this one out, thanks Vjeko.

  3. Freddy Kristiansen

    Ran into the same thing – and ended up moving everything to OnBefore (as you stated). Since there is no Abort opportunity that seems OK, unless you need to act on something the original trigger has done.

    1. Vjeko

      We ran into this problem yesterday when we depended on No. Series logic to have actually occurred, but needed to do something before the actual database insert happened. It’s not that we can’t get away with an additional MODIFY(FALSE) (and then doing the IF NOT RunTrigger THEN EXIT), but it’s unnecessary forcing of developers to write unnecessary database write logic.

      I agree that for many situations, when the OnAfter* logic does not need to write to the same table, the OnAfter* triggers are good enough, but when the same table is needed, then it’s a bit clumsy. Works, alright, but clumsy 🙂

      Any chances this gets revised at a point?

      1. Freddy Kristiansen

        That would be a breaking change and probably not something that is done easily. But I guess we could add a parameter or another event if needed.

        1. Vjeko

          My personal take on this is – try to give us another event. There’s no breaking change there, and it’s probably not such a big deal to create another OnAfter*Trigger (or something) event that happens just before database write operation, which then saves a lot of roundtrips to server.

  4. George

    Hi guys,

    Run into same problem today.
    I try use this simple code OnAfterInsert in table Sales Header

    IF RunTrigger THEN BEGIN
    Rec.”Assigned User ID” := USERID;

    Now when user go to Sales Quote and create a new document and assign a new number from series the field “Assigned User ID” is filled like I was expecting.

    Surprising is that in case user hit Refresh button “Assigned User ID” field will become empty.
    So record is not persistent. This is somehow confusing.

    1. Vjeko

      Yes, this is because OnAfterInsert happens after database insert has occurred. It’s a confusing trigger, and as my post argues – it’s not the correct replacement for the OnInsert customizations.

  5. Jaspreet

    Hi,
    Is there a way to comment standard code written in Base objects using Events. The Goal is to stop a validation from happening in a standard trigger but while maintaining the standard object as is for later packing in a extension?

    Thanks

    1. Vjeko

      No, there isn’t such a possibility.

  6. Nessuno

    what about transactions here is OnAfterInsert event called in a separate transaction ( that is an implicit COMMIT is executed before calling the event ) ?

    1. Vjeko

      It is after the original transaction, yes. You cannot make your code execute in the same transaction from the OnAfterInsert. You must use OnBeforeInsert for that.

Leave a Reply