Introduction
Dynamics 365 plugins are the backbone of custom business logic in the platform. When done right, they provide powerful automation and validation. When done wrong, they can cause performance issues, data inconsistencies, and maintenance nightmares. After developing hundreds of plugins across various industries, I've learned what separates good plugin code from problematic implementations.
1. Keep Plugins Focused and Single-Purpose
One of the most common mistakes is creating monolithic plugins that handle multiple scenarios. A plugin should do one thing and do it well.
❌ Bad Example: Kitchen Sink Plugin
public void Execute(IServiceProvider serviceProvider)
{
// 200+ lines of code handling:
// - Field validation
// - Related record updates
// - Email notifications
// - Document generation
// - External API calls
// All in one plugin!
}
✅ Good Example: Focused Plugins
// ValidateAccountData.cs
public void Execute(IServiceProvider serviceProvider)
{
// Only validates account data
}
// UpdateRelatedContacts.cs
public void Execute(IServiceProvider serviceProvider)
{
// Only updates related contacts
}
// SendNotificationEmail.cs
public void Execute(IServiceProvider serviceProvider)
{
// Only sends notification
}
Why this matters: Focused plugins are easier to test, debug, and maintain. They also allow you to register them on different stages and messages without unwanted side effects.
2. Use Early-Bound Types
While late-bound entities provide flexibility, early-bound types offer significant advantages for maintainability and compile-time safety.
❌ Late-Bound (String-based)
var accountName = entity.GetAttributeValue<string>("name");
entity["accountnumber"] = "ACC-" + DateTime.Now.Year;
✅ Early-Bound (Strongly-typed)
var accountName = entity.Name;
entity.AccountNumber = $"ACC-{DateTime.Now.Year}";
Benefits: IntelliSense support, compile-time validation, refactoring safety, and self-documenting code.
3. Implement Proper Error Handling
Always handle exceptions gracefully and provide meaningful error messages to users.
✅ Proper Error Handling Pattern
public void Execute(IServiceProvider serviceProvider)
{
ITracingService tracingService = null;
try
{
tracingService = (ITracingService)serviceProvider
.GetService(typeof(ITracingService));
tracingService.Trace("Plugin execution started");
// Your plugin logic here
tracingService.Trace("Plugin execution completed successfully");
}
catch (InvalidPluginExecutionException ex)
{
tracingService?.Trace($"Expected error: {ex.Message}");
throw; // Re-throw to show user-friendly message
}
catch (Exception ex)
{
tracingService?.Trace($"Unexpected error: {ex.ToString()}");
throw new InvalidPluginExecutionException(
"An error occurred while processing your request. Please contact your system administrator.",
ex
);
}
}
4. Avoid Infinite Loops
One of the most dangerous issues in plugin development is creating infinite loops where plugins trigger themselves recursively.
✅ Depth Check Pattern
public void Execute(IServiceProvider serviceProvider)
{
var context = (IPluginExecutionContext)serviceProvider
.GetService(typeof(IPluginExecutionContext));
// Prevent infinite loops
if (context.Depth > 1)
{
tracingService.Trace("Exiting plugin to prevent infinite loop");
return;
}
// Your logic here
}
5. Use Shared Variables for Context
When you need to pass data between plugin steps or prevent cascading updates, use the SharedVariables collection.
// Pre-Operation Plugin
context.SharedVariables["SkipUpdate"] = true;
// Post-Operation Plugin
if (context.SharedVariables.Contains("SkipUpdate"))
{
tracingService.Trace("Skipping update as requested");
return;
}
6. Optimize Database Queries
Every database call adds latency. Minimize queries and retrieve only the data you need.
❌ Multiple Queries
var account = service.Retrieve("account", accountId, new ColumnSet(true));
var primaryContact = service.Retrieve("contact", account.PrimaryContactId.Id, new ColumnSet(true));
var relatedCases = /* Another query */;
✅ Single Query with Linked Entities
var query = new QueryExpression("account");
query.ColumnSet = new ColumnSet("name", "accountnumber");
query.Criteria.AddCondition("accountid", ConditionOperator.Equal, accountId);
var contactLink = query.AddLink("contact", "primarycontactid", "contactid");
contactLink.Columns = new ColumnSet("firstname", "lastname", "emailaddress1");
contactLink.EntityAlias = "primarycontact";
var result = service.RetrieveMultiple(query);
7. Register Plugins Appropriately
Choosing the right stage and mode for your plugin registration is critical for performance and user experience.
Stage Selection Guidelines:
- Pre-Validation (10): Use for simple validation that should prevent the operation entirely
- Pre-Operation (20): Use for modifying the target entity before it's saved to the database
- Post-Operation (40): Use for operations that need the saved record (like updating related records)
Execution Mode:
- Synchronous: Operation waits for plugin to complete. Use when immediate feedback is needed.
- Asynchronous: Operation continues; plugin runs in background. Use for non-critical, time-consuming operations.
8. Implement Unit Tests
Always write unit tests for your plugins. Use the FakeXrmEasy framework or similar tools to test without a live CRM environment.
[TestMethod]
public void ValidateAccount_WithInvalidData_ThrowsException()
{
// Arrange
var context = new XrmFakedContext();
var plugin = new ValidateAccountPlugin();
var account = new Account { Name = "" }; // Invalid
var pluginContext = context.GetDefaultPluginContext();
pluginContext.InputParameters["Target"] = account;
// Act & Assert
Assert.ThrowsException<InvalidPluginExecutionException>(
() => context.ExecutePluginWith(pluginContext, plugin)
);
}
9. Use IOrganizationServiceFactory
When you need to impersonate a different user or perform operations outside the context of the current transaction, use the service factory.
var serviceFactory = (IOrganizationServiceFactory)serviceProvider
.GetService(typeof(IOrganizationServiceFactory));
// Current user context
var service = serviceFactory.CreateOrganizationService(context.UserId);
// System/admin context (use carefully!)
var adminService = serviceFactory.CreateOrganizationService(null);
10. Document Your Plugins
Good documentation saves countless hours. Document the purpose, dependencies, and any special considerations for each plugin.
/// <summary>
/// Updates related contact records when account address changes.
/// Registered on: Account, Update, Post-Operation, Synchronous
/// Triggers when: address1_line1, address1_city, address1_stateorprovince changed
/// Dependencies: Requires "Contact" entity access
/// </summary>
public class UpdateContactAddressPlugin : IPlugin
{
// Implementation
}
Common Anti-Patterns to Avoid
❌ Retrieving the Entire Target Entity
// DON'T DO THIS in Pre-Operation
var account = service.Retrieve("account", targetId, new ColumnSet(true));
// You already have the Target in context!
❌ Not Checking for Attribute Existence
// This throws exception if attribute doesn't exist
var value = entity.GetAttributeValue<string>("customfield");
// Use Contains or TryGetValue instead
if (entity.Contains("customfield"))
{
var value = entity.GetAttributeValue<string>("customfield");
}
❌ Using Plugin for Long-Running Operations
Synchronous plugins timeout after 2 minutes. For operations that might take longer, use asynchronous plugins or Azure Functions.
Performance Considerations
Target Only Changed Attributes
In Pre-Operation plugins, only process the attributes that actually changed:
var target = (Entity)context.InputParameters["Target"];
if (target.Contains("address1_city"))
{
// Address changed, process update
}
Batch Operations
When updating multiple records, use ExecuteMultiple for better performance:
var multipleRequest = new ExecuteMultipleRequest()
{
Settings = new ExecuteMultipleSettings()
{
ContinueOnError = false,
ReturnResponses = false
},
Requests = new OrganizationRequestCollection()
};
foreach (var contact in contactsToUpdate)
{
multipleRequest.Requests.Add(new UpdateRequest { Target = contact });
}
service.Execute(multipleRequest);
Security Best Practices
- Never store sensitive data in plain text in plugin configuration
- Use Azure Key Vault for storing credentials
- Validate all input data, even from trusted sources
- Use the principle of least privilege when impersonating users
- Log security-relevant events for audit purposes
Conclusion
Writing effective Dynamics 365 plugins requires attention to detail, understanding of the execution pipeline, and adherence to best practices. By following these guidelines, you'll create plugins that are:
- Performant and scalable
- Easy to maintain and debug
- Robust and error-resistant
- Well-documented and testable
Remember: a well-architected plugin solution is an investment in your system's long-term health. Take the time to do it right, and you'll save countless hours of troubleshooting down the road.