Runtime Eventing in Dynamics365 for Operations


To help troubleshoot some things at a recent launch, we had a request to be able to monitor CRUD on certain tables.  Database logging is a viable option, however I wanted to take time to see how difficult it would be to build this on my own. 

While there are only 1 or 2 tables I needed to monitor now, I want something flexible that we can turn on or off on a table level.  In an effort to find a solution where I can select which tables are being audited on the fly, I went down the path of learning more about how Events and Delegates work.

Quickly, there is a difference between how X++ works with Events and Delegates compared to C#:

  • In AX 2012 there are only two kinds of events
    • Start or end of a method ( XppPrePostArgs )
    • An explicit call to a delegate
  • In D365 there are a multitude of standard events
    • Every table has around a dozen events ( Inserting, Inserted, Deleting, Deleted, etc. )
    • Every form and form control has events ( OnClicked, Initializing, OnPostLoad )
    • Standard delegates exist in many classes
  • In C#, Delegates can be their own type just as a Class is a type. 
    • An Event is simply the internalization of a Delegate inside of a Class

So, I’m thinking – let’s just create an Event Handler class for the Common table object!  That should impact everything right?  And then we can specifically handle tables that are configured for auditing and ignore the rest.

However, the Compiler stops us here:

This is likely for good reason, as adding a handler to all tables is a very drastic move with some performance considerations.  However, I am stubborn and wanted to push forward anyway.

To learn more about how the DataEventHandler metadata works, I decompiled a few of the system DLLs to find Common.cs:

  1. public bool HasDeleteEventHandler()
  2. {
  3. DataEvent dataEvent = DataEventManager.GetEvent(this.__TableNode.Handle.Name);
  4. return dataEvent != null && (dataEvent.DeletedEvent != null || dataEvent.DeletingEvent != null);
  5. }

This method in particular is interesting as it appears the presence of a Delete Handler is retrieved from the DataEventManager class.  Looking in to this class, we can see Events are simply cataloged in a Dictionary:

  1. private static IDictionary<string, DataEvent> dataEvents = (IDictionary<string, DataEvent>) new ConcurrentDictionary<string, DataEvent>();
  2.  
  3. public static void Subscribe(string tableName, DataEventType eventType, DataEventHandler handlerDelegate)
  4. {
  5. DataEvent dataEvent = (DataEvent) null;
  6. if (!DataEventManager.dataEvents.TryGetValue(tableName, out dataEvent))
  7. {
  8. dataEvent = new DataEvent();
  9. DataEventManager.dataEvents[tableName] = dataEvent;
  10. }
  11. dataEvent.Subscribe(eventType, handlerDelegate);
  12. }
  13.  
  14. public static DataEvent GetEvent(string tableName)
  15. {
  16. DataEvent dataEvent = (DataEvent) null;
  17. DataEventManager.dataEvents.TryGetValue(tableName, out dataEvent);
  18. return dataEvent;
  19. }

This is likely initialized at Runtime based on all of the metadata in our code.  What is exciting to me, is that there is a public static Subscribe method!

Let’s see if we can add a new Event Handler on the fly to monitor deletes of the VendInvoiceInfoTable.  To start, we’ll create a C# class library:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using Microsoft.Dynamics.Ax.Xpp;
  7. using Microsoft.Dynamics.AX.Data.Core;
  8. using Dynamics.AX.Application;
  9.  
  10.  
  11. namespace McaConnect.Extensions.DataUtils
  12. {
  13. public class DeleteHandler
  14. {
  15.  
  16. public static void Subscribe(string _tableName, DataEventType _type)
  17. {
  18. DataEventManager.Subscribe(_tableName, _type, onRecordDeleted);
  19. }
  20.  
  21. public static void onRecordDeleted(Common _table, DataEventArgs _args)
  22. {
  23. McaDatabaseLogger.handler1(_table, _args);
  24. }
  25.  
  26. }
  27. }

We also need to add the following DLLs as references to the project:

You’ll see I have a method that follows the signature of the DataEventHandler called onRecordDeleted.  Then, I have a Subscribe method simply because the way that we handle the passing of the delegate pointer.  Only in C# can we pass in this parameter, as the X++ compiler won’t appreciate this syntax. 

Now, let’s create an X++ class, I’ve called mine McaDatabaseLogger which has a couple static methods:

  1. using MSFT = Microsoft.Dynamics.Ax.Xpp;
  2. using McaConnect.Extensions.DataUtils;
  3.  
  4. class McaDatabaseLogger
  5. {
  6.  
  7. public static void Main(Args _args)
  8. {
  9. DeleteHandler::Subscribe('VendInvoiceInfoTable', DataEventType::Deleted);
  10. info('subscribed');
  11. }
  12.  
  13. public static void handler1(Common sender, DataEventArgs e)
  14. {
  15.  
  16. if(sender is VendInvoiceInfoTable)
  17. {
  18. VendInvoiceInfoTable record = sender as VendInvoiceInfoTable;
  19. error(strFmt('Database log event :: %1 RecId=%2', tableStr(VendInvoiceInfoTable), record.RecId));
  20. }
  21. }
  22.  
  23. }

Add a reference in your X++ project to your C# project.  Then link up a test Action Menu Item to this class and fire it up in the debugger.  You’ll see we successfully subscribe the table which adds our new delegate to the OnDeleted event of the VendInvoiceInfoTable. 

Now, go and delete a pending vendor invoice.  You’ll see our logic is hit! 

Moreover, this is cross-session so this is not limited to my user account but it is across the board. 

Doing a quick IISReset and we see that the system is reset and our custom event handler is lost.  This is important to note, as there doesn’t seem to be a way to programmatically Unsubscribe. 

It would get lost the next time the system is restarted, but what you would want to do is ensure that if you no longer care about auditing a table you would ignore the events that would continue to be handled.