Thursday, April 18, 2013

Programmatically Generating Properties for OptionSet Enums

Generating early bound entity classes for CRM Entities is fairly simple.  Generating the Enums for the OptionSets is fairly simple as well.  Generating the OptionSet Enum properties for entities that are typed to the correct Enum is not so simple, and not currently supported by the CrmSrvUtil.exe.  If you want to be able to use the enums for populating the option set values, you have to continually write code that looks like this:
contact.Address1_AddressTypeCode = new OptionSetValue((int)contact_address1_addresstypecode.Home);


Which I don’t like for three reasons:

No Compile Time Error Checking to Ensure the Correct Enum Type is Used
For my current customer, they have some OptionSets that were created in CRM 4.0, before they had global OptionSets.  Once they upgraded to CRM 2011, they added global versions of these OptionSets as well, so if you’re not paying attention, you could easily be populating a local OptionSetValue on an entity, with the int value of a global OptionSetEnum (which have different int values).

Extra “Boiler Plate” Code Requires More KeyStrokes, and is Less Readable
Any time there is extra code that really isn’t needed, it makes it harder on the human brain to determine exactly what the code is doing because it in essence, has to ignore part of the information.

Just as 
(((x)*2)+1) = 13 
is harder to read than 
x*2+1 = 13 
so is

contact.Address1_AddressTypeCode = new OptionSetValue((int)contact_address1_addresstypecode.Home);

harder to read than

contact.Address1_AddressTypeCodeEnum = contact_address1_addresstypecode.Home;

25% of the text has been eliminated, which results in code that requires less typing, and is more readable.

Inconsistent Mapping of Attribute/Property Name to Enum Type
Determining the correct Enum for a given entity’s OptionSet property is usually pretty straight forward, but sometimes it’s not. i.e. The Enum for FieldPermission.CanCreate is field_security_permission_type and the Enum for Metric.AmountDataType is metric_goaltype.  The only intellisense that’s provided is that it’s an OptionSet, and it is up to the developer to find and choose the correct one.


The Entity OptionSet Enum Mapper Utility to the Rescue!
I have created an EntityOptionSetEnumMapper utility to generate a property for each OptionSetValue with the correct Enum.  For example, it generates these additional properties for the Contact class (CrmEntities is the namespace):

public partial class Contact
{
  public CrmEntities.contact_accountrolecode? AccountRoleCodeEnum { ... }
  public CrmEntities.contact_address1_addresstypecode? Address1_AddressTypeCodeEnum { ... }
  public CrmEntities.contact_address1_freighttermscode? Address1_FreightTermsCodeEnum { ... }
  public CrmEntities.contact_address1_shippingmethodcode? Address1_ShippingMethodCodeEnum { ... }
  public CrmEntities.contact_address2_addresstypecode? Address2_AddressTypeCodeEnum { ... }
  public CrmEntities.contact_address2_freighttermscode? Address2_FreightTermsCodeEnum { ... }
  public CrmEntities.contact_address2_shippingmethodcode? Address2_ShippingMethodCodeEnum { ... }
  public CrmEntities.contact_customersizecode? CustomerSizeCodeEnum { ... }
  public CrmEntities.contact_customertypecode? CustomerTypeCodeEnum { ... }
  public CrmEntities.contact_educationcode? EducationCodeEnum { ... }
  public CrmEntities.contact_familystatuscode? FamilyStatusCodeEnum { ... }
  public CrmEntities.contact_gendercode? GenderCodeEnum { ... }
  public CrmEntities.contact_haschildrencode? HasChildrenCodeEnum { ... }
  public CrmEntities.contact_leadsourcecode? LeadSourceCodeEnum { ... }
  public CrmEntities.contact_paymenttermscode? PaymentTermsCodeEnum { ... }
  public CrmEntities.contact_preferredappointmentdaycode? PreferredAppointmentDayCodeEnum { ... }
  public CrmEntities.contact_preferredappointmenttimecode? PreferredAppointmentTimeCodeEnum { ... }
  public CrmEntities.contact_preferredcontactmethodcode? PreferredContactMethodCodeEnum { ... }
  public CrmEntities.contact_shippingmethodcode? ShippingMethodCodeEnum { ... }
  public CrmEntities.contact_statuscode? StatusCodeEnum { ... }
  public CrmEntities.contact_territorycode? TerritoryCodeEnum { ... }
}

This is a partial class, and so will be combined at compile time with the existing Contact Class.  All of the existing OptionSetValue properties will still exist, but now there will be new ones (appended with “Enum”) that are typed to the correct Enum type.

Each property still uses the OptionSetValue property to store and retrieve the data, so when Address1_AddressTypeCodeEnum is updated, it will also update Address1_AddressTypeCode and visa versa, since they use the Entity.Attributes collection and share the same key.  Here is the Address1_AddressTypeCodeEnum property that is generated:

public CrmEntities.contact_address1_addresstypecode? Address1_AddressTypeCodeEnum
{
     get
     {
         return (CrmEntities.contact_address1_addresstypecode?)EntityOptionSetEnum.GetEnum(this, "address1_addresstypecode");
     }
     set
     {
         Address1_AddressTypeCode = value.HasValue ? new Microsoft.Xrm.Sdk.OptionSetValue((int)value) : null;
     }
}

It makes use of an internal static class that is also in the output file, that retrieves the OptionSet from the Attributes collection as a nullable int:

internal static class EntityOptionSetEnum
{
    public static int? GetEnum(Microsoft.Xrm.Sdk.Entity entity, string attributeLogicalName)
    {
        if(entity.Attributes.ContainsKey(attributeLogicalName))
        {
            return entity.GetAttributeValue<Microsoft.Xrm.Sdk.OptionSetValue>(attributeLogicalName).Value;
        }
        else
        {
            return null;
        }
    }
}

The nullable int is then cast to the correct enum type which is then returned. 

Using the Entity OptionSet Enum Mapper Executable
The only thing that the Entity OptionSet Enum Mapper requires is the dll that contains the early bound Entities and Enums created via the CrmSrvcUtil.  It requires no interaction with the CRM server itself.  This creates a standard “which came first, the chicken or the egg” problem.  The Entity and Enum dll must be built first before the utility can be ran, but it must run before the dll is built in order for the generated code to be included in the dll.  This is fixed by adding a pre-build event to the Entities and Enums dll project to build a temporary version of the dll, and then invoking the utility to run and generated the code.      

"C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe" /target:library /reference:"C:\PathOfSdk\microsoft.xrm.sdk.dll" "$(ProjectDir)Entities.cs" "$(ProjectDir)OptionSets.cs"

This basically says, compile the Entities and OptionSet files, referencing the Microsoft.Xrm.Sdk.  By default this will get compiled to the bin directory of the project, with the name Entities.dll.  A second pre-build event will be needed to call the utility and generate the EntityOptionSetEnum.cs file (which will need to be manually added to the project file the first time).

"C:\PathOfExe\EntityOptionSetEnumMapper.exe" "$(TargetDir)Entities.dll" "$(ProjectDir)EntityOptionSetEnums.cs"

This will run the utility, passing in the path to the Entities.dll to use to generate the EntityOptionSetEnum.cs file, and the path and name of the file to generate.

Both these steps happen before the dll itself get’s built, generating the EntityOptionSetEnum.cs file, which will then be compiled with the actual dll.


Below are separate downloads for the Executable, and the Source Code.  Enjoy!



Entity OptionSet Enum Mapper Executable
Entity OptionSet Enum Mapper Source

2 comments:

Unknown said...

Thanks for sharing this!
I try to use your code, but seems there is missing something in "EntityOptionSetEnum.cs" file, and it is not getting generated completely.
Please can you provide some guidance on that.

Daryl said...

Anand Patel Define missing and not getting generated completely.