Tuesday, December 28, 2010

How to use the CRM 2011 Organization (SOAP) Service from Silverlight

Now that the CRM 2011 RC1 is out it’s finally possible to use the SOAP web service from Silverlight applications that are deployed as web-resources to the CRM server.  The SOAP service offers a number of advantages over the other REST service, such as the ability to retrieve CRM metadata, execute fetchxml queries, and the ability to work with dynamic entity objects instead of strongly typed classes.   

Unfortunately the SDK still doesn’t provide much information, and a number of people have been asking in the beta forums about how to get this working.  The page titled “Use the SOAP endpoint for Silverlight clients” currently just says “Content coming soon”.  On top of that there are still a couple of serialization bugs that prevent certain SDK messages from working correctly when sent from a Silverlight client.  But we can get around these bugs with a couple tricks.  Here’s how you do it:

  1. Add the Organization service as a Service Reference to your Silverlight project

    First you need to add the service reference. You can do this the same way you would add a reference for the REST service.  In CRM, go to Settings -> Customizations -> Developer Resources, and download the WSDL file for the organization service. Then in your Silverlight project click Add Service Reference, and enter the path to the wsdl file you downloaded.  


  2. Fixing the service reference classes

    Now that we have the service reference, we need to make some modifications to it to get around those serialization bugs.  Without these changes some of the SDK messages (such as retrieving metadata) will fail.  Microsoft has already indicated that they are working on these issues, so hopefully these changes are just be a temporary workaround until it gets fixed for real.
    The main problem is that the Reference.cs file generated by Visual Studio doesn’t serialize the requests in the same format that CRM is expecting, but we can fix this by making some modifications to Reference.cs.  Find the Reference.cs file that was generated by Visual Studio when you added the service reference and open it in Visual Studio. 

    The first issue is that the reference doesn’t create serializers for certain classes. When trying to retrieve metadata you will see a "Type 'EntityFilters' with data contract name 'EntityFilters:http://schemas.microsoft.com/xrm/2011/Metadata' is not expected" error.  You can fix this by adding a knownTypeAttribute to the OrganizationRequest class, like this:

    [System.Runtime.Serialization.KnownTypeAttribute(typeof(EntityFilters))]
    public
    partial class OrganizationRequest : object, System.ComponentModel.INotifyPropertyChanged {

    You will also need to add the following KnownTypeAttribute to the OrganizationResponse class:


    [System.Runtime.Serialization.KnownTypeAttribute(typeof(EntityMetadata))]
    public
    partial class OrganizationResponse : object, System.ComponentModel.INotifyPropertyChanged {

    Note: These are the only two KnownTypeAttributes I had to add, but I didn’t test every possibly type of CRM SDK message. It’s possible that other SDK message will require adding more KnownTypeAttributes to the reference.cs file.  Fortunately the error message you will get is pretty descriptive and will tell you which type you need to add the KnownTypeAttribute for. 


    UPDATE:  you will also need to add the following KnownTypeAttributes to the Entity class:
    [System.Runtime.Serialization.KnownTypeAttribute(typeof(OptionSetValue))]
    [System.Runtime.Serialization.KnownTypeAttribute(typeof(EntityReference))]
    public partial class Entity : object, System.ComponentModel.INotifyPropertyChanged




    The second issue is the way that it serializes the KeyValuePair class.  For some reason it serializes them as "Key" and "Value" (with capitalization), while CRM will only accept "key" and "value" (all lowercase).  To get around this you can create your own KeyValuePair class and replace all references to System.Collections.Generic.KeyValuePair in Reference.cs with your own class.  So first create the following class (I just put this class in the same namespace as the rest of the classes in Reference.cs):


    [DataContract(Name = "KeyValuePairOfstringanyType", Namespace = "http://schemas.datacontract.org/2004/07/System.Collections.Generic")]
    public
    class KeyValuePair<TKey, TValue>
    {

        public KeyValuePair(TKey key, TValue value)

        {

            this._key = key;

            this._value = value;

        }


        private TKey _key;

        private TValue _value;


        [DataMember(Name = "key")]

        public TKey Key

        {

            get { return _key; }

            set { _key = value; }

        }
        [DataMember(Name = "value")]
        public TValue Value

        {

            get { return _value; }

            set { _value = value; }

        }

    }


    Then do a find-and-replace in the Reference.cs file and replace "System.Collections.Generic.KeyValuePair" with "[yournamespace].KeyValuePair".   The new KeyValuePair class will actually serialize the way CRM wants it to, and you will be able to execute requests. 


    UPDATE:  Don't replace every instance of KeyValuePair in the reference.cs file.  You actually do not want to replace any of the KeyValuePairs that appear inside KnownTypeAttribute attributes.


  3. Use the service reference

    To use the service you need to have the correct binding.  The following code will set up the binding correctly for an on-premise installation:


    String url = "http://myServer/myOrganization/xrmservices/2011/organization.svc/web";
    BasicHttpBinding
    binding = new BasicHttpBinding(BasicHttpSecurityMode.None);
    binding.MaxReceivedMessageSize = 2147483647;

    OrganizationServiceClient
    client = new OrganizationServiceClient(binding, new EndpointAddress(url));

    There are three interesting thing in here. First, notice is that the url has “/web” on the end of it. This is required for the organization service to work from Silverlight.  Second, notice that the binding is using the “None” security mode, because the URL is using the HTTP scheme.  If you need to connect over HTTPS then you will need to use BasicHttpSecurityMode.Transport instead of BasicHttpSecurityMode.None.   CRM Online always uses HTTPs so it will always require the Transport security mode. 


    Also, if you’re using CRM Online you will need to make sure the URL has the exact same domain as the page hosting the Silverlight application.  The CRM documentation claims you should connect to [organization].api.crm.dynamic.com, but this won’t work because the Silverlight application you deploy as a web resource will not be on that domain.  The domain used for web-resources won’t have the “.api” in it, so your web-service URL should be the same.
     
  4. Executing service requests

    Making requests to the organization service from Silverlight is almost the same as from C# code.  The main difference is that you do not have the strongly typed entity classes that are generated by CrmSvcUtil.exe, and you do not have any of the strongly typed message classes found in Microsoft.Xrm.Sdk.dll.  For Create, Update, and Retrieve messages you will have to use the late-bound Entity class, and set attributes using the entity[“propertyname”] = value syntax.


    For Execute messages, you will have to use the dynamic OrganizationRequest class, and add all the parameters required for each specific message.  For example, the following will retrieve metadata for the account entity:



    private void button1_Click(object sender, RoutedEventArgs e)
    {
        OrganizationServiceClient client = GetOrganizationService();
               
        OrganizationRequest req = new OrganizationRequest();
        req.RequestName = "RetrieveEntity";
        req.Parameters = new ParameterCollection();
        req.Parameters.Add(new KeyValuePair<string, object>("LogicalName", "ae_tasker"));
        req.Parameters.Add(new KeyValuePair<string, object>("EntityFilters", EntityFilters.Attributes));
        req.Parameters.Add(new KeyValuePair<string, object>("MetadataId", new Guid("00000000-0000-0000-0000-000000000000")));
        req.Parameters.Add(new KeyValuePair<string, object>("RetrieveAsIfPublished", false));

        client.ExecuteCompleted += new EventHandler<ExecuteCompletedEventArgs>(client_ExecuteCompleted);
        client.ExecuteAsync(req);
    }

    void client_ExecuteCompleted(object sender, ExecuteCompletedEventArgs e)
    {
        EntityMetadata data = (EntityMetadata)e.Result.Results[0].Value;
        string s = "attributes: ";
        for (int i = 0; i < 10; i++)
        {
            s += data.Attributes[i].LogicalName + ", ";
        }
        MessageBox.Show(s);
    }


    Update: Added an example silveright application making SOAP requests to CRM (including source code and all the changes made to the reference.cs file).  You can download it here.