Last active
December 10, 2018 19:21
-
-
Save NickPl/5b652b4e99374df7ec84ae3f1b26a603 to your computer and use it in GitHub Desktop.
Blog XrmMtl - Essential Plugin base class
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Xrm.Plugins.Sandbox | |
{ | |
using Microsoft.Xrm.Sdk; | |
using System; | |
using System.Collections.ObjectModel; | |
using System.Globalization; | |
using System.Linq; | |
using System.ServiceModel; | |
using System.ComponentModel.Design; | |
/// <summary> | |
/// Base class for all Plugins. | |
/// </summary> | |
public class Plugin : IPlugin | |
{ | |
protected class LocalPluginContext | |
{ | |
internal IServiceProvider ServiceProvider | |
{ | |
get; | |
private set; | |
} | |
internal IOrganizationService OrganizationService | |
{ | |
get; | |
private set; | |
} | |
internal IPluginExecutionContext PluginExecutionContext | |
{ | |
get; | |
private set; | |
} | |
internal ITracingService TracingService | |
{ | |
get; | |
private set; | |
} | |
internal EntityReference Assignee | |
{ | |
get | |
{ | |
if (PluginExecutionContext.InputParameters.Contains("Assignee") && PluginExecutionContext.InputParameters["Assignee"] is EntityReference) | |
return (EntityReference)PluginExecutionContext.InputParameters["Assignee"]; | |
else | |
return null; | |
} | |
} | |
internal EntityReference EntityMoniker | |
{ | |
get | |
{ | |
if (PluginExecutionContext.InputParameters.Contains("EntityMoniker") && PluginExecutionContext.InputParameters["EntityMoniker"] is EntityReference) | |
return (EntityReference)PluginExecutionContext.InputParameters["EntityMoniker"]; | |
else | |
return null; | |
} | |
} | |
/// <summary> | |
/// To use with the Delete message | |
/// </summary> | |
internal EntityReference EntityReference | |
{ | |
get | |
{ | |
if (PluginExecutionContext.InputParameters.Contains("Target") && PluginExecutionContext.InputParameters["Target"] is EntityReference) | |
return (EntityReference)PluginExecutionContext.InputParameters["Target"]; | |
else | |
return null; | |
} | |
} | |
/// <summary> | |
/// Can be casted into IncidentResolution | |
/// </summary> | |
internal Entity IncidentResolution | |
{ | |
get | |
{ | |
if (PluginExecutionContext.InputParameters.Contains("IncidentResolution") && PluginExecutionContext.InputParameters["IncidentResolution"] is Entity) | |
return (Entity)PluginExecutionContext.InputParameters["IncidentResolution"]; | |
else | |
return null; | |
} | |
} | |
//internal OpportunityClose OpportunityClose | |
//{ | |
// get | |
// { | |
// if (PluginExecutionContext.InputParameters.Contains("OpportunityClose") && PluginExecutionContext.InputParameters["OpportunityClose"] is Entity) | |
// return ((Entity)PluginExecutionContext.InputParameters["OpportunityClose"]).ToEntity<OpportunityClose>(); | |
// else | |
// return null; | |
// } | |
//} | |
internal OptionSetValue State | |
{ | |
get | |
{ | |
if (PluginExecutionContext.InputParameters.Contains("State") && PluginExecutionContext.InputParameters["State"] is OptionSetValue) | |
return PluginExecutionContext.InputParameters["State"] as OptionSetValue; | |
else if (Target.Attributes.Contains("statecode")) | |
return Target.Attributes["statecode"] as OptionSetValue; | |
else | |
return null; | |
} | |
} | |
internal OptionSetValue Status | |
{ | |
get | |
{ | |
if (PluginExecutionContext.InputParameters.Contains("Status") && PluginExecutionContext.InputParameters["Status"] is OptionSetValue) | |
return PluginExecutionContext.InputParameters["Status"] as OptionSetValue; | |
else if (Target.Attributes.Contains("statuscode")) | |
return Target.Attributes["statuscode"] as OptionSetValue; | |
else | |
return null; | |
} | |
} | |
internal Entity Target | |
{ | |
get | |
{ | |
if (PluginExecutionContext.InputParameters.Contains("Target") && PluginExecutionContext.InputParameters["Target"] is Entity) | |
return PluginExecutionContext.InputParameters["Target"] as Entity; | |
else if (PluginExecutionContext.InputParameters.Contains("Target") && PluginExecutionContext.InputParameters["Target"] is EntityReference) | |
{ | |
// Just to mess with you Microsoft changed the type of the target on an assign. | |
var targetOnAssign = PluginExecutionContext.InputParameters["Target"] as EntityReference; | |
return new Entity(targetOnAssign.LogicalName) | |
{ | |
Id = targetOnAssign.Id | |
}; | |
} | |
else | |
return new Entity(PluginExecutionContext.PrimaryEntityName); | |
} | |
} | |
internal Entity TargetState | |
{ | |
get | |
{ | |
var entity = new Entity(); | |
if (EntityMoniker != null) | |
entity = new Entity() | |
{ | |
Id = EntityMoniker.Id, | |
LogicalName = EntityMoniker.LogicalName, | |
}; | |
//else if (OpportunityClose != null) | |
// entity = OpportunityClose.OpportunityId.ToEntity(); | |
else throw new Exception("TargetState can't find the type of the entity"); | |
entity.Attributes.Add("statecode", State); | |
entity.Attributes.Add("statuscode", Status); | |
return entity; | |
} | |
} | |
/// <summary> | |
/// Get the first preImage | |
/// </summary> | |
internal Entity PreImage | |
{ | |
get | |
{ | |
return PluginExecutionContext.PreEntityImages != null && PluginExecutionContext.PreEntityImages.Count >= 1 ? PluginExecutionContext.PreEntityImages.FirstOrDefault().Value : null; | |
} | |
} | |
/// <summary> | |
/// Get the first postImage | |
/// </summary> | |
internal Entity PostImage | |
{ | |
get | |
{ | |
return PluginExecutionContext.PostEntityImages != null && PluginExecutionContext.PostEntityImages.Count >= 1 ? PluginExecutionContext.PostEntityImages.FirstOrDefault().Value : null; | |
} | |
} | |
/// <summary> | |
/// Merge - Id of the entity being merge to the parent entity | |
/// </summary> | |
internal Guid? SubordinatedId | |
{ | |
get | |
{ | |
if (PluginExecutionContext.InputParameters.Contains("SubordinateId") && PluginExecutionContext.InputParameters["SubordinateId"] is Guid) | |
return (Guid)PluginExecutionContext.InputParameters["SubordinateId"]; | |
else | |
return null; | |
} | |
} | |
/// <summary> | |
/// Merge | |
/// </summary> | |
internal Entity UpdateContent | |
{ | |
get | |
{ | |
if (PluginExecutionContext.InputParameters.Contains("UpdateContent") && PluginExecutionContext.InputParameters["UpdateContent"] is Entity) | |
return PluginExecutionContext.InputParameters["UpdateContent"] as Entity; | |
else | |
return null; | |
} | |
} | |
/// <summary> | |
/// Merge | |
/// </summary> | |
internal bool? PerformParentingChecks | |
{ | |
get | |
{ | |
if (PluginExecutionContext.InputParameters.Contains("PerformParentingChecks") && PluginExecutionContext.InputParameters["PerformParentingChecks"] is bool) | |
return PluginExecutionContext.InputParameters["PerformParentingChecks"] as bool?; | |
else | |
return null; | |
} | |
} | |
private LocalPluginContext() | |
{ | |
} | |
internal LocalPluginContext(IServiceProvider serviceProvider) | |
{ | |
if (serviceProvider == null) | |
{ | |
throw new ArgumentNullException("serviceProvider"); | |
} | |
// Important to be able to impersonify | |
this.ServiceProvider = serviceProvider; | |
// Obtain the execution context service from the service provider. | |
this.PluginExecutionContext = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext)); | |
// Obtain the tracing service from the service provider. | |
this.TracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService)); | |
// Obtain the Organization Service factory service from the service provider | |
IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory)); | |
// Use the factory to generate the Organization Service. | |
this.OrganizationService = factory.CreateOrganizationService(this.PluginExecutionContext.UserId); | |
} | |
internal void Trace(string message) | |
{ | |
if (string.IsNullOrWhiteSpace(message) || this.TracingService == null) | |
{ | |
return; | |
} | |
if (this.PluginExecutionContext == null) | |
{ | |
this.TracingService.Trace(message); | |
} | |
else | |
{ | |
this.TracingService.Trace( | |
"{0}, Correlation Id: {1}, Initiating User: {2}", | |
message, | |
this.PluginExecutionContext.CorrelationId, | |
this.PluginExecutionContext.InitiatingUserId); | |
} | |
} | |
/// <summary> | |
/// Adds the target values to the existing preImage | |
/// </summary> | |
/// <param name="target"></param> | |
/// <param name="preImage"></param> | |
/// <returns></returns> | |
internal Entity Merge(Entity target, Entity preImage) | |
{ | |
if (preImage == null) | |
return target; | |
else if (target == null) | |
return PreImage; | |
Entity resultat = preImage; | |
foreach (var a in target.Attributes) | |
{ | |
if (resultat.Attributes.Contains(a.Key)) | |
{ | |
resultat.Attributes[a.Key] = a.Value; | |
} | |
else | |
{ | |
resultat.Attributes.Add(a.Key, a.Value); | |
} | |
} | |
return resultat; | |
} | |
internal Entity Merge() | |
{ | |
return Merge(Target, PreImage); | |
} | |
/// <summary> | |
/// Adds the target values to the existing preImage. Changes won't go to step 30. Use the Target for that. | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
/// <returns></returns> | |
internal T Merge<T>() where T : Entity | |
{ | |
return Merge(Target, PreImage).ToEntity<T>(); | |
} | |
internal T MergePost<T>() where T : Entity | |
{ | |
return Merge(Target, PostImage).ToEntity<T>(); | |
} | |
internal T MergeDelete<T>() where T : Entity | |
{ | |
var entityRef = EntityReference; | |
var entity = new Entity() | |
{ | |
Id = entityRef.Id, | |
LogicalName = entityRef.LogicalName | |
}; | |
return Merge(entity, PreImage).ToEntity<T>(); | |
} | |
internal T MergeState<T>() where T : Entity | |
{ | |
var entity = new Entity(); | |
if (EntityMoniker != null) | |
entity = new Entity() | |
{ | |
Id = EntityMoniker.Id, | |
LogicalName = EntityMoniker.LogicalName, | |
}; | |
//else if (OpportunityClose != null) | |
// entity = OpportunityClose.OpportunityId.ToEntity(); | |
entity.Attributes.Add("statecode", State); | |
entity.Attributes.Add("statuscode", Status); | |
return Merge(entity, PreImage).ToEntity<T>(); | |
} | |
/// <summary> | |
/// To use when the plugin is triggered on create, update and SetState | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
/// <returns></returns> | |
internal T MergeOrEntityMoniker<T>() where T : Entity, new() | |
{ | |
return Merge<T>() ?? MergeState<T>(); | |
} | |
/// <summary> | |
/// To use when the plugin is triggered on create, update, SetState and delete | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
/// <returns></returns> | |
internal T MergeEverything<T>() where T : Entity, new() | |
{ | |
// in order of likeliness | |
switch (PluginExecutionContext.MessageName) | |
{ | |
case "Create": | |
case "Update": | |
return Merge<T>(); | |
case "SetState": | |
case "SetStateDynamicEntity": | |
return MergeState<T>(); | |
case "Delete": | |
return MergeDelete<T>(); | |
default: | |
return null; | |
} | |
} | |
} | |
private Collection<Tuple<int, string, string, Action<LocalPluginContext>>> registeredEvents; | |
/// <summary> | |
/// Gets the List of events that the plug-in should fire for. Each List | |
/// Item is a <see cref="System.Tuple"/> containing the Pipeline Stage, Message and (optionally) the Primary Entity. | |
/// In addition, the fourth parameter provide the delegate to invoke on a matching registration. | |
/// </summary> | |
protected Collection<Tuple<int, string, string, Action<LocalPluginContext>>> RegisteredEvents | |
{ | |
get | |
{ | |
if (this.registeredEvents == null) | |
{ | |
this.registeredEvents = new Collection<Tuple<int, string, string, Action<LocalPluginContext>>>(); | |
} | |
return this.registeredEvents; | |
} | |
} | |
/// <summary> | |
/// Gets or sets the name of the child class. | |
/// </summary> | |
/// <value>The name of the child class.</value> | |
protected string ChildClassName | |
{ | |
get; | |
private set; | |
} | |
/// <summary> | |
/// Initializes a new instance of the <see cref="Plugin"/> class. | |
/// </summary> | |
/// <param name="childClassName">The <see cref=" cred="Type"/> of the derived class.</param> | |
internal Plugin(Type childClassName) | |
{ | |
this.ChildClassName = childClassName.ToString(); | |
} | |
/// <summary> | |
/// Executes the plug-in. | |
/// </summary> | |
/// <param name="serviceProvider">The service provider.</param> | |
/// <remarks> | |
/// For improved performance, Microsoft Dynamics CRM caches plug-in instances. | |
/// The plug-in's Execute method should be written to be stateless as the constructor | |
/// is not called for every invocation of the plug-in. Also, multiple system threads | |
/// could execute the plug-in at the same time. All per invocation state information | |
/// is stored in the context. This means that you should not use global variables in plug-ins. | |
/// </remarks> | |
public void Execute(IServiceProvider serviceProvider) | |
{ | |
if (serviceProvider == null) | |
{ | |
throw new ArgumentNullException("serviceProvider"); | |
} | |
// Construct the Local plug-in context. | |
LocalPluginContext localcontext = new LocalPluginContext(serviceProvider); | |
localcontext.Trace(string.Format(CultureInfo.InvariantCulture, "Entered {0}.Execute()", this.ChildClassName)); | |
try | |
{ | |
// Iterate over all of the expected registered events to ensure that the plugin | |
// has been invoked by an expected event | |
// For any given plug-in event at an instance in time, we would expect at most 1 result to match. | |
Action<LocalPluginContext> entityAction = | |
(from a in this.RegisteredEvents | |
where ( | |
a.Item1 == localcontext.PluginExecutionContext.Stage && | |
a.Item2 == localcontext.PluginExecutionContext.MessageName && | |
(string.IsNullOrWhiteSpace(a.Item3) ? true : a.Item3 == localcontext.PluginExecutionContext.PrimaryEntityName) | |
) | |
select a.Item4).FirstOrDefault(); | |
if (entityAction != null) | |
{ | |
localcontext.Trace(string.Format( | |
CultureInfo.InvariantCulture, | |
"{0} is firing for Entity: {1}, Message: {2}", | |
this.ChildClassName, | |
localcontext.PluginExecutionContext.PrimaryEntityName, | |
localcontext.PluginExecutionContext.MessageName)); | |
entityAction.Invoke(localcontext); | |
// now exit - if the derived plug-in has incorrectly registered overlapping event registrations, | |
// guard against multiple executions. | |
return; | |
} | |
} | |
catch (Exception ex) | |
{ | |
/*-------------------------------------------------------------------------------------------------- | |
Si c’est vous qui avez lancé une InvalidPluginExecutionException, alors ce n’est pas imprévue et on | |
ne fait que la relancer à CRM. Si vous voulez lancer une exception qui doit être formatée en | |
erreur, lancer une System.Exception à la place. | |
--------------------------------------------------------------------------------------------------*/ | |
if (ex is InvalidPluginExecutionException) | |
{ | |
throw; | |
} | |
else | |
{ | |
/*-------------------------------------------------------------------------------------------------- | |
Sinon, c’est une exception imprévue. Alors on formate un message. | |
--------------------------------------------------------------------------------------------------*/ | |
System.IO.StringWriter msg = new System.IO.StringWriter(); | |
msg.WriteLine("An unexpected error occurred: {0}", ex.Message); //Changez le nom du client/projet | |
/*-------------------------------------------------------------------------------------------------- | |
Ce qui nous intéresse vraiment au-delà du message de l’exception, ce sont les particularités de | |
certaines exceptions, par exemple l’erreur SOAP suite à un appel de SDK à CRM (maintenant pris en | |
charge via OrganizationServiceFault). L’exception reçue est peut-être du type recherché mais il | |
est aussi probable qu’elle soit un niveau plus bas dans la chaine (InnerException). | |
--------------------------------------------------------------------------------------------------*/ | |
System.ServiceModel.FaultException<Microsoft.Xrm.Sdk.OrganizationServiceFault> organizationServiceFault = null; | |
if (ex is System.ServiceModel.FaultException<Microsoft.Xrm.Sdk.OrganizationServiceFault>) | |
organizationServiceFault = (System.ServiceModel.FaultException<Microsoft.Xrm.Sdk.OrganizationServiceFault>)ex; | |
if (organizationServiceFault == null && ex.InnerException != null && ex.InnerException is System.ServiceModel.FaultException<Microsoft.Xrm.Sdk.OrganizationServiceFault>) | |
organizationServiceFault = (System.ServiceModel.FaultException<Microsoft.Xrm.Sdk.OrganizationServiceFault>)ex.InnerException; | |
if (organizationServiceFault != null) | |
{ | |
/*-------------------------------------------------------------------------------------------------- | |
Formater la OrganizationServiceFault trouvée | |
--------------------------------------------------------------------------------------------------*/ | |
msg.WriteLine("OrganizationServiceFault details:"); | |
msg.WriteLine("Fault Timestamp: {0}", organizationServiceFault.Detail.Timestamp); | |
msg.WriteLine("Fault Error Code: {0}", organizationServiceFault.Detail.ErrorCode); | |
msg.WriteLine("Fault Message: {0}", organizationServiceFault.Detail.Message); | |
if (organizationServiceFault.Detail.InnerFault != null | |
&& organizationServiceFault.Message != organizationServiceFault.Detail.InnerFault.Message) | |
{ | |
msg.WriteLine("Inner Fault Timestamp: {0}", organizationServiceFault.Detail.InnerFault.Timestamp); | |
msg.WriteLine("Inner Fault Error Code: {0}", organizationServiceFault.Detail.InnerFault.ErrorCode); | |
msg.WriteLine("Inner Fault Message: {0}", organizationServiceFault.Detail.InnerFault.Message); | |
} | |
} | |
/*-------------------------------------------------------------------------------------------------- | |
On ajoute le stack trac à la fin car c’est lui qui prend le plus de place | |
--------------------------------------------------------------------------------------------------*/ | |
msg.WriteLine("Stack Trace: {0}", ex.StackTrace); | |
/*-------------------------------------------------------------------------------------------------- | |
On renvoie le tous à CRM via une InvalidPluginExecutionException au statut Failed | |
--------------------------------------------------------------------------------------------------*/ | |
localcontext.Trace(msg.ToString()); | |
if (localcontext.Target != null) | |
{ | |
msg.WriteLine("--Target--"); | |
msg.WriteLine(Crunch(localcontext.Target)); | |
msg.WriteLine(""); | |
} | |
if (localcontext.PreImage != null) | |
{ | |
msg.WriteLine("--PreImage--"); | |
// Ajouter le contenu du preimage | |
msg.WriteLine(Crunch(localcontext.PreImage)); | |
msg.WriteLine(""); | |
} | |
if (localcontext.PostImage != null) | |
{ | |
msg.WriteLine("--PostImage--"); | |
// Ajouter le contenu du preimage | |
msg.WriteLine(Crunch(localcontext.PostImage)); | |
msg.WriteLine(""); | |
} | |
throw new InvalidPluginExecutionException(OperationStatus.Failed, msg.ToString()); | |
} | |
} | |
finally | |
{ | |
localcontext.Trace(string.Format(CultureInfo.InvariantCulture, "Exiting {0}.Execute()", this.ChildClassName)); | |
} | |
} | |
private System.Text.StringBuilder Crunch(Entity entity) | |
{ | |
var msg = new System.Text.StringBuilder(); | |
if (entity == null) | |
return msg; | |
// Ajouter le contenu du target | |
foreach (var attrib in entity.Attributes) | |
{ | |
string value = string.Empty; | |
if (attrib.Value == null) | |
value = "null"; | |
else if (attrib.Value is OptionSetValue) | |
value = ((OptionSetValue)attrib.Value).Value.ToString(); | |
else if (attrib.Value is EntityReference) | |
value = string.Format("{0}-{1}-{2}", ((EntityReference)attrib.Value).LogicalName, ((EntityReference)attrib.Value).Id, ((EntityReference)attrib.Value).Name); | |
else if (attrib.Value is Entity) | |
value = string.Format("{0}-{1}-{2}", ((Entity)attrib.Value).LogicalName, ((Entity)attrib.Value).Id, ((Entity)attrib.Value).ToEntityReference().Name); | |
else | |
value = attrib.Value.ToString(); | |
msg.AppendLine(string.Format("{0}: {1}", attrib.Key, value)); | |
} | |
return msg; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment