Kipon SOLID plugin API
The Kipon SOLID plugin framework is building on top of the SDK provided by Microsoft. This article will only go in details on what the Kipon SOLID plugin framework is adding to the plate. You can read the "Learn" article on this site, and the Solid article to get some understanding, but you will also need to diig deep in the standard documentation provided by Microsoft. Below the starting point that can help you there.
Dynamics 365 CE plugin documentation.
To get API details for each class and interface, please navigate to the detailed documentation page here:
Example
Namespace: Kipon.Xrm
In the Kipon folder of your project you will find Kipon.Xrm.cs. This is a single merged source-file with all the functionality provided by this framework. To work with the framework, you will address this namespace. Tools, Licens and more require this namespace to be used. Tools and more will not work, if you change this namespace, and it would be a license violation. Even though the source is there directly as a source file, consider this folder to be "an external DLL". The reason it is not provided as an external is that it would put constrains on your deployment model, and force you to use ILMerge or similar to be able to deploy your assembly to Dynamics 365. I am not a big fan of that, and this framework aims to keep the development and deployment process as simple as possible.
Abstract plugins
Kipon.Xrm.BasePlugin
Most plugins will extend the Kipon.Xrm.BasePlugin
namespace Kipon.PluginExample.Plugins.Account.v0
{
// purpose: Map entity events related to the account entity to appropriate services
// reason: changes in how account events should map to the services.
public class AccountPlugin : Kipon.Xrm.BasePlugin
{
public void OnPreCreate(Entities.Account target, Microsoft.Xrm.Sdk.IOrganizationService orgService)
{
}
}
}
The Kipon.Xrm.BasePlugin is an implementation of the Microsoft.Xrm.Sdk.IPlugin. Most of your plugins should extend the class. You must create one or more public methods in the plugin, following a naming convention to hook your plugin to certain events on the Dynamics 365 platform. As such, the Kipon.Xrm.BasePlugin is using Duck method-type pattern match to resolve the map between a plugin method, and the Dynamics 365 CE event. If a method is public, and it follows the naming convention, it is suppose to be called by the plugin. The Kipon SOLID deployment tool will ensure that each plugin is registred on the appropriate events on deployment.
Method name should follow this naming standard:
Stage
- Validate (transaction might not have been started yet)
- Pre (In transaction before data is pushed to the database)
- Post (In transaction after data has been pushed to the database)
Event
- Create
- Update
- Delete
- Retrieve
- RetrieveMultiple
- SetState (depreciated .. do not use)
- Associate
- Disassociate
- AddMember
- RemoveMember
- AddListMembers
- .. more CRM standard actions
- Custom actions
Async. (Will start in a new transaction)
Mark the plugin to push the process to the async. queue. This only applies to Post events.
Parameters
public void OnPreCreate(Entities.Account target, .....)
The parameters defines what to listen for. If we inject the stronly type: Entities.Account class, that will tell the framework that this event hook should be called whenever an Account is created.
Be carefull and make sure to add a parameter that maps to an entity. If a method on a plugin is following the naming convention (it looks like a duck), and no parameter can be mapped to an entity, the plugin will be registred without a target, and the consequence is that it will run for ALL entities. This is a supported scenario, but it should be used with extreame care, it can have significant performance impact on the system.
In addition to the target, you can inject preimages, postimages and any services defined in you code.
Kipon.Xrm.VirtualEntityPlugin
The VirtualEntityPlugin has been created to make it easy to implement the plugin infrastructure needed by virtual entities. Plugins extending the VirtualEntityPlugin should have a set of Two methods to support the retrieve an retrieve multi events
using System;
namespace Kipon.PluginExample.Plugins.Virtual
{
public class VirtualEntityPlugin : Kipon.Xrm.VirtualEntityPlugin
{
public Microsoft.Xrm.Sdk.Entity OnRetrieve(Guid primaryentityid, string primaryentityname)
{
return new Microsoft.Xrm.Sdk.Entity { LogicalName = primaryentityname, Id = primaryentityid };
}
public Microsoft.Xrm.Sdk.EntityCollection OnRetrieveMultiple(string primaryentityname, Microsoft.Xrm.Sdk.Query.QueryExpression query)
{
var result = new Microsoft.Xrm.Sdk.EntityCollection();
for (var i = 0; i < 10; i++)
{
result.Entities.Add(new Microsoft.Xrm.Sdk.Entity { LogicalName = primaryentityname, Id = Guid.NewGuid() });
}
return result;
}
}
}
Take a look at above example plugin, extending the VirtualEntityPlugin. The first method OnRetrieve is taking the primaryentityid and primaryentityname as parameters, and then it builds up an instance of the Microsoft.Xrm.Sdk.Entity and returns it. The second method OnRetrieveMultiple is only asking for the primaryentityname but then get the query provided by the Dynamics 365 infrastructure injected, so it can use the query to resolve what data to return.
The example above is ofcause a 100% simplicifaction of what you would normally do when implementing virtual entities, but it is a working example that shows how easy you can get started with this powerfull feature of the Dynamics 365 platform.
Services
Dynamics 365 SDK Services
The following standard services from the Dyanmics 365 SDK has been prepared for injection in any plugin event method and in any service. If the element is marked Avoid, you can inject it, but from a Kipon SOLID perspective there is a better alternative.
-
Microsoft.Xrm.Sdk.IOrganizationService Avoid.
In most cases you will not use the organization service directly. Instead you will make operation through a unit-of-work, that under the hood is using the organiation service. -
Microsoft.Xrm.Sdk.IPluginExecutionContext Limit.
The Kipon.Solid framework has been put in place to make access to the context more explicit. Most of what you can get out of the context can be injected as strongly typed interfaces instead. - Microsoft.Xrm.Sdk.ITracingService
Inject this service if you need to add tracing to your plugin. -
Microsoft.Crm.Sdk.IOrganizationServiceFactory Avoid.
In most cases, you should not create instance of the IOrganizationService your self. In most cases you will inject a unit-of-work instead.
Kipon.Xrm Services
The following services is added by the Kipon SOLID plugin framework:
-
Kipon.Xrm.IPluginContext Avoid
This service is used by the framework to solve task such as resolving plugin methods to events, inject appropriate data and service into the plugin method. It should not be nessesary to inject this service anywhere. - Kipon.Xrm.IService.. The intension of this interface is NOT to inject it in any plugin or service. The api can be implemented by any of your own services, and thereby give you the opportunity to "reset" your service between method invocations. If several methods are registred for the same stage, same entit and same plugin, the Kipon SOLID framework will call these methods one by one, in the order defined. Services are reused in this situation, and as such, the state of the service is retained (only within same Stage and Same Event and same Plugin). If you need to clear something between each step, then implement the Kipon.Xrm.IService interface and add your "cleanup" logic to the OnStepFinalized method.
Unit of work - and the IOrganizationService
Building plugins for Dynamics 365 CE is all about interacting with the datamodel of the platform (dataverse). The standard IOrganizationService is actually providing the basic CRUD operations, based on the base class Microsoft.Xrm.Sdk.Entity. An entity is also a table in Dynamics 365 CE. The property "LogicalName" is providing the unique name of the entity, ex. "account", and you can operate on the Attributes property or the indexer property of the entity to get and set fields for the entity, ex account["name"] or account.Attributes["name"]. The first one is "safe" meaning that you can call if even if name is not in the indexer, while operating on the Attributes property, it works like a dictionary. If you address a field that is not in collection, it will throw a key not found exception. But all this is actually not that important. Because we do not wish to work directly on the Entity, beause when working on the entity, we must know and hardcode the fields names as string and we need to know the datatype and cast the values in an appropriate manner. That is an error prone process to do for the entire model. Instead we wish to work on strongly typed objects with appropriate names and types, so instead of writing:
string name = entity["name"] as string;
We would like to be able to write:
string name = account.Name;
The first example is assuming we work on an instance of Microsoft.Xrm.Sdk.Entity. The secound is working on a generated proxy class for account. The code generated by the crmsvcutil service (Entities\generate.cmd) is something like this:
namespace Kipon.PluginExample.Entities
{
public partial class Account : Microsoft.Xrm.Sdk.Entity
{
public const string EntityLogicalName = "account";
...
public Account() : base(EntityLogicalName)
{
}
[Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("name")]
public string Name
{
get
{
return this.GetAttributeValue<string>("name");
}
set
{
this.OnPropertyChanging("Name");
this.SetAttributeValue("name", value);
this.OnPropertyChanged("Name");
}
}
...
}
}
The above example is an extract from the generated proxy classes. As you can see, for each entity, a proxy class with corresponding name will be generated extending the Microsoft.Xrm.Sdk.Entity class, and for each field on the entity class (in this example the account), a strongly typed property with an appropriate name will be generated. So instead of working on entity["name"] as string, we can now work on account.Name, and be sure it will use the correct logical attribute name and return the correct type (string). Also be aware that the class is generated with the partial flag. This means that we can add additional code to the Account class in our project.
The generated Organization Service Context
Beside generating a class for each entity, the crmsvcutil also generates a Context. The context has and IQueryable property for each entity.
namespace Kipon.PluginExample.Entities
{
public partial class PluginExampleContextService : Microsoft.Xrm.Sdk.Client.OrganizationServiceContext
{
/// <summary>
/// Constructor.
/// </summary>
public PluginExampleContextService(Microsoft.Xrm.Sdk.IOrganizationService service) :
base(service)
{
}
/// <summary>
/// Gets a binding to the set of all <see cref="Kipon.PluginExample.Entities.Account"/> entities.
/// </summary>
public System.Linq.IQueryable<Kipon.PluginExample.Entities.Account> AccountSet
{
get
{
return this.CreateQuery<Kipon.PluginExample.Entities.Account>();
}
}
...
}
}
As you can se, the context adds a queryable property for each entity, and it extends Microsoft.Xrm.Sdk.Client.OrganizationServiceContext, meaning that you can use the context to do CRUD operations on the dataset.
When it comes to do development of plugins using the Kipon solid framework, you should basically forget all about the generated context. You need to know it is there because actual implementations of the interface you are gonna inject in your services i relying on the implementation, but that's it. The framework does not support that you inject the context anywhere (it would be an anti-pattern). The only thing you should be aware of, is the "caching" nature of the underlying Microsoft.Xrm.Sdk.Client.OrganizationServiceContext. If you fetch an entity from the context, and only include name and createdon in your list of fields to fetch. Each instance fetch in that query will be cached within the OrganizationServiceContext, so if you later request the same entity on the same instance of the context, it will return the instance from the cache, even if you new query has additional fields. The end result might be that you think a field is null on an instance, but it is not, some other code has fetch the instance, without doing proper cleanup, so you see the cached version of the entity instance.
Caching in the crm-context from version 1.0.9: From version 1.0.9 the caching nature of the standard context has been removed. Because it is the SDK from Microsoft that actually put in any object in the cache, fetch with a Linq query on the context, The Kipon framework is wrapping the IQueryables and remove it instantly. Due to the shared nature og the IUnitOfWork implementation in the framework, the caching is causing more problems than goodies, so we better not have it at all.
Kipon.Xrm.IUnitOfWork, My.Plugins.Entities.IUnitOfWork, My.Plugins.Entities.IAdminUnitOfWork
The Kipon.Xrm.IUnitOfWork interface is a simple wrap of the Microsoft.Xrm.Sdk.IOrganizationService. The only thing it adds to the plate is some "Detach" methods and a generic based method to execute requests. The first part is the relevant part. While IOrganizationService is not aware and also not interested in the fact that we have a context to handle "database state" in our application, this interface is adding that awareness. At same time it serves as a base interface for the full database unit-of-work, generated by the Kipon.Solid part of the crmsvcutil tool
namespace Kipon.PluginExample.Entities
{
public partial interface IUnitOfWork : Kipon.Xrm.IUnitOfWork
{
Kipon.Xrm.IRepository<Account> Accounts { get; }
....
}
}
So My.PluginExample.Entities.IUnitOfWork has a repository property for each entity in the datamodel.
So what is a IRepository. A repository is providing CRUD operations and query operations for a single entity. So you can use it to perform operations against the specific type of entity. As an example, the Accounts property of the IUnitOfWork will return an instance of IRepository specific for the Account class and provide the following functionality:
Kipon.Xrm.IRepository<T>
-
IQueryable<T> GetQuery()
Get a query object where you can query directly on the strong type, ex.
uow.Account.GetQuery().Where(a => A.Name == "Kipon").ToArray() - T GetById(Guid id)
Get the full instance of the object with the exact key. Use with care, it will fetch all properties and can have performance penelty. - void Add(T entity)
Will trigger a "Create" event of the entity and attach the created instance to the context. - void Update(T entity)
Will trigger an "Update" event for entity. - void Delete(T entity)
Will trigger a "Delete" event for the entity -
void Attach(T entity)
will add the entity the the context cache. - void Detach(T entity)
Remove the entity instance from the context cache if it is in there.
Instead of injecting the Microsoft.Xrm.Sdk.IOrganizationService you can now inject the generated unit-of-work, and get access to a type safe and interface based reflection of the datamodel, and the good news is, you don't even have to implement it. The Kipon Solid plugin framework also generated an implementation of the interface. The implementation is based on the generated Context describe above, but at same time it hides it away, and allow us to work on strongly typed classes, based on interfaces rather than implementations.
So you could ask "should I just inject my project generated IUnitOfWork whenever i need to access data?". The short anwer is, that it is possible, it will work, but it is not the most SOLID solution ever.
Every time you inject the full IUnitOfWork, you basically say "I need to know everything about the database, each and every entity, and potentially i will create, update, delete, query all of them". At least from an api-point of view, that is what your are stating when injecting the IUnitOfWork. That is asking for a lot, or putting a lot on your plate. Are you really gonna eat all that.
If i should not inject the full IUnitOfWork, what should i Inject. The short answer is "the parts of it your are gonna eat"
Lets say your service implementation needs to query account, then inject an IQueryable<Account>.. That is perfectly possible and valid. Under the hood you will actually get the instance from the GetQuery() method of the IUnitOfWork, but from an Api point of view, you are much more precise now. Your unit-test team now only have to mock the Account queryable to create a unit test for your service, they don't have to mock the entire database of the system. On top of that, you have explicitly stated in your implementation of a service, that this implementation need to query some account data.
But what if I need to also create or update instance of the account. Easy peasy, just inject Kipon.Xrm.IRepository<Account> instead. Now you have an account specific repository and you have stated to the unit-test team, that you are gonna query, and do CRUD operations on the account instance.
Need to access more than one IQueryable or IRepository, just inject more than one. You are not limited in the number of parameters you can inject into a plugin event method, and you are not limited in the number of parameters you can inject into a service constructor implementation.
Finally you can always create a "jummy jummy" interface. A jummy jummy interface is a simple interface that has the pure purpose of defining what part of the burger (a larger interface or an implementation) we are interested in. The IUnitOfWork is the full burger with everything. It is implemented by the auto generated class CrmUnitOfWork, and even that class is partial.
using System.Linq;
namespace Kipon.PluginExample.Entities
{
public partial class CrmUnitOfWork : ISpecialUnitOfWork
{
IQueryable<Contact> ISpecialUnitOfWork.Contacts => this.Contacts.GetQuery();
}
public interface ISpecialUnitOfWork
{
Kipon.Xrm.IRepository<Account> Accounts { get; }
IQueryable<Contact> Contacts { get; }
}
}
Above interface ISpecialUnitOfWork is defining a specialized version of the unit of work, only exposing the Accounts repository and the Contacts query. This version of the unit of work interface is clearly asking for less, and therefore easier to mock. We could of cause create a service and in that service inject the full unit of work, but the generated implementation of the IUnitOfWork is also partial, so we can simply let that cat also implement our specialized unit-of-work. The Accounts repository is already in there, and the Contact queryable is easy to implement explicitly.
By using above "jummy jummy" pattern we get a smaller interface that is safe to inject in any service that asks for less. It makes the dependencies between components more precise and is putting a very limited overhead on the implementation, because most of what we need is already there, generated by the tool.
Kipon shared services
A small number of standard services has been put in place to impl. services to solve common problems. These services are described below.
Kipon.Xrm.ServiceAPI.INamingService
INamingService can be injected into any plugin method or service constructor to get a simple way of resolving the name of a reference property. If you need the name of the record, represented by the Microsoft.Xrm.Sdk.EntityReference (property Name) the problem is, that the Name value is not always populated, hence it will be null, event though the reference is holding a reference to an entity record.
To provide a simple efficient way to solve this problem, the framework is offering the INamingService. The naming service is using metadata based code generated information about each entity to know witch is the primary name property of any entity, and it is using the Name property of the entity reference it self as a "caching" mechanism, so if the naming service is called twice for the same entity reference, the name will only bee lookedup once in Dynamics 365 CE data source:
using Kipon.Xrm.ServiceAPI;
namespace Kipon.PluginExample.Services
{
public class UseNameServiceExample : ServiceAPI.ISomeInterface
{
private readonly INamingService namingService;
public UseNameServiceExample(Kipon.Xrm.ServiceAPI.INamingService namingService)
{
this.namingService = namingService;
}
public void SetDescriptionFromPrimaryContactName(Entities.Account target)
{
if (target.PrimaryContactId != null)
{
target.Description = namingService.NameOf(target.PrimaryContactId);
}
}
}
}
The above example is demonstrating the usage of the service. we are injecting the service by its api into the constructor of our specific service, and in the SetDescriptionFromPrimaryContactName method, we are using the service method "NameOf(...)" to get the name of the primary contact, and put it into the description field of the account.
Kipon.Xrm.ServiceAPI.IEntityCache
The IEntityCache is a "system" interface, implemented by the crmsctutil generated xrm-context class, used by the underlying framework to prevent the context from caching entities. You should not used the interface explicitly or think to much about it.
Pipeline target and entities
A plugin typically has a target object. The target object is the record that is being created/update or deleted, or the target property of a bounded action.
Target can be inject into a plugin method by naming convention, or by creating interfaces that represent a part of the target (entity)
Each plugin method hook [ex. OnPreUpdate(parameters)] needs to identify the relevant entity type from the set of parameters. Parameters must be consistent, meaning if you inject a target of type Account, you cannot inject a preimage of Type Contact. But injecting the full proxy class of an entity (ex. Account) is actually not the best choice. First of all, you are not explcit in your interface on witch part of the proxy class you are gonna work on, secondly, especially when it comes to target in an update event, you are saying to the Dynamics 365 platform that your code must run on any update, any field on the entity:
Myentity target
Describe a plugin parameter as the target by namingconvention.
namespace Kipon.PluginExample.Plugins.Account.e0
{
public class AccountPlugin : Kipon.Xrm.BasePlugin
{
public void OnPreUpdate(Entities.Account target)
{
}
}
}
Take a look at above example. We inject an Account instance into our plugin, names the parameter "target", and that way we tell the platform that we expect to get the target as an account instance to be injected here. It looks so peacefull but it is not. There is a performance penelty. Every time an account is updated, for what-ever-reason, this plugin will be started and executed. If the plugin implementation will only address certain fields, and have work to do in certain situations, the interface to be injected should reflect this.
Kipon.Xrm.Target<T>
To support this, the Kipon Solid platform is adding an implementation of Kipon.Xrm.Target<T> where T is the proxy class, ex .Account to each and every entity proxy class. The interface is actually empty and has a declarative purpose only. It states, any class implementing this interface is the Target of a pipleline. For each entity, the Kipon Solid generate tool will create a unique interface, ex. IAccountTarget, IContactTarget etc. The purpose of this interface is to be the Base interface for any "jummy jummy" interface, defining what part of the entity we are interested in.
Lets take a case, where we are only interested if the name of an account was changed:
namespace Kipon.PluginExample.Entities
{
public partial class Account
: Account.INameChanged
{
public interface INameChanged : IAccountTarget
{
string Name { get; set; }
}
}
}
Now, instead of injecting Entities.Account, we can inject Entities.Account.INamedChanged, and we don't even need to follow the "target" naming convention anymore we can call the paramter "account" if we like because the fact that INamedChanged extends the IAccountTarget interface is explicitly defining that the purpose of this interface is to be the target of an account pipeline. (my recommendation on naming the parameter is to stick to target for consistency). When the plugin is deployed, it will tell the underlying platform, that this plugin method should only be called if the account.Name is part of the updated payload. The plugin taking advantage of the new interface will look like below:
namespace Kipon.PluginExample.Plugins.Account.e1
{
public class AccountPlugin : Kipon.Xrm.BasePlugin
{
public void OnPreUpdate(Entities.Account.INameChanged target)
{
}
}
}
The signature of the method even became easier to read. It is now very clear that this method will be called whenever the name of an account is updated. We can diig into the requirement specification and find out what we expect on such event.
Kipon.Xrm.ITarget - marker interface for multi entities implementing same interface
Kipon.Xrm.ITarget is a marker interface that indicates that the underlying implementation is the target of a plugin invocation. The underlying implementation must inherit from Microsoft.Xrm.Sdk.Entity. The purpose of the interface is to allow several entities to implement same interface without having any strong bindings to the actual entity.
namespace Kipon.PluginExample.Models.v6
{
public interface IPhonenumberChanged : Kipon.Xrm.ITarget
{
string[] Fields { get; }
Microsoft.Xrm.Sdk.AttributeCollection Attributes { get; }
}
}
Above example is such an interface. The purpose is to align phonenumbers for one or more fields in an entity.
An implementation could look like this
using Kipon.Xrm.Extensions.Sdk;
using Kipon.Xrm.Attributes;
namespace Kipon.PluginExample.Entities
{
[TargetFilter(
typeof(Models.v6.IPhonenumberChanged),
nameof(Account.Telephone1),
nameof(Account.Telephone2),
nameof(Account.Telephone3))]
public partial class Account : Models.v6.IPhonenumberChanged
{
string[] Models.v6.IPhonenumberChanged.Fields
=> this.TargetFilterAttributesOf(typeof(Models.v6.IPhonenumberChanged));
}
}
For a full explanation of the example above, please read the SOLID section on this page. The short version is: the Attributes property of an entity is part of the Microsoft.Xrm.Sdk.Entity class, and as such you do not need to implement it again, The Fields property is taking advantage of one of the Kipon.Xrm.Extensions.Sdk method, that allow extractions target fields related to a specific interface, and finally we decorate the class with a [TargetFilter] attribute, specific for the IPhonenumberChanged interface. That way we tell the underlying framework that target filter for a target of type IPhonenumberChanged is the 3 phonenumbers of the account.
Target by EntityReference (Delete and Bounded actions)
Whenever the Dynamics 365 platform is invoking a Delete event (delete a row of a certain type, ex. delete an account), the target is an instance of Microsoft.Xrm.Sdk.EntityReference
This framework is solving this problem, by generating a TargetReference class for each entity, ex. For the account entity, you will find a class call Entities.AccountReference.
namespace Kipon.PluginExample.Plugins.Account.e2
{
public class AccountPlugin : Kipon.Xrm.BasePlugin
{
public void OnPreDelete(Entities.AccountReference target)
{
}
}
}
The AccountTargetReference is having a reference to the EntityReference, but this signature allow the framework to know, that you are listening for account deletes only.
Target for Actions
Target for actions also support the TargetReference implementation for bounded actions same way as the Delete operation
Other input parameters of an action can be injected by naming-convention. So if you have an input parameter in your action call "SearchString" of type string, you can inject that parameter into your plugin by adding a parameter to your methd OnPost[logical-action-name](..., string SearchString, ...)
But there is a better way to get a more holistic interface for actions.
First of all, you need to setup the action in the <actions> section in the Entities\filter.xml. When this has been put in place you must regenerate the entities. For the defined actions this will generate a Request interface and a Response class that reflects the definition og the action input and out parameters
<actions><action name="AccountCountContacts">kipon_AccountCountContacts</action></actions>
If you have the above actions in the Entities\filter.xml file, and Myaction has been defined in CRM, ex. as below:
Based on above action definition, you can implement the plugin for the action as below:
namespace Kipon.PluginExample.Plugins
{
public class AccountCountContactsPlugin : Kipon.Xrm.BasePlugin
{
public Actions.AccountCountContactsResponse
OnPostkipon_AccountCountContacts(Actions.IAccountCountContactsRequest request)
{
return new Actions.AccountCountContactsResponse
{
Count = 10,
AMoney = new Microsoft.Xrm.Sdk.Money(500M)
};
}
}
}
The tool has generated a strongly type response type, that allow you to let your plugin return the result as a strongly typed object. The tool also generated the Actions.IAccountCountContactsRequest input interface. This interface has a property Target that is an instance of EntityReference, pointing to the account for witch this action has been invoked, and on top, it has a strongly type property for each input parameter defined by the action.
Pre- and Post images
The dynamics 365 plugin platform allow you get information about the actual entity, how it looked before the operation (preimage) and how it looked after the operation (postimage). Please go into details in the "Learn" section to see relevance of images mapped to event and stages.
Images by naming convention
Images can be injected into you plugin by naming convention. If a parameter is named "preimage", the framework assume you wish to have an instance of the preimage entity. Same goes for "postimage". The parameter class type should eather be the strongly typed entity, or an interface that is implemented by the strongly typed entity. You can only request images that has same entity type as the target. So if you are listening to an account type, you can ask for the preimage of the account, and the postimage of the account.
namespace Kipon.PluginExample.Plugins.Account.api0
{
public class AccountPlugin : Kipon.Xrm.BasePlugin
{
public void OnPreUpdate(Entities.Account target, Entities.Account preimage)
{
// do something on target, if you need to know the past, look into preimage
}
}
}
Above code snipper demonstrates the very basics of calling for a preimage. As for target, injected the full preimage of the account is not the best choice. You are proberbly asking for more than you need. As a side effect you also tell the underlying platform, than whenever this method is called, it need to know evertying about how the account looked before this operation started. That can have a performance penelty, because Dynamics 365 will have to provide you with each an every field on the entity, even if you do not need all the knowledge.
As for target - there is an interface based alternative. For each and every entity included in the Entities\filter.xml, a I[EntityLogicalName]Preimage, I[EntityLogicalName]Postimage and a I[EntityLogicalName]Mergedimage. Please look below for a description of the mergedimage. For now lets focus on the I[EntityLogicalName]Preimage. It is a "marker" interface, stating that it represent the part of an entity image you are interested in, in regards to its pre stage.
namespace Kipon.PluginExample.Entities
{
public partial class Account
: Account.IPreNameChanged
{
public interface IPreNameChanged : IAccountPreimage
{
string Name { get; }
}
}
}
Above interface IPreNameChanged extends the IAccountPreimage and as such it indicates that it is expected to be implemented by the Account entity, and we added the inteface as a sub interface class of the account, and made sure the class declaration implements the interface. So now we can ask for a preimage where Dynamics 365 only have to provide us the pre value of the Name field. We only get what we ask for, and we should not ask for more than we need.
namespace Kipon.PluginExample.Plugins.Account.api1
{
public class AccountPlugin : Kipon.Xrm.BasePlugin
{
public void OnPreUpdate(
Entities.Account.INameChanged target,
Entities.Account.IPreNameChanged preimage)
{
// do something on target, if you need to know the past, look into preimage
}
}
}
Above implementation taks advantage of a dedicated target interface for the name changed on account, and a dedicated preimage interface that expose the value of the Name of the account, just before this update event.
Post images
Post images works as preimages, and has a similar interface for each entity I[EntityLogicalName]Postimage.
I rarly use post images, but if you need to know the value of a derived field, ex. fullname of a contact after update, Post images can be usefull
Remember, you can only ask for post images in post state, and only for relevant events, post image on a delete does not make sense, just to give you and example.
Merged image
A merge image is something the Kipon.Xrm framework adds to the plate. It is a combination of preimage and target. If a field is in the target, you will get the value giving by the target payload. If the field is not in the target, you will get the value from the preimage. Lets say you have an entity with column my_a, my_b, my_c , and you need to make a very special calculation, based on the 3 values, and this calculation needs all of the values, if just one of these values are changed, you will need to know the "new a", "new b", "new c", and if it was only a that was actually changed, "b" i just what we already knows and same for "c". It this case, the mergedimage is perfect, because you can listen to all three fields, and if an update happens, you will get all 3 values, regardsless of the fact that only one of them was changed.
On top of that, mergedimage even behaves as target (in validate and pre state only). This means, if you do any update of entity properties to the merged images, the change will also go into the actual target of the event. Same goes for update to target. Any update to target entity, will reflect in the mergedimage.
using Kipon.Xrm.Attributes;
namespace Kipon.PluginExample.Entities
{
public partial class Contact :
Contact.INameChanged
{
public interface INameChanged : IContactMergedimage
{
[TargetFilter]
string FirstName { get; }
[TargetFilter]
string LastName { get; }
[TargetFilter]
string MiddleName { get; }
string Address1_Country { get; }
}
}
}
Above is an example of a merged image for contact, listening to any of the name fields changes, and also expose the value of the country. Only the properties with the [TargetFilter] flag will be added to the target filter. If an event happens, where only Address1_Country is changed, this event will not hit a method taking above interface as input, while if any of the name fields are changed, the method will be invoked, and all the fields will have values, eather populated from the target (if it part of the update), or from the pre-image, if not.
A merged image can be used as the target in a Create event, even though a create does not have a pre-image. This is to reduce the need for duplicate interfaces. In pre stage create, a mergedimage is the same as the target.
© Kipon ApS 2020, 2021, 2022, 2023, 2024. All content on the page is the property of Kipon ApS. Any republish or copy of this content is a violation. The content of this
site is NOT open source, and cannot be copied, republished or used in any context without explcit permission from the owner.