Sunday, March 5, 2017

I get by with a little help from my [base class]

Developing plugins for Microsoft Dynamics 365 (CRM) only using bare SDK libraries make you do the same stuff over and over.
This is why one of the first things I did when starting to work with the platform was to create a helping hand in the form of a plugin base class, implementing IPlugin.

This class soon evolved to a library implementing our own organization service, custom file logging, encapsulate our translation and configuration management entities, and tons of other useful stuff that helps me and my colleagues develop plugins and integration services so much faster and easier than without it. Development speed aside, it makes our code more readable and easier to follow, as most of the background activities are performed in the same way.

Another benefit is that if optimizations to the backing libraries are possible, we can ship the improvements with a few simple clicks to all of our products and customer projects by just publishing a new version of our private NuGet packages, instantly available to any and all consumers.

As these libraries have evolved over the versions, beginning with Microsoft Dynamics CRM 4.0, they quite naturally contain some technical debt, which for me as responsible developer translates to bad sleeping patterns and a nagging feeling that one day I want to rip it all out and rewrite everything. In our day to day work, we improve on the libraries, but want to keep breaking changes to a minimum, which by definition can only increase the debt.

 

I saw the light

Average winter day in SwedenThen a few months ago, the clouds parted and I got a glimpse of the light – I was invited to speak at the CRM Saturday event in London to tell my story about XrmToolBox and how I use it to simplify development on the Dynamics platform. If I was to do this I really wanted to show my own favorite tool; the Plugin Trace Viewer which has helped me and my colleagues time after time after time when analyzing plugin behavior.

I didn’t want the demo to go with bare SDK, and I didn’t want to bring in our bloated DevUtils dinosaur. So this gave me an opportunity to start over, to create a more minimalistic plugin base class that helps with the things I need most:

  • Extract plugin context
  • Get a service to work with
  • Initialize tracing service
  • Always write details and timing information to the trace when using the service
  • Provide easy access to target entity, pre image and post image

 

JonasPluginBase

I rather quickly had something that was good enough for a demo, to get a feeling for the benefits of working this way and to make a habit of writing to the trace log. And it didn’t take long until I thought someone else might actually find this useful. So I went public.

However, my Swedish humility tells me that things can always be made better than how I make them. I do know a little about a few things, but I am definitely no wizard in C# technicalities like interfaces, inheritance and other stuff making development more efficient, so going open source with my little library was the only way to go. I hoped that if anyone actually picked it up, it might go from my little hack to a really professional library.

You can find it here: https://github.com/rappen/JonasPluginBase

The repository contains two projects, one containing “the library” and one containing some sample plugins and custom workflow activities.

 

How does it work?

I would like to think that this sentence says everything you need to know:

Inherit class JonasPluginBase instead of implementing the IPlugin interface.

But maybe a few more words are necessary after all.

Adding the package

The JonasPluginBase library is available as a package on NuGet. Add it to the project, and install updates to dependent packages if there are any available.

image

To make this library available on the server executing the plugin, you can use ILMerge to merge your plugin assembly with the JonasPluginBase assembly. An instruction is published here, and a sample post-build event could look like this:

ILMerge.exe /keyfile:..\..\mysignature.snk /target:library /copyattrs /targetplatform:v4,"C:\Windows\Microsoft.NET\Framework\v4.0.30319" "/out:$(TargetName).Merged.dll" "$(TargetFileName)" "JonasPluginBase.dll"

I think it is convenient to include ILMerge by adding the ILMerge NuGet package to my project and adding the ILMerge.exe from the package folder as a reference, with Copy Local = True. When this is done you can use the build event sample above.

Creating a plugin

First you need to implement the abstract method Execute.

image

This method gets a parameter of type JonasPluginBag. This is a bag packed and ready for anything (well almost) you might want to do in your plugin!

The techy details

This class exposes the following public methods and properties:

        public IPluginExecutionContext PluginContext { get; }
        public IWorkflowContext WorkflowContext { get; }
        public JonasServiceProxy Service { get; }
        public JonasTracingService TracingService { get; }
        public Entity TargetEntity { get; set; }
        public Entity PreImage { get; } 
        public Entity PostImage { get; }
        public Entity CompleteEntity { get; }

        public void Trace(string format, params object[] args);
        public void TraceBlockStart();
        public void TraceBlockStart(string label);
        public void TraceBlockEnd();
        public string GetOptionsetLabel(string entity, string attribute, int value);

Since this library contains a base class for custom workflow assemblies too, the Bag contains properties for both PluginContext and WorkflowContext. Obviously only one of them is assigned a value, depending on where the library is used.

The Service property is of type JonasServiceProxy, which implements the IOrganizationService interface. The benefit of using this type is that it automatically writes detailed timing information to the plugin trace log when using it’s methods.

Similarly the JonasTracingService automatically adds timestamps to each line written using the Trace method.

Then there are four properties of type Entity. These are all extracted from the context, where TargetEntity is the entity found using code similar to context.InputParameters[“Target”] as Entity. The PreImage and PostImage contains the first (if any) item in the PreEntityImages and PostEntityImages collections of the context.

The CompleteEntity contains a copy of the Target entity (not direct reference) where attributes from the PostImage and the PreImage have been added, to comprise “as much information possible” about the record that triggered the plugin. This is very useful when you just want to know the current value of attributes, without concern if they were changed in this transaction or not, and not having to if-else look for it in the target and images, and for the sake of performance you don’t want to retrieve the record from the database again.

image

The Trace method found in the bag is simply a shortcut to writing to the log using JonasTracingService.Trace. As mentioned above, this method adds timestamps, but also adds indentation defined by the following methods.

TraceBlockStart adds a trace block to the trace in the format BEGIN <name of calling method>. Each line written to the trace after this will be indented, until next call to TraceBlockEnd. This feature makes it easier to read the log and follow the execution with an appearance resembling a causality chain, if used consistently. The TraceBlockStart overload that takes a label parameter can be used to “name” the block something else than default.

Finally, the GetOptionsetLabel method is an example of how to encapsulate common methods and making them available through the base class structure.

A simple example

A simple plugin using the JonasPluginBase is available in this example on GitHub.

Resulting trace log for an account that moved from the 5th Floor to the 6th Floor, where one of the company’s contacts is defined to inherit the address:

23:48:03.782 *** Enter
23:48:03.798 [JPB] Context details:
  Step:  eXtremePlugins.AccountAddressToContacts: Update of account
  Msg:   Update
  Stage: 40
  Mode:  0
  Depth: 1
  Corr:  81762ffb-db01-4400-93ad-e47193686626
  Type:  account
  Id:    15cc51ab-58fe-e611-80fb-5065f38b2601
  User:  9cc6ecfd-cce1-4bd5-8832-20e476f776e1

23:48:03.861 [JPB] Incoming account
  accountid          = 15cc51ab-58fe-e611-80fb-5065f38b2601 (Guid)
  address1_line2     = 6. Floor (String)
                  PRE: 5. Floor

23:48:03.876 BEGIN GetContactsInheritingAddress
23:48:03.876   Adding filter for address1_line1 = 9068 Muir Road
23:48:03.876   Adding filter for address1_line2 = 5. Floor
23:48:03.876   Adding filter for address1_line3 = 
23:48:03.876   Adding filter for address1_city = Los Angeles
23:48:03.876   Adding filter for address1_stateorprovince = KA
23:48:03.876   Adding filter for address1_postalcode = 20593
23:48:03.876   Adding filter for address1_country = U.S.
23:48:03.876 END GetContactsInheritingAddress
23:48:03.892 [JPB] RetrieveMultiple(contact)
23:48:04.064 [JPB] Retrieved 1 records in: 83 ms
23:48:04.064 BEGIN UpdateContacts
23:48:04.079   [JPB] Execute(RetrieveEntity)
23:48:04.548   [JPB] Executed in: 478 ms
23:48:04.548   [JPB] Metadata retrieved for contact
23:48:04.548   [JPB] Primary attribute is: fullname
23:48:04.548   Updating contact: Sidney Higa (sample)
23:48:04.548   Checking: address1_line1
23:48:04.564   Checking: address1_line2
23:48:04.564   Setting address1_line2 = 6. Floor
23:48:04.564   Checking: address1_line3
23:48:04.564   Checking: address1_city
23:48:04.564   Checking: address1_stateorprovince
23:48:04.564   Checking: address1_postalcode
23:48:04.564   Checking: address1_country
23:48:04.579   [JPB] Update(contact) 7bcc51ab-58fe-e611-80fb-5065f38b2601 (9 attributes)
23:48:04.704   [JPB] Updated in: 137 ms
23:48:04.704 END UpdateContacts
23:48:04.704 [JPB] Internal execution time: 845 ms
23:48:04.704 *** Exit

Note that all log lines have a timestamp, and that log lines originating from within the JonasPluginBase library itself have a prefix [JPB].

 

I know I’m not alone

Being completely aware of the fact that I am not alone with the approach of creating supporting libraries – I guess most developers or companies that regularly produce plugins for Microsoft Dynamics CRM (365) do it – this is an example of a minimalistic base class for plugins. I am sure there are a lot more advanced libraries and templates out there.

But I like things simple.

You can always create more features, more “must haves” that in the end might be used so rarely that you could just as well have implemented them in the few plugins that need them rather than making it part of your framework on which you base all plugin development.

I honestly think this is the hardest part of building frameworks like this – to keep them light weight and have them focus on doing the most common tasks that we do not want to deal with over and over in our code.

Finally I am really eager to know what you think – so please comment on this post, or fork the repository for JonasPluginBase and create a pull request with suggested improvements!

Do you love early bound? What changes would you like to see in this library?

4 comments:

  1. I don't like the SDK Plugin templates too (the one created by the developer toolkit for example) as I start always with an emply class that just inherits IPlugin.
    If I can comment on 2 things (actually 3):
    the first is that you need to have also the sdk workflow dll in a plugin project (because you have the WorkflowContext property), not a big deal but I don't include references that are not used.
    the second is the nuget package, because the project is opensource, why don't drop directly the .cs file instead of a dll? in this way you don't force the users to use ILMerge.
    the third is the naming, I personally don't have problems to use JonasPluginBase, my superiors may have :) but if the nuget gives the .cs a user can do a refactory by himself.
    and f course great project as always!

    ReplyDelete
    Replies
    1. Thanks for the feedback Guido :)

      And of course - I completely agree with all your three points.
      Having both NuGet and GitHub "distribution" gives you the choice to do it either way, I guess :)
      And the naming.... well, it sure is a good indication how this lib started ;) I was considering doing a global rename of files, classes and everything before publishing yesterday, but it felt like too much work. Perhaps I should reconsider that...

      Delete
  2. I think you and I need to do an XrmVirtual meeting where we compare our two base classes. They sound very similar! Mine are in the DLaB.Xrm nuget packages. My favorite features is auto prevention of duplicate function calls...

    ReplyDelete