Wednesday, July 11, 2012

Adding Duplicates in N:N-Relations

The possibility to use many-to-many relations in Microsoft Dynamics CRM 2011 is very handy for various scenarios.
However, it's implementation is not very forgiving when you try to add a relation which already exists.
 
There is no way to configure the relationship or functionality of the "Add existing..." ribbon button to prevent this "ugly" message - so I decided to create a plugin which smooths things out, so that adding a relation which already exists only makes sure the relationship is there, but does not complain if it already was.
 
Scenario
We have a N:N-relation between Account and Product, to indicate which products are interesting to which accounts.
 
Clicking the Add Existing Product button brings up the lookup dialog for products.
When I now want to add a number of products, I do not want to have recall or check which products were already added. I just want to select all products (we can really sell anything to this company ;) click OK and be done with it.
 
Solution
We can create a plugin before main operation that verifies if the added relation already exists. But if it exists, we cannot prevent the relation from being created in any other way than throwing an exception, which will then result in a similar "ugly" message to the user, except it will contain the text we throw.
So we have to get around the implementation in the main operation (CRM core) some other way.
My choice was to simply delete the existing relation, so adding the new one will not be a problem.
Naturally, this causes extra transactions, but in normal circumstances this will not have any greater impact.
 
 
using System;
using System.ServiceModel;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Metadata;
using Microsoft.Xrm.Sdk.Query;

namespace Cinteros.Xrm
{
    public class AssociateAllower : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

            if (context.MessageName == "Associate" &&
                context.Stage == 20 &&      // Before, inside transaction
                context.Mode == 0 &&        // Synchronous
                context.InputParameters.Contains("Target") &&
                context.InputParameters["Target"] is EntityReference &&
                context.InputParameters.Contains("Relationship") &&
                context.InputParameters["Relationship"] is Relationship &&
                context.InputParameters.Contains("RelatedEntities") &&
                context.InputParameters["RelatedEntities"] is EntityReferenceCollection)
            {
                try
                {
                    ITracingService tracer = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
                    IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
                    IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);

                    Trace(tracer, "Get the name of the intersect table and related entities from the context");
                    Relationship relationship = ((Relationship)context.InputParameters["Relationship"]);
                    Trace(tracer, "Intersect: {0}", relationship);
                    EntityReference entity1ref = (EntityReference)context.InputParameters["Target"];
                    Trace(tracer, "Entity1: {0} {1}", entity1ref.LogicalName, entity1ref.Id);
                    EntityReferenceCollection entity2refs = (EntityReferenceCollection)context.InputParameters["RelatedEntities"];
                    Trace(tracer, "Entity2 count: {0}", entity2refs.Count);

                    Trace(tracer, "Get metadata for intersect relation: {0}", relationship);
                    RetrieveRelationshipResponse relationshipmetadata = (RetrieveRelationshipResponse)service.Execute(new RetrieveRelationshipRequest() { Name = relationship.SchemaName });
                    if (relationshipmetadata != null && relationshipmetadata.RelationshipMetadata is ManyToManyRelationshipMetadata)
                    {
                        ManyToManyRelationshipMetadata mmrel = (ManyToManyRelationshipMetadata)relationshipmetadata.RelationshipMetadata;
                        Trace(tracer, "Get intersect attribute names (we cannot know which entity is entity1 and which is entity2)");
                        string entity1idattribute = mmrel.Entity1LogicalName == entity1ref.LogicalName ? mmrel.Entity1IntersectAttribute : mmrel.Entity2IntersectAttribute;
                        string entity2idattribute = mmrel.Entity1LogicalName == entity1ref.LogicalName ? mmrel.Entity2IntersectAttribute : mmrel.Entity1IntersectAttribute;
                        Trace(tracer, "Entiy1id: {0} Entity2id: {1}", entity1idattribute, entity2idattribute);

                        Trace(tracer, "Verify if any of the new relations already exist");
                        foreach (EntityReference entity2ref in entity2refs)
                        {
                            QueryByAttribute qba = new QueryByAttribute(relationship.SchemaName);
                            qba.AddAttributeValue(entity1idattribute, entity1ref.Id);
                            qba.AddAttributeValue(entity2idattribute, entity2ref.Id);
                            EntityCollection existingAssociations = service.RetrieveMultiple(qba);
                            Trace(tracer, "Found {0} existing relations", existingAssociations.Entities.Count);
                            if (existingAssociations.Entities.Count > 0)
                            {
                                EntityReferenceCollection deleteRefs = new EntityReferenceCollection();
                                deleteRefs.Add(entity2ref);
                                Trace(tracer, "Disassociating entities");
                                service.Execute(new DisassociateRequest()
                                {
                                    Target = entity1ref,
                                    RelatedEntities = deleteRefs,
                                    Relationship = relationship
                                });
                                Trace(tracer, "Disassociated");
                            }
                        }
                    }
                    else
                    {
                        throw new InvalidPluginExecutionException("Metadata for relation " + relationship + " is not of type ManyToManyRelationshipMetadata");
                    }
                    Trace(tracer, "Done!");
                }
                catch (FaultException<OrganizationServiceFault> ex)
                {
                    throw new InvalidPluginExecutionException(
                        String.Format("An error occurred in plug-in {0}. {1}: {2}", this, ex, ex.Detail.Message));
                }
            }
        }

        private void Trace(ITracingService tracer, string format, params object[] args)
        {
            tracer.Trace(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") + " " + format, args);
        }
    }
}

No comments:

Post a Comment