In Dynamics CRM a mapping in a relationship is just a way to speed up the process of creating sub entities. But what if you would like have a little bit more out of it. Say that you would like to have them cascading information down, so that when an address is updated on an account that same address is updated on all of the child contacts as well.
Note here that this is a one way update. Parent entity will update child entities and not the other way around.
Let’s start with what we already have in Dynamics CRM. So take the Account to contact relationship and have a look.
First picture here it’s the relationship with the relationship name (1).
Then mapping of fields, and these are the fields to keep updated.
So we need a place to keep record of what needs to be updated, so create a new entity. I named it “Active Mapping”, with two string fields,
- Name (or relationship name)
- Primary entity
When that’s done the rest is just code. No more information need to get this to work. So let’s get to it. I created a plugin using the developer toolkit but removed all the features that control when the plugin is fired. So I have a
LocalPluginContext localContext = new LocalPluginContext(serviceProvider);
But not much more.
So when the plugin has fired I want to start with getting all the “Active Mapping” entities that I created above matching my “primary enity”.
private static List<string> GetActiveMappings(LocalPluginContext localContext)
{
QueryExpression activeMappingsQuery = new QueryExpression("mer_activemapping")
{
ColumnSet = new ColumnSet("mer_name"),
};
activeMappingsQuery.Criteria.AddCondition("mer_primaryentity", ConditionOperator.Equal, localContext.PluginExecutionContext.PrimaryEntityName);
EntityCollection activemappings = localContext.OrganizationService.RetrieveMultiple(activeMappingsQuery);
List<string> returnList = new List<string>();
foreach (Entity activeMapping in activemappings.Entities)
{
if (activeMapping.Attributes.Contains("mer_name"))
returnList.Add(activeMapping["mer_name"].ToString());
}
return returnList;
}
Now this will return a list of relationship names that are relevant to update.
With that list of names I need to get the details of this list of relationships:
private static List<OneToManyRelationshipMetadata> GetRelationshipDetails(LocalPluginContext localContext, List<string> relationshipNames)
{
RetrieveEntityRequest retrieveEntityRequest = new RetrieveEntityRequest()
{
EntityFilters = EntityFilters.Relationships,
RetrieveAsIfPublished = true,
LogicalName = localContext.PluginExecutionContext.PrimaryEntityName,
};
RetrieveEntityResponse retrieveEntityResp = (RetrieveEntityResponse)localContext.OrganizationService.Execute(retrieveEntityRequest);
List<OneToManyRelationshipMetadata> returnList = new List<OneToManyRelationshipMetadata>();
foreach (OneToManyRelationshipMetadata relationshipMetadata in retrieveEntityResp.EntityMetadata.OneToManyRelationships)
{
foreach (string relName in relationshipNames)
{
if (relationshipMetadata.SchemaName.Equals(relName))
{
returnList.Add(relationshipMetadata);
}
}
}
return returnList;
}
When I have the actual relationships I need to get the attribute mappings list, so for each of the items in the returned list in the previous method I do:
private static List<InternalAttributeMap> GetChildAttributeMappings(LocalPluginContext localContext, string relatedentity)
{
QueryExpression relationshipMappings = new QueryExpression("attributemap");
relationshipMappings.ColumnSet.AddColumns("sourceattributename", "targetattributename");
relationshipMappings.Criteria.AddCondition("parentattributemapid", ConditionOperator.Null);
LinkEntity entityMap = new LinkEntity("attributemap", "entitymap", "entitymapid", "entitymapid", JoinOperator.Inner);
entityMap.LinkCriteria.AddCondition("sourceentityname", ConditionOperator.Equal, localContext.PluginExecutionContext.PrimaryEntityName);
entityMap.LinkCriteria.AddCondition("targetentityname", ConditionOperator.Equal, relatedentity);
relationshipMappings.LinkEntities.Add(entityMap);
EntityCollection attributeMaps = localContext.OrganizationService.RetrieveMultiple(relationshipMappings);
List<InternalAttributeMap> attributeMappings = new List<InternalAttributeMap>();
foreach (Entity attributeMap in attributeMaps.Entities)
{
attributeMappings.Add(new InternalAttributeMap(attributeMap["sourceattributename"].ToString(), attributeMap["targetattributename"].ToString()));
}
return attributeMappings;
}
I put all of them in a helper class to make life easier for me:
internal class InternalAttributeMap
{
public InternalAttributeMap(string sourceattributename, string targetattributename)
{
this.SourceAttributeName = sourceattributename;
this.TargetAttributeName = targetattributename;
}
public string SourceAttributeName { get; set; }
public string TargetAttributeName { get; set; }
}
Note here that only source entity and target entity is needed, that means that CRM actually don’t have an attributes mapping per relationship. But have a common for each source-target pair. This means that if you have multiple relationships from one entity to another, and you change the attribute mapping for one of them, you have changed the attribute mapping for the other as well.
It’s easy to try. Change one of the account to opportunity mapping on account and then check the other.
Anyway, I have to remove the particular mapping that comprises the relationship, I don’t even want to try to change that field.
private static void RemoveSourceValue(List<InternalAttributeMap> list, string sourceAttributeName)
{
foreach (InternalAttributeMap attribute in list)
{
if (attribute.SourceAttributeName.Equals(sourceAttributeName))
{
list.Remove(attribute);
}
}
}
Update the child entities:
private static void UpdateChildEntities(LocalPluginContext localContext, List<InternalAttributeMap> attributeMappings, string entity, string relationshipattribute)
{
ColumnSet contactAttributes = new ColumnSet();
ColumnSet campaignResonseAttributes = new ColumnSet();
foreach (InternalAttributeMap attribute in attributeMappings)
{
contactAttributes.AddColumn(attribute.SourceAttributeName);
campaignResonseAttributes.AddColumn(attribute.TargetAttributeName);
}
Entity primaryEntity = localContext.OrganizationService.Retrieve(localContext.PluginExecutionContext.PrimaryEntityName, localContext.PluginExecutionContext.PrimaryEntityId, contactAttributes);
QueryExpression retreiveChildEntitiesQuery = new QueryExpression(entity);
retreiveChildEntitiesQuery.Criteria.AddCondition(relationshipattribute, ConditionOperator.Equal, localContext.PluginExecutionContext.PrimaryEntityId);
EntityCollection childEntitiesCollection = localContext.OrganizationService.RetrieveMultiple(retreiveChildEntitiesQuery);
foreach (Entity childEntity in childEntitiesCollection.Entities)
{
foreach (InternalAttributeMap attribute in attributeMappings)
{
if (primaryEntity.Attributes.Contains(attribute.SourceAttributeName))
childEntity[attribute.TargetAttributeName] = primaryEntity[attribute.SourceAttributeName];
else
childEntity[attribute.TargetAttributeName] = null;
}
localContext.OrganizationService.Update(childEntity);
}
}
So when you have all the pieces just add them together:
public void Execute(IServiceProvider serviceProvider)
{
// Construct the Local plug-in context.
LocalPluginContext localContext = new LocalPluginContext(serviceProvider);
List<string> relationshipsToUpdate = GetActiveMappings(localContext);
List<OneToManyRelationshipMetadata> metadataInfos = GetRelationshipDetails(localContext, relationshipsToUpdate);
foreach (OneToManyRelationshipMetadata relationship in metadataInfos)
{
List<InternalAttributeMap> attributeMappingsList = GetChildAttributeMappings(localContext, relationship.ReferencingEntity);
RemoveSourceValue(attributeMappingsList, relationship.ReferencedAttribute);
UpdateChildEntities(localContext, attributeMappingsList, relationship.ReferencingEntity, relationship.ReferencingAttribute);
}
}
So this is still missing just one little thing. Just because users add a row to my custom entity “active mapping” there is no event to trigger the plugin. You can definitely do this also as a plugin to that entity itself and then create and remove events to the primary entities that exits, but this is where this solution ends. So for each primary entity that exists in the “active mapping” list you have to create an event yourself. But I think you can live with that.