MVP Michael Höhne is our guest blogger this week.
A few weeks ago Mitch Milam showed how to use the EntityMetadata class of the metadata service to get the attribute names of the primary key and primary field. Even though it was a very basic example, you eventually saw that there is lots of valuable information in the metadata and in my opinion its value is totally underestimated.
If you want to create real dynamic applications not relying on the WSDL you get at compile time, you have to use both the DynamicEntity class and the metadata service. If you’re not familiar with these, feel free to look at the articles on my website listed at the end of this article.
Introduction
A while back I thought that writing code like the following is crap:
entity.integerField = new CrmNumber();
entity.integerField.Value = 10;
int? value = null;
if ((entity.integerField != null) && !entity.integerField.IsNull) {
value = entity.integerField.Value;
}
It doesn’t look good, is error prone and hard to maintain. You can use the CRM SDK helper classes (sdk samples\crmsdkhelpers and sdk samples\crmsdkhelpers.vb) or build your own to make it somewhat easier, but in the end we would like something like this:
private static void CreateAccountAndContacts(CrmService service) {
Account account = new Account(“(Generated Account)”);
account.Address1Line1 = “Street”;
account.Address1City = “City”;
account.Industrycode = Account.IndustrycodeOptions.Consulting;
account.Customertypecode = Account.CustomertypecodeOptions.Consultant;
account.Numberofemployees = 10;
account.Revenue = 2000000;
account.PrimaryKey = service.Create(account.CrmBusinessEntity);
Contact contact = new Contact(“(Generated Contact)”);
contact.Parentcustomerid = account.ToCustomer();
contact.PrimaryKey = service.Create(contact.CrmBusinessEntity);
Account primaryCustomer = new Account();
primaryCustomer.PrimaryKey = account.PrimaryKey;
primaryCustomer.Primarycontactid = contact.ToLookup();
service.Update(primaryCustomer.CrmBusinessEntity);
}
That looks a lot easier and indeed it is. So what are the Account and Contact classes used in the code above? Here’s a code snippet:
public partial class Account : CrmBusinessEntity, ICloneable {
account _crmData;
/// <summary>
/// Creates a new Account.
/// </summary>
public Account() {
_crmData = new account();
}
/// <summary>
/// Creates a new Account and initializes it with the values of the
/// passed account.
/// </summary>
/// <param name=”crmData”>An existing CRM record. Can be null.</param>
public Account(account crmData) {
_crmData = crmData;
}
/// <summary>
/// Creates a new Account.
/// </summary>
/// <param name=”pName”>Name of the account.</param>
/// <remarks>
/// The parameters passed to this constructor are the minimal information you have to set in order to
/// save a new Account using the web services.
/// </remarks>
public Account(string pName) {
_crmData = new account();
Name = pName;
}
/// <summary>
/// Valid options for the <see cref=”Accountcategorycode”/> property.
/// </summary>
public enum AccountcategorycodeOptions {
undefined = 0,
PreferredCustomer = 1,
Standard = 2,
}
/// <summary>
/// Drop-down list for selecting the category of the account.
/// </summary>
public AccountcategorycodeOptions Accountcategorycode {
get {
return ((_crmData.accountcategorycode == null) || _crmData.accountcategorycode.IsNull) ? AccountcategorycodeOptions.undefined : (AccountcategorycodeOptions) _crmData.accountcategorycode.Value;
}
set {
_crmData.accountcategorycode = new Picklist();
if (value == AccountcategorycodeOptions.undefined) {
_crmData.accountcategorycode.IsNull = true;
_crmData.accountcategorycode.IsNullSpecified = true;
}
else {
_crmData.accountcategorycode.Value = (int) value;
}
}
}
As you might guess, the code is auto-generated using the information from the metadata service. I’m not going to explain how to write the code generator here (watch my website for news on this topic); instead I’m explaining some rarely used properties of the metadata classes as well as some pitfalls you need to be aware of when being “real dynamic”.
Which entities are descendants of the BusinessEntity class?
Writing dynamic code often is driven by a requirement to have some user interaction. Maybe it should be possible to select a CRM entity and map some of its attributes to data found in an import file. The first step in such an application is an entity list, but it should only include entities that can be used in a call to crmService.Create().
You can look at the metadata service as long as you want, but you won’t find this piece of information. You could also use reflection to parse the CRM classes, but let’s see how far we can get with the metadata.
Mitch pointed you to the PrimaryKey and PrimaryField properties of the EntityMetadata class. Each entity has a primary key, but not all of them have a primary field. So your first step is filtering out all entities with PrimaryField == null, eliminating all of the internal helper entities like accountleads, businessunitmap, competiorproduct and so on. That almost does it, but some entities really look like they’re business entities while they’re not: bulkimport, competitoraddress, documentindex, importconfig, roletemplate, wfaction, wfrule and wfstep. I haven’t found any attribute distinguishing them from the real business entities, so I’m hard-coding these names in my code.
Which entities can be retrieved using RetrieveMultiple?
Besides creating entities you usually want to retrieve them as well. All entities can be queried with FetchXml, but not all can be used in a call to RetrieveMultiple. Again there’s no flag in the metadata telling you whether you can use them or not, so here’s my best bet:
1. The entity should have a ReportViewName. The ReportViewName tells you the name of the filtered view being accessed to retrieve the data. If there’s no view, you can’t use the entity.
2. The OwnershipType should be set to OwnershipTypes.User, OwnershipTypes.Business or OwnershipTypes.Organization. If it is not set (value of 0), don’t use the entity.
3. The primary field must be set
4. Do the same hard-coded check as before, filtering out bulkimport, competitoraddress, documentindex, importconfig, roletemplate, wfaction, wfrule and wfstep.
Now you know which entities can be used, so let’s come to the attributes.
What is the correct property type of an attribute?
You may think that AttributeMetadata.Type gives you that information and in about 95% or even more this is true. But sometimes it’s not, so you have to perform these additional checks:
1. If the Type is set to AttributeType.Picklist and the DisplayMasks.ObjectTypeCode flag is set in the DisplayMask property, the property is of type EntityNameReference instead.
2. If the Type is set to AttributeType.Integer and the DisplayMasks.ObjectTypeCode flag is set in the DisplayMask property, the property is of type String and has to be set to an entity name (EntityName.account.ToString() for example).
Also note that a Boolean attribute has an associated PicklistAttributeMetadata record. You can use it to retrieve the two display values for true and false. I’m mentioning it because it’s not as obvious as for picklist, state and status fields.
Which attributes can be used?
There are a lot of attributes that can’t be used when dealing with the web service. Here are the rules to know which attributes can be used:
1. The AttributeOf property must be null. If it is not null, it represents a property in a CrmReference or Picklist property. For instance “customeridname” has AttributeOf set to “customerid”, meaning that it represents the customerid.name property. If AttributeOf is not null, the attribute type is AttributeType.Virtual, which is another way to check the same condition.
2. The DisplayMask must not be set to DisplayMasks.None.
3. Check ValidForRead, ValidForCreate and ValidForUpdate. For instance the deletionstatecode attribute specifies false in all three flags.
Even with these checks you still find attributes that cannot be used and again some hard-coding is needed to filter them out:
1. If the attribute name is owninguser or owningbusinessunit and the entity has an attribute named ownerid, then neither owninguser nor owningbusinessunit can be used.
2. If the attribute name is accountid or contactid and the entity has an attribute named customerid, then neither accountid nor contactid can be used.
3. The attributes accountid and parentcontactid can’t be used in the contact entity.
The attributes mentioned above are helper fields used in owner and customer fields.
What attributes have to be passed to successfully create an entity?
Often it’s hard to find what attributes you have to set when creating an entity. The attributes that must be set can be found with these rules (check all attributes returned in the EntityMetadata.Attributes array):
1. The attribute must be valid according to the rules defined before (Which attributes can be used)
2. ValidForCreate is true
3. The AttributeRequiredLevel is set to AttributeRequiredLevel.Required
4. The DisplayMasks.RequiredForForm flag is set in the DisplayMask property
5. The attribute type is not AttributeType.Owner
6. The attribute name is not createdby, createdon, modifiedby, modifiedon or statecode
This gives you a list of the attributes you have to specify. At least I hope it does. However the above does not include the attributes that are marked as business required. So here’s the same to get a list of attributes that “should” be set:
1. The attribute must be valid according to the rules defined before (Which attributes can be used)
2. ValidForCreate is true
3. One of the following:
a. AttributeRequiredLevel is set to AttributeRequiredLevel.Required or AttributeRequiredLevel.SystemRequired and the DisplayMasks.ValidForForm flag is set in the DisplayMask property
b. The AttributeRequiredLevel is set to AttributeRequiredLevel.Required and the DisplayMasks.RequiredForForm flag is set in the DisplayMask property
4. The attribute type is not AttributeType.Owner
5. The attribute name is not createdby, createdon, modifiedby, modifiedon or statecode
I can’t guarantee that the results you’re getting when following these rules are totally correct, but at least I could use them to create the classes you saw at the beginning.
Related readings
If you want to know more about some of the techniques mentioned in this article, visit the following links:
Understanding the CRM Metadata/The CRM Metadata Viewer
Dynamic Entities – Getting Started
Finding data Part 6 – The FetchXML Wizard
Hope to see more questions about the metadata service in the future!