Sunday, March 20, 2011

Filtering generated entities with CrmSvcUtil

In CRM 2011 you can create early-bound entity classes using the code generation utility (CrmSvcUtil.exe) that comes with the SDK.  This gives you nice strongly-typed entity classes that you can use with full intellisense in Visual Studio and in linq queries (and many other benefits).

However, the cs file generated from this utility can be over 5 - 10MB in size, which is a lot when you want to include it in a CRM plug-in where you should try to keep your assembly as small as possible.

By default the utility will generate classes for every entity in the CRM organization, but fortunately Microsoft has provided a way to filter which entities are generated.

To filter the entities that are generate, we need to create an extension for the CrmSvcUtil utility. Basically, we have to create a small class library that implements an interface used by the utility. The SDK provides a little bit of info, but not much in the way of examples.  So here's what we need to do:
  1. Create a new C# class library project in Visual Studio called SvcUtilFilter.

  2. In the project, add references to the following:
    1. CrmSvcUtil.exe   This exe has the interface we will implement.
    2. Microsoft.Xrm.Sdk.dll  (found in the CRM SDK).
    3. System.Runtime.Serialization.

  3.   Add the following class to the project:

using System;
using System.Collections.Generic;
using System.Xml.Linq;
using Microsoft.Crm.Services.Utility;
using Microsoft.Xrm.Sdk.Metadata;

namespace SvcUtilFilter
{
    /// <summary>
    /// CodeWriterFilter for CrmSvcUtil that reads list of entities from an xml file to
    /// determine whether or not the entity class should be generated.
    /// </summary>
    public class CodeWriterFilter : ICodeWriterFilterService
    {
        //list of entity names to generate classes for.
        private HashSet<string> _validEntities = new HashSet<string>();
       
        //reference to the default service.
        private ICodeWriterFilterService _defaultService = null;

        /// <summary>
        /// constructor
        /// </summary>
        /// <param name="defaultService">default implementation</param>
        public CodeWriterFilter( ICodeWriterFilterService defaultService )
        {
            this._defaultService = defaultService;
            LoadFilterData();
        }

        /// <summary>
        /// loads the entity filter data from the filter.xml file
        /// </summary>
        private void LoadFilterData()
        {
            XElement xml = XElement.Load("filter.xml");
            XElement entitiesElement = xml.Element("entities");
            foreach (XElement entityElement in entitiesElement.Elements("entity"))
            {
                _validEntities.Add(entityElement.Value.ToLowerInvariant());
            }
        }

        /// <summary>
        /// /Use filter entity list to determine if the entity class should be generated.
        /// </summary>
        public bool GenerateEntity(EntityMetadata entityMetadata, IServiceProvider services)
        {
            return (_validEntities.Contains(entityMetadata.LogicalName.ToLowerInvariant()));
        }

        //All other methods just use default implementation:

        public bool GenerateAttribute(AttributeMetadata attributeMetadata, IServiceProvider services)
        {
            return _defaultService.GenerateAttribute(attributeMetadata, services);
        }

        public bool GenerateOption(OptionMetadata optionMetadata, IServiceProvider services)
        {
            return _defaultService.GenerateOption(optionMetadata, services);
        }

        public bool GenerateOptionSet(OptionSetMetadataBase optionSetMetadata, IServiceProvider services)
        {
            return _defaultService.GenerateOptionSet(optionSetMetadata, services);
        }

        public bool GenerateRelationship(RelationshipMetadataBase relationshipMetadata, EntityMetadata otherEntityMetadata, IServiceProvider services)
        {
            return _defaultService.GenerateRelationship(relationshipMetadata, otherEntityMetadata, services);
        }

        public bool GenerateServiceContext(IServiceProvider services)
        {
            return _defaultService.GenerateServiceContext(services);
        }
    }
}

    This class implements the ICodeWriterFilterService interface.  This interface is used by the class generation utility to determine which entities, attrributes, etc. should actually be generated.  The interface is very simple and just has seven methods that are passed metadata info and return a boolean indicating whether or not the metadata should be included in the generated code file.  

    For now I just want to be able to determine which entities are generated, so in the constructor I read from an XML file (filter.xml) that holds the list of entities to generate and put the list in a Hashset.  The format of the xml is this:

    <filter>
      <entities>
        <entity>systemuser</entity>
        <entity>team</entity>
        <entity>role</entity>
        <entity>businessunit</entity>
      </entities>
    </filter>

    Take a look at the methods in the class. In the GenerateEntity method, we can simply check the EntityMetadata parameter against our list of valid entities and return true if it's an entity that we want to generate.

    For all of the other methods we want to just do whatever the default implementation of the utility is.  Notice how the constructor of the class accepts a defaultService parameter.  We can just save a reference to this default service and use it whenever we want to stick with the default behavior.  All of the other methods in the class just call the default service.

    To use our extension when running the utility, we just have to make sure the compiled DLL and the filter.xml file are in the same folder as CrmSvcUtil.exe, and set the /codewriterfilter command-line argument when running the utility (as described in the SDK):

    crmsvcutil.exe /url:http://<server>/<org>/XrmServices/2011/Organization.svc /out:sdk.cs  /namespace:<namespace> /codewriterfilter:SvcUtilFilter.CodeWriterFilter,SvcUtilFilter

    That's it! You now have a generated sdk.cs file that is only a few hundred kilobytes instead of 5MB.

    One final note:  There is actually a lot more you can do with extensions to the code generation utility.  For example: if you return true in the GenerateOptionSet method, it will actually generated Enums for each CRM picklist (which it doesn't normally do by default).

    Also, the source code for this SvcUtilFilter example can be found here.  Use at your own risk, no warranties, etc. etc.

    -Erik Pool

    12 comments:

    1. Hi Erik,
      1. The latest version of CrmSvcUtil has two more methods GenerateSdkMessage,GenerateSdkMessagePair which you need to implement
      2. I tried your code and it didn't work. When I try to run crmsvcutil.exe and provide this custom class as parameter, I got error in the command line saying 'Exception has been thrown by the target of an invocation'. Enable tracing didn't brought much.

      ReplyDelete
    2. Hmm, I just tried it with the CrmSvcUtil from the latest SDK (v5.0.9688.583, released on March 2nd) and didn't have any issues. And I don't see anything about GenerateSdkMessage or GenerateSdkMessagePair. Are you saying you see those two methods in the ICodeWriterFilterService interface?

      ReplyDelete
    3. Yep It worked for me too. I was trying to generate Enums. Although I noticed that if I request enumeration for each option set by just returning true in GenerateOptionSet then generated code has some naming issues in some enums. I think I should better use the filtration for enums. Like return true only for option sets where I need an enum.

      ReplyDelete
    4. Excellent, and just what I need. But this seems to be 2011 only. I tried this with CRM 4.0 and it doesn't seem to work?? Is this a new feature with 2011 only?

      ReplyDelete
    5. Yeah, this will only work for CRM 2011. For CRM 4, your options are to either just use the DynamicEntity class, or go through the generated service reference and manually strip out the unneeded classes. Or there may be some tools out there for stripping out unused code from C# projects (resharper maybe?)

      ReplyDelete
    6. Great article! Thanks for the example. Seems that there are still some issues with how OptionSets are converted to enums, even beyond a separate post I read on the naming convention (special characters, first char #, etc.).

      It appears that the utility will duplicate the same enum for global Optionsets, once for the global definition, then again each time it is referenced by an entity attribute. I also noticed that the type conversion in OptionSetValue property setters contain an invalid cast for (int)value where value is an OptionSetValue. Will have to do some additional investigation to see if this is something that can be overcome via the interfaces provided or if we wait for an SDK update to be released.

      ReplyDelete
    7. Austin Jones : I noticed the exact same thing. It's unfortunate, as we have to edit the generated file after each regeneration. Not ideal, when in dev and things are changing rapidly.

      ReplyDelete
    8. Has anyone had a problem generating the enums when using the CodeCustomization extensions from the SDK?
      Described here:
      http://msdn.microsoft.com/en-us/library/gg695820.aspx

      Basically, when I use the extensions, I do not get any enums and when I omit the extensions, no enums are created.

      Also, I noticed that if you create a secondary filter that does not generate entities you only seem to get only global option sets. I'm not sure if that will help anyone.

      As far as the problem with the naming of the enums, check out this post, which incidentally references this blog.
      http://manny-grewal.blogspot.com/2011/03/generate-enum-for-option-set-picklist.html

      ReplyDelete
    9. What a solution!!............I got relief from the plugin registration issue...:)
      Thanks Erik Pool.

      ReplyDelete
    10. This utility also allows you to specify Entities to not generate along with a whole list of other features: https://xrmearlyboundgenerator.codeplex.com/

      ReplyDelete