Contact Assignment Web Roles - Post Operation Associate
This plugin automatically assigns a Contact to appropriate Web Roles when they are associated with an Assignment.
What Does This Do?
When a Contact is linked to an Assignment (like "Audit Team Member" or "Tax Advisory Client"), this plugin automatically grants them the correct web portal permissions by associating them with the appropriate Web Roles.
For example, if a contact is assigned to "Business Tax - Data Provider", this plugin will:
- Look up the configuration that maps this assignment to specific Web Roles
- Check which Web Roles should be granted (e.g., "Business Tax - Data Provider", "Dashboard - Corporates")
- Automatically associate the contact with those Web Roles
- Skip any Web Roles the contact is already associated with
Important: This only runs when using the "Associate" action to link a Contact with an Assignment through the tt_Contact_tt_Assignment relationship.
When Does This Run?
| Property | Value |
|---|---|
| Entity | Contact to Assignment Relationship (tt_Contact_tt_Assignment) |
| Message | Associate |
| Stage | Post-Operation (runs after the association is created) |
| Execution Mode | Synchronous |
| Conditions | Only runs when Contact is associated with Assignment |
How It Works
Step 1: Validate the Associate message
The plugin first confirms it was triggered by an "Associate" action. If triggered by any other message (Create, Update, etc.), it throws an exception.
Exit condition: Throws InvalidPluginExecutionException if message is not "Associate"
Step 2: Validate the relationship
Checks that the association being created is specifically between a Contact and an Assignment using the tt_Contact_tt_Assignment relationship. If it's a different relationship, the plugin exits silently.
What happens: Validates relationship schema name, Target entity (Contact), and RelatedEntities (Assignment)
Exit condition: Returns early if relationship is not tt_Contact_tt_Assignment or entity types don't match
Step 3: Extract Contact and Assignment references
Retrieves the Contact ID and Assignment ID from the plugin execution context's input parameters.
What happens:
- Extracts
Targetparameter (Contact EntityReference) - Extracts
RelatedEntitiesparameter (Assignment EntityReference) - Logs the IDs for tracing
Step 4: Get the Assignment name
Retrieves the full Assignment record to get its name (e.g., "Business Tax - Data Provider"). This name is used to look up the Web Role configuration.
What happens: Queries the Assignment entity for the tt_name field
Step 5: Load the Assignment-to-Web-Role configuration
Retrieves the configuration from the tt_configuration entity. This configuration is stored as JSON and maps Assignment names to lists of Web Roles.
What happens:
- Queries for configuration named "Assignments.json"
- Parses JSON content into a nested dictionary structure
- Returns null if configuration is not found
Exit condition: Returns early if configuration is missing or cannot be parsed
Step 6: Find Web Roles for this Assignment
Searches the configuration dictionary for the Assignment name and extracts the list of Web Roles that should be granted.
What happens:
- Looks up Assignment name in configuration
- Retrieves the "Web Roles" array for that assignment
- Returns empty list if no Web Roles are configured
Exit condition: Returns early if no Web Roles are found for this Assignment
Step 7: Process each Web Role
Iterates through each Web Role name in the list and associates the Contact with that Web Role (if not already associated).
For each Web Role:
Sub-step 7a: Look up the Web Role record by name
- Queries
adx_webroleentity for matchingadx_name - Skips this Web Role if not found
Sub-step 7b: Check if Contact is already associated
- Queries the
adx_webrole_contactintersection table - Skips this Web Role if association already exists
Sub-step 7c: Associate Contact with Web Role
- Creates the many-to-many relationship using
Associatemethod - Links Contact to Web Role via
adx_webrole_contactrelationship - Logs success
Step 8: Log completion
Records that the plugin has finished processing in the Plugin Trace Log. This helps with debugging and verifying the plugin executed correctly.
What happens: Writes a trace message indicating successful completion
Why This Matters
Web Roles control what users can see and do in the web portal. Manually assigning Web Roles every time someone gets a new assignment is time-consuming and error-prone. This plugin:
- Automates permissions - Correct portal access is granted automatically
- Ensures consistency - The same assignment always grants the same permissions
- Prevents duplicates - Checks existing associations to avoid redundant records
- Maintains audit trail - All associations are logged in the trace log
- Centralizes configuration - Role mappings live in one place and can be updated without code changes
Code
using System;
using System.Collections.Generic;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using Newtonsoft.Json;
namespace PracticeGateway.Plugin
{
public class AssociateEntitiesPostOperationUpdate : PluginBase
{
public AssociateEntitiesPostOperationUpdate(string unsecure, string secure)
: base(typeof(AssociateEntitiesPostOperationUpdate))
{
}
/// <summary>
/// This plugin runs after a Contact is associated with an Assignment.
/// It automatically assigns the Contact to the appropriate Web Roles based on the Assignment.
/// </summary>
protected override void ExecuteCdsPlugin(ILocalPluginContext localContext)
{
if (localContext == null)
throw new InvalidPluginExecutionException(nameof(localContext));
// Get the services we need to work with data and log messages
ITracingService tracingService = localContext.TracingService;
IPluginExecutionContext context = localContext.PluginExecutionContext;
IOrganizationService currentUserService = localContext.CurrentUserService;
try
{
tracingService.Trace("Starting AssociateEntitiesPostOperationUpdate");
// Make sure this plugin was triggered by an "Associate" action
if (context.MessageName != "Associate")
throw new InvalidPluginExecutionException($"Unexpected message: {context.MessageName}");
// Helper class for common operations
Utils.Utils utils = new Utils.Utils(currentUserService, tracingService);
// Get the Contact and Assignment that were just linked together
if (!utils.ValidateContactAssignmentRelationship(context, tracingService, out EntityReference contact, out EntityReference assignment))
{
return; // Stop if the relationship isn't what we expect
}
tracingService.Trace($"Contact {contact.Id} associated with Assignment {assignment.Id}");
// Get the Assignment's name (we need this to find the right web roles)
Entity assignmentEntity = currentUserService.Retrieve("tt_assignment", assignment.Id, new ColumnSet("tt_name"));
string assignmentName = assignmentEntity.GetAttributeValue<string>("tt_name");
tracingService.Trace($"Assignment Name: {assignmentName}");
// Load the configuration that maps Assignments to Web Roles
var assignmentConfig = utils.GetAssignmentConfiguration(currentUserService, tracingService);
if (assignmentConfig == null)
{
return; // Stop if configuration is missing
}
// Find which Web Roles should be assigned for this Assignment
List<string> webRoles = utils.GetWebRolesForAssignment(assignmentConfig, assignmentName, tracingService);
if (webRoles.Count == 0)
{
tracingService.Trace($"No web roles configured for assignment '{assignmentName}'.");
return;
}
tracingService.Trace($"Found {webRoles.Count} web roles to process for assignment '{assignmentName}'.");
// Add the Contact to each Web Role (if not already added)
foreach (var webRoleName in webRoles)
{
// Look up the Web Role record by name
Entity webRole = utils.GetWebRoleByName(currentUserService, webRoleName);
if (webRole == null)
{
tracingService.Trace($"Web Role '{webRoleName}' not found.");
continue;
}
// Check if Contact is already linked to this Web Role
if (utils.IsContactAssociatedWithWebRole(currentUserService, contact.Id, webRole.Id))
{
tracingService.Trace($"Contact already associated with Web Role '{webRoleName}', skipping.");
continue;
}
// Link the Contact to the Web Role
currentUserService.Associate(
"contact",
contact.Id,
new Relationship("adx_webrole_contact"),
new EntityReferenceCollection { new EntityReference("adx_webrole", webRole.Id) }
);
tracingService.Trace($"Successfully associated contact with Web Role '{webRoleName}'.");
}
tracingService.Trace("Finished AssociateEntitiesPostOperationUpdate");
}
catch (Exception ex)
{
// Log the error and stop the plugin
tracingService.Trace($"Error in AssociateEntitiesPostOperationUpdate: {ex}");
throw new InvalidPluginExecutionException("Plugin execution failed.", ex);
}
}
}
}
Common Issues & Troubleshooting
Problem: Web Roles aren't being assigned when I associate a Contact with an Assignment
Possible Causes:
- The Assignment name doesn't match any entry in the configuration
- The configuration record is missing or inactive
- The plugin registration might be missing or disabled
- The relationship being used is not
tt_Contact_tt_Assignment
How to Check:
- Verify the Assignment name exactly matches a key in the "Assignments.json" configuration
- Query the
tt_configurationentity for a record named "Assignments.json" - Check the Plugin Trace Log for messages indicating why the plugin exited early
- Confirm the plugin is registered on Associate message for the
tt_Contact_tt_Assignmentrelationship
Problem: Plugin throws "Assignments.json configuration not found"
Cause: The Configuration entity doesn't have a record named "Assignments.json"
Solution:
- Create a Configuration record with:
- Name = "Assignments.json"
- Content (tt_content) = Valid JSON mapping assignments to web roles
- Ensure the record is active
Example JSON structure:
{
"Business Tax - Data Provider": {
"Web Roles": [
"Business Tax - Data Provider",
"Dashboard - Corporates"
]
},
"Audit - Primary Contact": {
"Web Roles": [
"Audit - Primary Contact",
"Dashboard - Corporates"
]
}
}
Problem: Plugin throws "Configuration parsing failed"
Possible Causes:
- The JSON in the configuration is malformed
- The JSON structure doesn't match the expected format
- Special characters in the JSON are not properly escaped
How to Check:
- Validate the JSON using a JSON validator (e.g., jsonlint.com)
- Ensure the structure is:
{ "AssignmentName": { "Web Roles": ["Role1", "Role2"] } } - Check for trailing commas, missing quotes, or unescaped characters
- Review the Plugin Trace Log for the specific parsing error
Problem: Some Web Roles are not being assigned
Possible Causes:
- The Web Role names in the configuration don't match the actual Web Role names in Dynamics
- The Web Roles don't exist in the system
- Web Role names have incorrect capitalization or extra spaces
How to Check:
- Review the
adx_webroleentity and verify exact names (including spaces and capitalization) - Check the Plugin Trace Log for "Web Role 'X' not found" messages
- Ensure Web Role names in the configuration exactly match those in Dynamics
- Verify the Web Roles are active (statecode = 0)
Problem: Contact is associated multiple times with the same Web Role
Cause: The duplicate check is failing, possibly due to a plugin issue or manual intervention
How to Check:
- Query the
adx_webrole_contactintersection table for duplicate records - Check if the plugin is registered multiple times for the same event
- Review the Plugin Trace Log to see if the "already associated" check is working
Solution:
- Manually remove duplicate associations
- Verify plugin registration (should only be registered once per event)
- Check for other plugins or workflows that might be creating associations
Problem: No Web Roles are assigned (count is 0)
Possible Causes:
- The Assignment name is not in the configuration
- The "Web Roles" array is empty in the configuration
- The configuration key doesn't exactly match the Assignment name
How to Check:
- Verify the Assignment name in the system (check
tt_namefield) - Open the "Assignments.json" configuration and search for that exact name
- Ensure the configuration has a "Web Roles" array with at least one role
- Review the Plugin Trace Log for "No web roles configured" message
Related Documentation
- Utils.ValidateContactAssignmentRelationship() - Utility method that validates the Associate relationship
- Utils.GetAssignmentConfiguration() - Utility method that retrieves the Assignments.json configuration
- Utils.GetWebRolesForAssignment() - Utility method that extracts Web Roles for an Assignment
- Utils.GetWebRoleByName() - Utility method that queries Web Role records
- Utils.IsContactAssociatedWithWebRole() - Utility method that checks for existing associations
- Assignment Configuration Guide [To Be Completed] - How to configure Assignment to Web Role mappings
- Web Roles Overview [To Be Completed] - Understanding Web Roles and portal permissions
Configuration Requirements
This plugin depends on the following configuration:
-
Configuration Record (
tt_configuration)- Name = "Assignments.json"
- Content (tt_content) = JSON mapping of Assignment names to Web Roles
- State = Active
-
JSON Structure:
{
"Assignment Name": {
"Web Roles": [
"Web Role Name 1",
"Web Role Name 2"
]
}
} -
Web Roles (
adx_webrole)- Web Roles referenced in configuration must exist
- Names must exactly match those in configuration (case-sensitive)
- Web Roles must be active
-
Assignments (
tt_assignment)- Must have
tt_namefield populated - Names should match keys in configuration
- Must have
-
Relationship (
tt_Contact_tt_Assignment)- Many-to-many relationship between Contact and Assignment entities
- Must be properly configured in Dynamics
-
Plugin Registration
- Registered on Associate message for
tt_Contact_tt_Assignmentrelationship - Post-Operation stage
- Synchronous execution mode
- Registered on Associate message for
Example Configuration
Configuration Record:
Name: Assignments.json
Content:
{
"Business Tax - Data Provider": {
"Web Roles": [
"Business Tax - Data Provider",
"Dashboard - Corporates"
]
},
"Audit - Primary Contact": {
"Web Roles": [
"Audit - Primary Contact",
"Dashboard - Corporates",
"Risk And Assurance - Primary Contact"
]
},
"Payroll - Basic User": {
"Web Roles": [
"Payroll - Basic User"
]
}
}
Usage Example:
When a Contact is associated with the Assignment "Business Tax - Data Provider", the plugin will automatically associate them with:
- Business Tax - Data Provider (Web Role)
- Dashboard - Corporates (Web Role)
Testing Checklist
When testing this plugin, verify:
- Associating a Contact with a configured Assignment grants correct Web Roles
- Associating a Contact with an unconfigured Assignment exits gracefully
- Plugin skips Web Roles that Contact is already associated with
- Plugin handles missing Web Roles gracefully (logs warning, continues)
- Plugin handles missing configuration gracefully (exits without error)
- Plugin exits silently when triggered on non-Contact/Assignment relationships
- Plugin throws exception when triggered by non-Associate message
- All Web Roles listed in configuration are assigned (if they exist)
- Multiple Contacts can be associated with the same Assignment successfully
- Plugin completes within acceptable performance timeframe
- Error messages are logged clearly in Plugin Trace Log
- Updating configuration affects future associations immediately