Wednesday, July 9, 2008

CRM DynamicEntity Limitations - Part 1

DynamicEntities in CRM are either a love or hate relationship. Either you love the flexibility of using them within code (no need to do WebService proxy updates as configuration changes are made in CRM) or you hate them for their lack of usability.

What do I mean by their lack of usability? Namely this:

Microsoft.Crm.Sdk.DynamicEntity leftSide = new Microsoft.Crm.Sdk.DynamicEntity("account");
Microsoft.Crm.Sdk.DynamicEntity rightSide = new Microsoft.Crm.Sdk.DynamicEntity("account");

leftSide.Properties.Add(new StringProperty("name", "Fabrikam"));
rightSide.Properties.Add(new StringProperty("name", "Fabrikam"));

Assert.IsTrue(leftSide == rightSide); // this will fail every time
Assert.IsTrue(leftSide.Equals(rightSide)); // this will fail every time too
I find that incredibly annoying. I love the TDD style to coding and not having these two objects do a data style comparison kills me. I have rarely run into a need to do a reference comparison and if so, I'll explicitly call a ReferenceEquals. To me. "Equals" is always a data-comparison. I mean, if you do "test" == "test" you'll get True, right?

Now, I could always create routines that will compare DynamicEntity classes (data wise) during my unit testing, but then every time I have some comparison of objects I have to create a new matcher. I tend to use NMock and NUnit which means I've got two different libraries to work with and about 3-4 different types of matching interfaces I have to implement. Why would I want to do that!?

Now, I could go off on MS for this sorely lacking detail but I'll refrain. That is better left for a well thought-out and tested post going over ALL the pain points! =-}

Here's my solution (which will hopefully make it up on CodePlex at some point as part of a general "Better CRM Library" project.

I first take and create my own DynamicEntity class that inherits from MS's DynamicEntity:

public class DynamicEntity : Microsoft.Crm.Sdk.DynamicEntity {}

Now let's get to some overriding! Now, I wouldn't say this code has been thoroughly unit tested or integration tested, so use at your own risk. It is only meant to point you in the right direction for your own implementations:

public class DynamicEntity : Microsoft.Crm.Sdk.DynamicEntity
{
public DynamicEntity(string name) : base(name)
{

}

public DynamicEntity()
{

}

public override bool Equals(object obj)
{
if (!(obj is Microsoft.Crm.Sdk.DynamicEntity || obj is DynamicEntity))
{
return false;
}
var rightSide = (DynamicEntity) obj;
if (Name != rightSide.Name)
return false;
foreach (Property property in Properties)
{
if (!rightSide.Properties.Contains(property.Name))
return false;
if (!PropertiesEqual(Properties[property.Name], rightSide[property.Name]))
{
return false;
}
}
return true;
}

public static bool operator ==(DynamicEntity left,
DynamicEntity right)
{
if (Equals(left, null) && Equals(right, null))
{
return true;
}
if (Equals(left, null))
{
return false;
}
return left.Equals(right);
}

public static bool operator !=(DynamicEntity left,
DynamicEntity right)
{
return !(left == right);
}

public bool Equals(DynamicEntity obj)
{
return Equals(this, obj);
}

public override int GetHashCode()
{
int runningTotal = 0;
runningTotal += Name.GetHashCode();
foreach (Property p in Properties)
runningTotal += p.GetHashCode();
return runningTotal;
}

public static bool PropertiesEqual(object leftSide,
object rightSide)
{
if (leftSide == null && rightSide == null)
return true;
if (leftSide == null)
return false;

if (leftSide.GetType() != rightSide.GetType())
return false;
if (leftSide.GetType() == typeof (string))
return leftSide == rightSide;

PropertyInfo[] properties = leftSide.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
foreach (PropertyInfo property in properties)
{
object leftValue = property.GetValue(leftSide, null);
object rightValue = property.GetValue(rightSide, null);
if (leftValue == null && rightValue != null)
return false;
if (leftValue != null && !leftValue.Equals(rightValue))
return false;
}
return true;
}
}
I haven't included the unit tests around this and I'm sure they don't have full code coverage anyway. But I want to get this out there.

This will hopefully be only one part of a whole series of these types of posts where I continue to expand and show how to handle some of the frustrating limitations of the CRM API. Let's just hope I've got the bench-time to get all them out.

=-}

P.S. Forgive me for the poor formatting of the code.

No comments: