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.
Then 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:
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.
I would like to think that this sentence says everything you need to know:
Inherit class
JonasPluginBase
instead of implementing theIPlugin
interface.
But maybe a few more words are necessary after all.
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.
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.
First you need to implement the abstract method Execute
.
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!
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.
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 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]
.
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?
Labels: Community, Open Source, Plugin