Below is a step-by-step guide to creating plugins in Microsoft Dynamics 365 (Dataverse) using C# that trigger on the Associate and Disassociate messages. These messages are used when creating or removing relationships (e.g., N:N or 1:N) between entities, such as associating/disassociating a contact with an account. The plugins will log these actions to a custom entity (new_relationshipauditlog) and include optional validation to prevent certain associations. This guide assumes familiarity with C# and Dynamics 365 plugin development.
Step-by-Step Guide to Creating Associate and Disassociate Message Plugins in Dynamics 365
Step 1: Set Up Your Development Environment
- Install Required Tools:
- Visual Studio (2019 or later) with .NET Framework 4.6.2 or higher.
- Install the Microsoft.CrmSdk.CoreAssemblies NuGet package for Dynamics 365 SDK references.
- Install the Plugin Registration Tool (PRT) from NuGet (Microsoft.CrmSdk.XrmTooling.PluginRegistrationTool) or use XrmToolBox.
- Create a New Project:
- In Visual Studio, create a Class Library (.NET Framework) project (e.g., Dynamics365RelationshipPlugins).
- Add the NuGet package Microsoft.CrmSdk.CoreAssemblies for plugin development.
- Sign the Assembly:
- Go to Project > Properties > Signing > Check Sign the assembly.
- Create or select a strong name key file (e.g., mykey.snk). This is required for Dynamics 365 plugins.
Step 2: Create the Custom Entity for Logging (Optional)
- Create the Custom Entity:
- In Dynamics 365, navigate to Settings > Solutions > [Your Solution] > Entities > New.
- Create a custom entity (e.g., new_relationshipauditlog) with fields:
- new_name (Single Line of Text, for log description).
- new_action (Single Line of Text, for action type: “Associate” or “Disassociate”).
- new_entityname (Single Line of Text, for relationship entity name).
- new_recordid (Single Line of Text, for related record ID).
- new_timestamp (Date and Time, for action timestamp).
- Save and publish the entity.
- Add to Solution:
- Include the custom entity in your unmanaged solution for deployment.
Step 3: Write the Plugin Code
Below are two separate plugin classes: one for the Associate message and one for the Disassociate message. Both plugins log the action to the new_relationshipauditlog entity and include validation to prevent association if a condition is met (e.g., contact has a specific attribute).
Associate Message Plugin
AssociateMessagePlugin.cs
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;
namespace Dynamics365RelationshipPlugins
{
public class AssociateMessagePlugin : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
// Obtain the execution context
IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
// Check if the plugin is triggered by the Associate message
if (context.MessageName.ToLower() != "associate" || context.Stage != 20) // Pre-Operation stage
{
return;
}
// Get the organization service
IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);
// Get the relationship and related entities
Relationship relationship = (Relationship)context.InputParameters["Relationship"];
EntityReference target = (EntityReference)context.InputParameters["Target"];
EntityReferenceCollection relatedEntities = (EntityReferenceCollection)context.InputParameters["RelatedEntities"];
// Example: Handle Contact-Account N:N relationship (e.g., contact_account)
if (relationship.SchemaName != "contact_account")
{
return;
}
try
{
foreach (EntityReference relatedEntity in relatedEntities)
{
// Example validation: Prevent association if contact has a specific attribute (e.g., email is empty)
Entity contact = service.Retrieve("contact", relatedEntity.Id, new ColumnSet("emailaddress1"));
if (string.IsNullOrEmpty(contact.GetAttributeValue<string>("emailaddress1")))
{
throw new InvalidPluginExecutionException("Cannot associate contact without an email address.");
}
// Log the association to new_relationshipauditlog
Entity auditLog = new Entity("new_relationshipauditlog");
auditLog["new_name"] = $"Contact-Account Association: {relatedEntity.Id}";
auditLog["new_action"] = "Associate";
auditLog["new_entityname"] = relationship.SchemaName;
auditLog["new_recordid"] = relatedEntity.Id.ToString();
auditLog["new_timestamp"] = DateTime.Now;
service.Create(auditLog);
}
}
catch (Exception ex)
{
throw new InvalidPluginExecutionException($"Error in AssociateMessagePlugin: {ex.Message}", ex);
}
}
}
}
Disassociate Message Plugin
DisassociateMessagePlugin.cs
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;
namespace Dynamics365RelationshipPlugins
{
public class DisassociateMessagePlugin : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
// Obtain the execution context
IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
// Check if the plugin is triggered by the Disassociate message
if (context.MessageName != "disassociate" || context.Stage != 20) // Pre-Operation stage
{
return;
}
// Get the organization service
IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);
// Get the relationship and related entities
Relationship relationship = (Relationship)context.InputParameters["Relationship"];
EntityReference target = (EntityReference)context.InputParameters["Target"];
EntityReferenceCollection relatedEntities = (EntityReferenceCollection)context.InputParameters["RelatedEntities"];
// Example: Handle Contact-Account N:N relationship (e.g., contact_account)
if (relationship.SchemaName != "contact_account")
{
return;
}
try
{
foreach (EntityReference relatedEntity in relatedEntities)
{
// Optional validation: Prevent disassociation if a condition is met
Entity contact = service.Retrieve("contact", relatedEntity.Id, new ColumnSet("statuscode"));
OptionSetValue status = contact.GetAttributeValue<OptionSetValue>("statuscode");
if (status != null && status.Value == 1) // Example: Active status
{
throw new InvalidPluginExecutionException("Cannot disassociate active contacts.");
}
// Log the disassociation to new_relationshipauditlog
Entity auditLog = new Entity("new_relationshipauditlog");
auditLog["new_name"] = $"Contact-Account Disassociation: {relatedEntity.Id}";
auditLog["new_action"] = "Disassociate";
auditLog["new_entityname"] = relationship.SchemaName;
auditLog["new_recordid"] = relatedEntity.Id.ToString();
auditLog["new_timestamp"] = DateTime.Now;
service.Create(auditLog);
}
}
catch (Exception ex)
{
throw new InvalidPluginExecutionException($"Error in DisassociateMessagePlugin: {ex.Message}", ex);
}
}
}
}
Step 4: Understand the Plugin Code
- AssociateMessagePlugin.cs:
- Message: Triggers on the Associate message in the Pre-Operation stage (context.Stage == 20).
- Purpose: Logs the association of a contact to an account (N:N relationship contact_account) and validates that the contact has an email address.
- Input Parameters:
- Relationship: The schema name of the relationship (e.g., contact_account).
- Target: The primary entity reference (e.g., account).
- RelatedEntities: The collection of related entities (e.g., contacts).
- Logic:
- Checks Validates each contact has an email address.
- Logs the association to the new_relationshipauditlog entity.
- DisassociateMessagePlugin.cs:
- Message: Triggers on the Disassociate message in the Pre-Operation stage.
- Purpose: Logs the disassociation of a contact from an account and prevents disassociation for active contacts.
- Logic:
- Checks if the contact is active (statuscode == 1).
- Logs the disassociation to the new_relationshipauditlog entity.
- Customization:
- Modify the relationship.SchemaName to match your specific N:N relationship (e.g., new_customrelationship).
- Adjust validation logic or remove it if not needed.
- Update the new_relationshipauditlog fields to match your custom entity.
- Build the Solution:
- Build the project in Visual Studio (Ctrl+Shift+B) to generate the DLL (e.g., bin\Debug\Dynamics365RelationshipPlugins.dll).
Step 5: Register the Plugins in Dynamics 365
- Open the Plugin Registration Tool(PRT):
- Launch the PRT and connect to your Dynamics 365 environment.
- Register the Plugin Assembly:
- Click Register > Register New Assembly.
- Browse to the compiled DLL.
- Select Sandbox isolation mode and Database storage.
- Click Register Selected Plugins.
- Register Steps for Associate Message:
- Locate the AssociateMessagePlugin class in the PRT.
- Right-click and select Register New Step:
- Message: Associate
- Primary Entity: none (for N:N relationships)
- Event Pipeline Stage of Execution: PreOperation
- Execution Mode: Synchronous
- Filtering Attributes: Leave blank (not applicable for Associate).
- Click Register New Step.
- Register Steps for Disassociate Message:
- Repeat the process for DisassociateMessagePlugin:
- Message: Disassociate
- Primary Entity: none
- Event Pipeline Stage of Execution: PreOperation
- Execution Mode: Synchronous
- Click Register New Step.
- Repeat the process for DisassociateMessagePlugin:
Step 6: Test the Plugins
- Deploy to a Sandbox Environment:
- Export the solution (including the custom entity and plugin) as unmanaged and import it into a sandbox environment.
- Test the Associate Plugin:
- In Dynamics 365, associate a contact with an account (e.g., via a subgrid or form).
- Verify:
- A record is created in new_relationshipauditlog with new_action set to “Associate”.
- If the contact has no email, the association is blocked with the error message.
- Test the Disassociate Plugin:
- Disassociate a contact from an account.
- Verify:
- A record is created in new_relationshipauditlog with new_action set to “Disassociate”.
- If the contact is active, the disassociation is blocked with the error message.
- Check Plugin Trace Log:
- Enable plugin tracing in Settings > Administration > System Settings > Customization.
- Check Settings > Customization > Plug-in Trace Log for errors or logs.
Step 7: Debug and Troubleshoot
- Debugging:
- Use the Plugin Profiler in PRT or XrmToolBox to step through the code in Visual Studio.
- Add tracing with ITracingService (e.g., tracingService.Trace(“Associating {0}”, relatedEntity.Id);) for debugging.
- Common Issues:
- “Relationship not found”: Ensure the relationship.SchemaName matches the actual N:N relationship name in Dynamics 365.
- Permission errors: Verify the plugin’s user has create permissions for new_relationshipauditlog.
- No execution: Check that the step is registered for the correct message and stage.
Step 8: Deploy to Production
- Export the Solution:
- Export the solution as managed (for production) or unmanaged (for development).
- Include the custom entity (new_relationshipauditlog) and plugin assembly.
- Import and Publish:
- Import the solution into the production environment.
- Publish all customizations.
- Verify in Production:
- Test association and disassociation in production.
- Monitor the Plug-in Trace Log for issues.
Step 9: Best Practices and Cleanup
- Best Practices:
- Use Synchronous mode for validation to block operations when needed.
- Handle exceptions with clear error messages using InvalidPluginExecutionException.
- Test thoroughly in a sandbox environment.
- Use meaningful names for plugins and steps (e.g., ContactAccountAssociatePlugin).
- Documentation:
- Document the plugins’ purpose, messages, entities, and dependencies.
- Note the relationship name (contact_account) and custom entity details.
- Backup:
- Back up the solution and plugin code before production deployment.
- Increment assembly versions for managed solutions (e.g., 1.0.0.1).
Additional Notes
- Relationship Scope: The plugins target the contact_account N:N relationship. Modify the relationship.SchemaName to target other relationships (e.g., new_customrelationship).
- Validation: The example validation checks for an email address (Associate) or status (Disassociate). Adjust based on your business rules.
- Performance: Minimize queries in plugins to avoid performance issues. Use ColumnSet to retrieve only necessary attributes.
- Alternative Tools: Use XrmToolBox or Power Platform CLI (pac plugin init) for faster development and registration.
- Managed Solutions: For managed solutions, increment the solution version and use the Stage for Upgrade process to update plugins.
This guide provides a complete process for creating and deploying Associate and Disassociate message plugins in Dynamics 365. If you need specific customizations, additional validation logic, or troubleshooting help, let us know!

Leave a comment