Friday, May 13, 2016

Dynamics CRM Online: Posting Messages to the Azure Service Bus

I have to say I've never really liked the way Dynamics CRM integrates with the Azure Service Bus. I've posted on this before here and here. What I dislike is the way it posts the Context to the Service Bus.  In order to do anything with the Context I need to use the RemotePluginContext and then extract the entity that I want. That means using the XrmSDK and being familiar with the way to handle the Context. But I like a loosely coupled architecture. I would expect a CRM plugin to place a strongly typed Xml message on the Service Bus. Then processing of the message requires no knowledge of CRM. I'm a BizTalk developer and for interfaces are always about messages that comply with XML schemas because that acts as the contract.  In a development environment where you have different teams of developers with different skill sets this separation I believe is vital.

Now you may know that Dynamics CRM only supports the ACS authentication of the Service Bus and you have to create this using PowerShell commands because it is not possible via the portal. See this post. You can find the details of setting up the Service Endpoint elsewhere but the bottom line is that you are using a Guid that points to the Service Endpoint record. So what about deploying to another environment where you will use a different Service Endpoint? Well you have to go through the manual process again. What in a Production environment? Are you kidding me? No, I want to be able to deploy and then just configure the endpoint url.

The challenge in doing this online is that the plugin runs in Sandbox mode and it restricts what .Net assemblies I can use, System.Security being one of them. That rules out SAS authentication because it requires on System.Security.Cryptography.

My solution is a plugin that does two things. It populates a data table with the attributes from the entity and produces strongly typed Xml.  It then uses WebClient to POST the xml as a string to the Service Bus using the REST API. The authentication uses the ACS token and the only configuration parameters I need are the service bus namespace, the queue name, and the issuer secret. The great thing about the solution is it is totally generic, it works with any entity, all you need is to configure a plugin step. A couple of points to note.
1. I'm using a queue.
2. My plugin is registered as synchronous because I want to maintain ordered delivery
3. I'm sending to the queue synchronously, because I want to know I successfully sent it.
4. I can choose if I want to use the PreImage, PostImage or Target
5.The plugin calls my EntitySerialize class
6. I am indebted to other bloggers for much of this code

The next post shows you can retrieve the message form the queue.
using System;

using System.Collections.Specialized;
using System.Data;
using System.Linq;
using System.Text;
using System.Net;
using Microsoft.Xrm.Sdk;

public class EntityHelper
    {
        // **************Use bits from  the Service bus namespace below ******************
        //
        static string ServiceNamespace = "yournamespace";
        static string ServiceHttpAddress = "https://yournamespace.servicebus.windows.net/queuename/messages";
        const string acsHostName = "accesscontrol.windows.net";
        const string sbHostName = "servicebus.windows.net";
        const string issuerName = "owner";
        const string issuerSecret = "your_issuer_secret_goes_here";
        public static void EntitySerialize(ITracingService tracingService,  IOrganizationService service, Entity entity, string orgName, string msgName, string correlation)
        {
            try
            {
                DataSet ds = new DataSet(entity.LogicalName);
                DataTable dt = new DataTable("attributes");
                DataTable dataTable = new DataTable();
                ConvertEntityToDataTable(dt, entity);
                ds.Tables.Add(dt);
                string xml = ds.GetXml();
                PostMessageToBus(entity, orgName, msgName, correlation, xml);
            }
            catch (System.Net.WebException  we)
            {
                tracingService.Trace(we.Message);
                throw new Exception("Web Exception: " +we.Message);
            }
            catch (Exception e)
            {
                tracingService.Trace(e.Message);
                throw e;
            }
            return ;
        }
        private static void PostMessageToBus(Entity entity, string orgName, string msgName, string correlation, string xml)
        {
            WebClient webClient = new WebClient();
            webClient.Headers[HttpRequestHeader.Authorization] = GetToken();
            webClient.Headers[HttpRequestHeader.ContentType] = "application/atom+xml;type=entry;charset=utf-8";
            // Set brokered Properties this way
            webClient.Headers.Add("BrokerProperties", "{ \"MessageId\":\"123456789\", \"Label\":\"M1\"}");
            // example of the parameters that can be passed and available to receiver as mesage.Properties collection
            webClient.Headers["Correlation"] = correlation;
            webClient.Headers["OrganisationName"] = orgName;
            webClient.Headers["MessageName"] = msgName;
            webClient.Headers["EntityName"] = entity.LogicalName;
            var response = webClient.UploadData(ServiceHttpAddress, "POST", System.Text.Encoding.UTF8.GetBytes(xml));
            string responseString = Encoding.UTF8.GetString(response);
        }
        ///////
          private static void ConvertEntityToDataTable(DataTable dataTable, Entity entity)
         {
             DataRow row = dataTable.NewRow();
             foreach (var attribute in entity.Attributes.OrderBy(a=>a.Key))
             {
                 if (!dataTable.Columns.Contains(attribute.Key))
                 {
                     dataTable.Columns.Add(attribute.Key);
                 }
                 if (getAttributeValue(attribute.Value) != null)
                 {
                     row[attribute.Key] = getAttributeValue(attribute.Value).ToString();
                 }
             }
             foreach (var fv in entity.FormattedValues.OrderBy(a=>a.Key))
             {
                 if (!dataTable.Columns.Contains(fv.Key + "name"))
                 {
                     dataTable.Columns.Add(fv.Key + "name");
                 }
                 row[fv.Key + "name"] = fv.Value;
             }
             dataTable.Rows.Add(row);
         }
        ///////
         private static object getAttributeValue(object entityValue)
         {
             object output = "";
             switch (entityValue.ToString())
             {
                 case "Microsoft.Xrm.Sdk.EntityReference":
                     output = ((EntityReference)entityValue).Name;
                     break;
                 case "Microsoft.Xrm.Sdk.OptionSetValue":
                     output = ((OptionSetValue)entityValue).Value.ToString();
                     break;
                 case "Microsoft.Xrm.Sdk.Money":
                     output = ((Money)entityValue).Value.ToString();
                     break;
                 case "Microsoft.Xrm.Sdk.AliasedValue":
                     output = getAttributeValue(((Microsoft.Xrm.Sdk.AliasedValue)entityValue).Value);
                     break;
                 default:
                     output = entityValue.ToString();
                     break;
             }
             return output;
         }
         private static string GetToken()
         {
             var acsEndpoint = "https://" + ServiceNamespace + "-sb." + acsHostName + "/WRAPv0.9/";
             // Note that the realm used when requesting a token uses the HTTP scheme, even though
             // calls to the service are always issued over HTTPS
             var realm = "http://" + ServiceNamespace + "." + sbHostName + "/";
             NameValueCollection values = new NameValueCollection();
             values.Add("wrap_name", issuerName);
             values.Add("wrap_password", issuerSecret);
             values.Add("wrap_scope", realm);
             WebClient webClient = new WebClient();
             byte[] response = webClient.UploadValues(acsEndpoint, values);
             string responseString = Encoding.UTF8.GetString(response);
             var responseProperties = responseString.Split('&');
             var tokenProperty = responseProperties[0].Split('=');
             var token = Uri.UnescapeDataString(tokenProperty[1]);
             return "WRAP access_token=\"" + token + "\"";
         }

     }
}