Answered by:
2011 Plugin structure/architecture concerns

Question
-
Hi All,
I'm still trying to wrap my head around CRM 2011 development and started messing with some basic plugins to get familiar with this.
So first thing I noticed and wanted to do was abstract redundant code into a base class.
I.e. the hook-ups for proxy, using early-bound classes etc.
So questions:
#1) I would like to create a generic base plugin class that I can somehow pass-in my crmsvcutil generated class.
We have several Orgs that are each in separate solutions and would like each to be able to use this generic plugin base.
What's best practice or ideas on this? Should I move each of the crmsvcutil generated classes into a seperate library with the base plugin class?#2) Settings or Resources to store config settings? Pros/Cons of this approach
#3) Service context using a singleton-ish approach. I'm concerned about how many times this object will be instantiated/destroyed and connect to the WS to get my early bound entity info.
There a "best-practices" way to accomplish?Thanks!
Jon
Monday, October 17, 2011 8:29 PM
Answers
-
The following code should help with #1. It is the base class that the Microsoft ships with their CRM Development Solution. I think it's pretty good. It seems to implement most of the necessities we've found over time in our own base Plugin class:
// <copyright file="Plugin.cs" company="Microsoft"> // Copyright (c) 2010 All Rights Reserved // </copyright> // <author>Microsoft</author> // <date>12/2/2010 9:17:30 AM</date> // <summary>Implements the Plugin Workflow Activity.</summary> // <auto-generated> // This code was generated by a tool. // Runtime Version:4.0.30319.1 // </auto-generated> namespace CrmSolutionTest.Plugins { using System; using System.ServiceModel; using Microsoft.Xrm.Sdk; /// <summary> /// Base class for all Plugins. /// </summary> public class Plugin : IPlugin { /// <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> /// Gets or sets the name of the entity. /// </summary> /// <value>The name of the entity.</value> protected string EntityName { 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> public Plugin(Type childClassName, string entityName) { this.ChildClassName = string.Format("{0}", childClassName); this.EntityName = entityName; } /// <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> void IPlugin.Execute(IServiceProvider serviceProvider) { if (serviceProvider == null) { throw new ArgumentNullException("serviceProvider"); } // Obtain the execution context from the service provider. IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext)); //Extract the tracing service. ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService)); if (tracingService == null) { throw new InvalidPluginExecutionException("Failed to retrieve tracing service."); } tracingService.Trace("Entered {0}.Execute(), Correlation Id: {1}, Initiating User: {2}", this.ChildClassName, context.CorrelationId, context.InitiatingUserId); // Get a reference to the Organization service. IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory)); IOrganizationService service = factory.CreateOrganizationService(context.UserId); try { if (this.ValidateContext(context)) { this.ExecutePlugin(serviceProvider, service, context, tracingService); } } catch (FaultException<OrganizationServiceFault> e) { tracingService.Trace("Exception: {0}", e.ToString()); // Handle the exception. throw; } tracingService.Trace("Exiting {0}.Execute(), Correlation Id: {1}", this.ChildClassName, context.CorrelationId); } /// <summary> /// Executes the plug-in. Child classes should override this method. /// </summary> /// <param name="serviceProvider">The <see cref="IServiceProvider"/>.</param> /// <param name="service">The <see cref="IOrganizationService"/>.</param> /// <param name="context">The <see cref="IPluginExecutionContext"/>.</param> /// <param name="tracingService">The <see cref="ITracingService"/>.</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> protected virtual void ExecutePlugin(IServiceProvider serviceProvider, IOrganizationService service, IPluginExecutionContext context, ITracingService tracingService) { } /// <summary> /// Validates the context. /// TODO: extend as necessary. /// </summary> /// <returns></returns> protected virtual bool ValidateContext(IPluginExecutionContext context) { if (context == null) { throw new ArgumentNullException("context"); } switch (context.MessageName) { case "Create": case "Update": { if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity) { // Obtain the target entity from the input parmameters. Entity entity = (Entity)context.InputParameters["Target"]; // Verify that the entity represents the desired entity. // If not, this plug-in was not registered correctly. return entity.LogicalName.Equals(this.EntityName); } return false; } case "SetStateDynamicEntity": case "SetState": { // Make sure that the Entity Moniker and State information are available. if (context.InputParameters.Contains("entityMoniker") && context.InputParameters.Contains("state")) { // Obtain the target entity from the input parmameters. Entity entity = (Entity)context.InputParameters["entityMoniker"]; // Verify that the entity represents the desired entity. // If not, this plug-in was not registered correctly. return context.PrimaryEntityName.Equals(this.EntityName, StringComparison.OrdinalIgnoreCase); } return false; } default: { return true; // Other messages to be implemented as required. } } } } }
On number two, if the settings are not security sensitive (or should be configurable through an on premise administrator) you can pass configuration strings to the plugin via their registration, or create an organization or business unit level custom configuration entity accessable by only an administrative user. For config settings that should be only available to your plugin assembly, I would recommend using assembly resources. This is (incedentally) how we configure translation strings for our plugin error messages, etc.
On three, I would recommend reading through the SDK best practices. You can use the same service context but may need to periodically refresh the login info:
http://msdn.microsoft.com/en-us/library/gg509027.aspx
- Marked as answer by Goosey314159 Tuesday, October 18, 2011 1:18 PM
Monday, October 17, 2011 9:37 PM
All replies
-
The following code should help with #1. It is the base class that the Microsoft ships with their CRM Development Solution. I think it's pretty good. It seems to implement most of the necessities we've found over time in our own base Plugin class:
// <copyright file="Plugin.cs" company="Microsoft"> // Copyright (c) 2010 All Rights Reserved // </copyright> // <author>Microsoft</author> // <date>12/2/2010 9:17:30 AM</date> // <summary>Implements the Plugin Workflow Activity.</summary> // <auto-generated> // This code was generated by a tool. // Runtime Version:4.0.30319.1 // </auto-generated> namespace CrmSolutionTest.Plugins { using System; using System.ServiceModel; using Microsoft.Xrm.Sdk; /// <summary> /// Base class for all Plugins. /// </summary> public class Plugin : IPlugin { /// <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> /// Gets or sets the name of the entity. /// </summary> /// <value>The name of the entity.</value> protected string EntityName { 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> public Plugin(Type childClassName, string entityName) { this.ChildClassName = string.Format("{0}", childClassName); this.EntityName = entityName; } /// <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> void IPlugin.Execute(IServiceProvider serviceProvider) { if (serviceProvider == null) { throw new ArgumentNullException("serviceProvider"); } // Obtain the execution context from the service provider. IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext)); //Extract the tracing service. ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService)); if (tracingService == null) { throw new InvalidPluginExecutionException("Failed to retrieve tracing service."); } tracingService.Trace("Entered {0}.Execute(), Correlation Id: {1}, Initiating User: {2}", this.ChildClassName, context.CorrelationId, context.InitiatingUserId); // Get a reference to the Organization service. IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory)); IOrganizationService service = factory.CreateOrganizationService(context.UserId); try { if (this.ValidateContext(context)) { this.ExecutePlugin(serviceProvider, service, context, tracingService); } } catch (FaultException<OrganizationServiceFault> e) { tracingService.Trace("Exception: {0}", e.ToString()); // Handle the exception. throw; } tracingService.Trace("Exiting {0}.Execute(), Correlation Id: {1}", this.ChildClassName, context.CorrelationId); } /// <summary> /// Executes the plug-in. Child classes should override this method. /// </summary> /// <param name="serviceProvider">The <see cref="IServiceProvider"/>.</param> /// <param name="service">The <see cref="IOrganizationService"/>.</param> /// <param name="context">The <see cref="IPluginExecutionContext"/>.</param> /// <param name="tracingService">The <see cref="ITracingService"/>.</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> protected virtual void ExecutePlugin(IServiceProvider serviceProvider, IOrganizationService service, IPluginExecutionContext context, ITracingService tracingService) { } /// <summary> /// Validates the context. /// TODO: extend as necessary. /// </summary> /// <returns></returns> protected virtual bool ValidateContext(IPluginExecutionContext context) { if (context == null) { throw new ArgumentNullException("context"); } switch (context.MessageName) { case "Create": case "Update": { if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity) { // Obtain the target entity from the input parmameters. Entity entity = (Entity)context.InputParameters["Target"]; // Verify that the entity represents the desired entity. // If not, this plug-in was not registered correctly. return entity.LogicalName.Equals(this.EntityName); } return false; } case "SetStateDynamicEntity": case "SetState": { // Make sure that the Entity Moniker and State information are available. if (context.InputParameters.Contains("entityMoniker") && context.InputParameters.Contains("state")) { // Obtain the target entity from the input parmameters. Entity entity = (Entity)context.InputParameters["entityMoniker"]; // Verify that the entity represents the desired entity. // If not, this plug-in was not registered correctly. return context.PrimaryEntityName.Equals(this.EntityName, StringComparison.OrdinalIgnoreCase); } return false; } default: { return true; // Other messages to be implemented as required. } } } } }
On number two, if the settings are not security sensitive (or should be configurable through an on premise administrator) you can pass configuration strings to the plugin via their registration, or create an organization or business unit level custom configuration entity accessable by only an administrative user. For config settings that should be only available to your plugin assembly, I would recommend using assembly resources. This is (incedentally) how we configure translation strings for our plugin error messages, etc.
On three, I would recommend reading through the SDK best practices. You can use the same service context but may need to periodically refresh the login info:
http://msdn.microsoft.com/en-us/library/gg509027.aspx
- Marked as answer by Goosey314159 Tuesday, October 18, 2011 1:18 PM
Monday, October 17, 2011 9:37 PM -
Thanks for the code, this gives me some good ideas on how I want to implement the base class.
#2) Does this mean you have to re-deploy to update a config setting? Any way to bypass this via direct DB access?
Thanks!
Jon
Tuesday, October 18, 2011 12:40 PM -
Resources can be deployed separately in an xml file. Unfortunately, for Online and Hosted CRM installations that won't help you since you have no access to the host system. In such situations the resources will then have to be compiled into the assembly.
Since direct DB access would require configuration information, that presents a bit of a catch-22, doesn't it? At any rate direct DB access to any of the CRM databases is non-supported/non-compliant.
Tuesday, October 18, 2011 1:04 PM -
We're using the on-premise model so the external resource works out good.
Thanks!
Tuesday, October 18, 2011 1:18 PM