Hi all,
I am planning to rewrite all of our plugins and here are some of my goals:
- Use CRM SDK only to get, update, delete, etc,. data
- Use an improved Error Handler class (see below)
- Merge multiple update statements into one.
- If opportunity gets updated in one method, then another, and then once more, I just want to update the opportunity once. I may have to utilize the strategy discussed here in step 5: https://community.dynamics.com/crm/b/powerxrmblog/archive/2016/06/09/microsoft-dynamics-crm-plugin-best-practices-part-1
- Add trace (tracingService.Trace)
- Read hardcoded values from a CRM Entity (this resembles the web.config with key/value pairs as records)
- Go through each registered plugin and clean up filtered attributes
- Unit Testing
Error Handler Class:
public static class ErrorHandler { public static void SendErrorMessage(string className, string method, string body, Exception ex) { MailMessage mm = new MailMessage(); mm.From = new MailAddress(Constants.FromErrorEmail); mm.To.Add(Constants.ToErrorEmails); mm.Subject = Constants.Environment + " Environment"; mm.IsBodyHtml = true; string emailBody = ""; emailBody += "Class Name: " + className + "<br /><br />"; emailBody += "Method Name: " + method + "<br /><br />"; emailBody += "Description: " + body + "<br /><br />"; emailBody += "--- Exception ---" + "<br /><br />"; if (ex != null) { emailBody += "Message: " + ex.Message + "<br /><br />"; emailBody += "Source: " + ex.Source + "<br /><br />"; emailBody += "StackTrace: " + ex.StackTrace + "<br /><br />"; FaultException<Microsoft.Xrm.Sdk.OrganizationServiceFault> fe1 = ex as FaultException<Microsoft.Xrm.Sdk.OrganizationServiceFault>; if (fe1 != null) { emailBody += "--- FaultException ---<br />"; emailBody += "Code: " + fe1.Detail.ErrorCode + "<br />"; emailBody += "Message: " + fe1.Detail.Message + "<br />"; emailBody += "Trace: " + fe1.Detail.TraceText + "<br />"; emailBody += "Inner Fault: " + (null == fe1.Detail.InnerFault ? "No Inner Fault<br /><br />" : "Has Inner Fault<br /><br />"); } if (ex.InnerException != null) { emailBody += ex.InnerException.Message + "<br /><br />"; emailBody += ex.InnerException.StackTrace + "<br /><br />"; FaultException<Microsoft.Xrm.Sdk.OrganizationServiceFault> fe = ex.InnerException as FaultException<Microsoft.Xrm.Sdk.OrganizationServiceFault>; if (fe != null) { emailBody += "--- FaultException ---<br />"; emailBody += "Code: " + fe.Detail.ErrorCode + "<br />"; emailBody += "Message: " + fe.Detail.Message + "<br />"; emailBody += "Trace: " + fe.Detail.TraceText + "<br />"; emailBody += "Inner Fault: " + (null == fe.Detail.InnerFault ? "No Inner Fault<br /><br />" : "Has Inner Fault<br /><br />"); } } } mm.Body = emailBody; SmtpClient sc = new SmtpClient(Constants.SMTP_HOST_ADDRESS); sc.Send(mm); } }
Before I start rewriting the plugins, I wanted to learn all the best practices when writing plugins.
Here are the articles I have explored:
- https://community.dynamics.com/crm/b/powerxrmblog/archive/2016/06/09/microsoft-dynamics-crm-plugin-best-practices-part-1
- Check if the target entity is the entity you're writing plugins for
- As far as naming main method as Execute for a Create or Update, we always use Execute and it's the only method that is inside our class, which for example, we call Opportunity_Create_Post_Sync. The Execute method contains a lot of logic.
- Use early bound types. We use late bound types because we make CRM SDK calls from many different projects. We would have to be careful to update all our models in all those projects.
- Exception handling. Got that covered, but can improve
- Update same entity in the pre-update plugin. We don't do this at all, but will start doing
- Use Tracing. Will start using
- https://community.dynamics.com/crm/b/crmchap/archive/2016/07/31/implementing-tracing-in-your-crm-plug-ins
- Since we are in CRM 2013, we will use XrmToolBox tool called Plugin Trace Viewer until we upgrade to the version that has a view in CRM
- https://amrattia.wordpress.com/2017/03/22/best-practices-when-writing-dynamics-crm-plugins/
- Plugin Execute method should be stateless
- When updating an entity, create a new entity object and update only the necessary fields
- Do not update Target entity. See above bullet point
- Check if plugin depth > 1 to prevent recurring plugins. Would stop if triggered from another plugin, but careful because you may actually want a plugin to be triggered like this.
- Do not save organization service in the CRM execution context in a static variable. Will lead to lots of OpenDataReader errors
- Use good error handling. For synchronous plugins, you can display a message to the user
- http://scaleablesolutions.com/best-practices-for-writing-dynamics-crm-plugin/
- Retrieve minimum amount of attributes
- Use images instead of retrieve
- Be careful to execute as calling user's context because they may not have correct privileges. Use a service account with impersonation if necessary
Here's how we currently have out CRMPlugin project set up:
- Each entity has its own folder with one main class that holds all plugin steps (post operation for update, create, set state request, etc,) and one folder that holds all individual sub-classes that handle logic.
Example of a SetState Request:
#region //### When WCTransaction is SetState public class WCTransaction_SetState_Post : IPlugin { //### This plugin will do the following when the new_wctransaction is SetState to be inactive //### 1. Send Winner Circle Deposit Confirmation Email if Confirmation Flag = true #region IPlugin Members public void Execute(IServiceProvider serviceProvider) { try { //Extract the tracing service for use in debugging sandboxed plug-ins. ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService)); // Obtain the execution context from the service provider. IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext)); IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory)); IOrganizationService service = serviceFactory.CreateOrganizationService(new Guid(Constants.CRM_GUID_ID_CRM_ADMIN)); if (context.InputParameters.Contains("EntityMoniker")) { var targetEntity = (EntityReference)context.InputParameters["EntityMoniker"]; Entity entity = service.Retrieve("new_wctransaction", targetEntity.Id, new Microsoft.Xrm.Sdk.Query.ColumnSet(new string[] { "statuscode", "new_confirmationemail", "new_contactid", "new_points" })); //### 1. Send Winner Circle Deposit Confirmation Email if Confirmation Flag = true //register: statuscode, new_confirmationemail, new_contactid, new_equipusedid, new_points int wcTransactionStatusCodeApproved = 2; if (entity.GetAttributeValue<OptionSetValue>("statuscode")?.Value == wcTransactionStatusCodeApproved && entity.GetAttributeValue<bool?>("new_confirmationemail").Value == true) { if (entity.GetAttributeValue<EntityReference>("new_contactid")?.Id != null && entity.GetAttributeValue<int?>("new_points") != null) { Post_Create_WCDepositConfirmationEmail.SendWCDepositConfirmationEmail( service, entity.GetAttributeValue<EntityReference>("new_contactid").Id, entity.GetAttributeValue<int?>("new_points").Value ); }; } } } catch (Exception ex) { //### TODO: Handle Error Here CustomError ce = new CustomError(); ce.projectName = "CRM Plugins"; ce.errorClass = "WCTransaction_SetState"; ce.errorSection = "Execute Method"; ce.errorDescription = Constants.Environment + " Environment - Error out in WCTransaction SetStateRequestPlugIn"; ce.errorExceptionMessage = ex.ToString(); ErrorHandler eh = new ErrorHandler(); eh.appendError(ce); eh.sendEmail(Constants.ADMIN_EMAIL); } } #endregion } #endregion
Questions:
- Are there best practices on adding ITracingService.Trace into code?
- I would like to use it as much as possible
- Performance is a huge factor when running plugins. Should I be timing the executions and then potentially outputting it via a Trace?
- Can I improve the error handler class I posted above? Please make suggestions and potentially include your solution
- I want to implement one of the testing frameworks such as Xrm UnitTest, FakeXRMEasy, XRMMoq, but I wonder if I should do it now if the task of rewriting all of the plugins will take up a lot of time? But if I do, then I would be writing unit testable methods. Thoughts?
- Should all our update steps be done in pre-operation or pre-validation stage?
- Can you link to any CRMPlugIn best practice training videos or classes? Or even directly to someone's public CRMPlugIns project where I can study the code
Thank you very much for your help!
*This post is locked for comments