Monthly Archives: March 2013

Accessing Microsoft Dynamics CRM 4 Web Services from Windows 8 Modern UI App

Visual Studio 2012 is a big improvement overall but the ability to add a web reference has now disappeared completely, even from the “Advanced” button in the add service reference feature as per VS2010. Although most documentation seems to suggest adding an old asmx service as a service reference should work fine, it doesn’t always. The solution below works for the Microsoft Dynamics v4 web services but it is probably also valid for consuming other old style asmx services in Modern UI apps.

The 2 main problems I encountered were

  • I couldn’t add a custom header. In this case the CrmAuthenticationToken which defines which authentication protocol to use. Without this I’d simply get “The HTTP request is unauthorized with client authentication scheme ‘Negotiate’. The authentication header received from the server was ‘Negotiate,NTLM’.”
  • When you import the service reference, the autogenerated code in References.cd create the elements as XmlElementAttribute objects with an order by e.g. XmlElementAttribute(Order=1) which cause elements to mysteriously get set to null even though Fiddler proves that they are definitely not null. The reason is that the code doesn’t read them if they appear out of order in the response XML which in the Dynamics 4 API, they often are.

The solution to the first problem was mostly provided in this blog post, with just a few tweaks needed to make it work with the Windows Store App framework. It essentially creates a new EndPoint behavior which manually adds the CrmAuthenticationToken into the header. Here is the class in it’s entirety.

using MyCompany.Windows8.App.PersonHelper.cRM;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.Text;
using System.Threading.Tasks;
using System.Xml;

namespace MyCompany.Windows8.App.PersonHelper.ServiceCall
{

    public class CrmServiceConnectionParams
    {
        public String Scheme { get; set; }

        public String Url { get; set; }

        public CrmAuthenticationToken AuthenticationToken { get; set; }

        public BasicHttpSecurityMode SecurityMode { get; set; }

        private void setSecurityModeFromScheme()
        {
            switch (Scheme)
            {
                case "https":
                    SecurityMode = BasicHttpSecurityMode.Transport;
                    break;
                default:
                    SecurityMode = BasicHttpSecurityMode.None;
                    break;
            }
        }

        public CrmServiceConnectionParams(String url, CrmAuthenticationToken token)
        {
            if (url.Contains("://"))
            {
                string[] urlSplit = url.Split(new string[] { "://" }, StringSplitOptions.None);

                Scheme = urlSplit[0];
                Url = urlSplit[1];
            }
            else
                throw new ArgumentException("Failure creating CrmServiceConnectionParams instance. Invalid or missing URL scheme (e.g. 'http://').");

            AuthenticationToken = token;

            setSecurityModeFromScheme();
        }

        public CrmServiceConnectionParams(String scheme, String url, CrmAuthenticationToken token)
        {
            Scheme = scheme;
            Url = url;
            AuthenticationToken = token;

            setSecurityModeFromScheme();
        }

        public CrmServiceConnectionParams(String scheme, String url, CrmAuthenticationToken token, BasicHttpSecurityMode securityMode)
        {
            Scheme = scheme;
            Url = url;
            AuthenticationToken = token;
            SecurityMode = securityMode;
        }
    }

    public class CrmServiceInstance
    {
        private CrmServiceConnectionParams connectionParams;
        public CrmServiceConnectionParams ConnectionParams
        {
            get { return connectionParams; }
            set
            {
                connectionParams = value;
                spawnCrmService();
            }
        }

        private CrmServiceSoapClient crmService;
        public CrmServiceSoapClient CrmService
        {
            get { return crmService; }
            set
            {
                crmService = value;
                isCrmServiceReady = true;

                if (OnCrmServiceReady != null)
                    OnCrmServiceReady(this, new EventArgs());
            }
        }

        #region OnCrmServiceReady Event

        public event EventHandler<EventArgs> OnCrmServiceReady;

        private bool isCrmServiceReady;
        public bool IsCrmServiceReady
        {
            get { return isCrmServiceReady; }
        }

        #endregion

        public CrmServiceSoapClient CreateCrmService(String crmServiceUrl, CrmAuthenticationToken authToken, BasicHttpSecurityMode securityMode)
        {
            BasicHttpBinding httpBinding = new BasicHttpBinding(securityMode);
            httpBinding.MaxReceivedMessageSize = Int32.MaxValue;
            httpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Windows;

            EndpointAddress crmEndpoint = new EndpointAddress(crmServiceUrl);

            CrmServiceSoapClient crmService = new CrmServiceSoapClient(httpBinding, crmEndpoint);

            MessageHeader authTokenHeader = MessageHeader.CreateHeader("CrmAuthenticationToken",
            "http://schemas.microsoft.com/crm/2007/WebServices", string.Empty, new CrmAuthenticationTokenSerializer(authToken));

            crmService.ChannelFactory.Endpoint.EndpointBehaviors.Add(new CrmServiceBehavior(new CrmServiceMessageInspector(authTokenHeader)));
            crmService.ClientCredentials.Windows.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation;
            crmService.ClientCredentials.Windows.ClientCredential = System.Net.CredentialCache.DefaultNetworkCredentials;

            return crmService;
        }

        private void spawnCrmService()
        {
            CrmService = CreateCrmService(
            ConnectionParams.Scheme + "://" + ConnectionParams.Url,
            ConnectionParams.AuthenticationToken,
            ConnectionParams.SecurityMode);
        }

        public CrmServiceInstance()
        {
            isCrmServiceReady = false;
        }

        private class CrmServiceMessageInspector : IClientMessageInspector
        {
            public MessageHeader ServiceHeader;

            #region IClientMessageInspector Members

            public void AfterReceiveReply(ref Message reply, object correlationState) { }

            public object BeforeSendRequest(ref Message request, IClientChannel channel)
            {
                request.Headers.Add(ServiceHeader);
                return null;
            }

            #endregion

            public CrmServiceMessageInspector(MessageHeader header)
            {
                ServiceHeader = header;
            }
        }

        private class CrmServiceBehavior : IEndpointBehavior
        {
            public CrmServiceMessageInspector ServiceInspector;

            #region IEndpointBehavior Members

            public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { }

            public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
            {
                clientRuntime.ClientMessageInspectors.Add(ServiceInspector);
            }

            public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher dispatcher)
            {
                throw new NotImplementedException(); // Silverlight does not invoke this method.
            }

            public void Validate(ServiceEndpoint endpoint) { }

            #endregion

            public CrmServiceBehavior(CrmServiceMessageInspector inspector)
            {
                ServiceInspector = inspector;
            }
        }
    }

    public class CrmAuthenticationTokenSerializer : XmlObjectSerializer
    {
        #region CrmAuthenticationTokenSerializer Members

        private readonly string authType;
        private readonly string organizationName;
        private readonly string callerId;
        private readonly string crmTicket;

        public CrmAuthenticationTokenSerializer(CrmAuthenticationToken authToken)
        {
            callerId = Guid.Empty.ToString();
            authType = authToken.AuthenticationType.ToString();
            organizationName = authToken.OrganizationName;
            crmTicket = authToken.CrmTicket;
        }

        #endregion

        #region XmlObjectSerializer Members

        public override bool IsStartObject(XmlDictionaryReader reader)
        {
            return true;
        }

        public override object ReadObject(XmlDictionaryReader reader, bool verifyObjectName)
        {
            return null;
        }

        public override void WriteEndObject(XmlDictionaryWriter writer)
        {
            return;
        }

        public override void WriteObjectContent(XmlDictionaryWriter writer, object graph)
        {
            string tokenXmlLiteral = String.Empty;

            tokenXmlLiteral += "<AuthenticationType xmlns='http://schemas.microsoft.com/crm/2007/CoreTypes'>"
            + authType
            + "</AuthenticationType>";

            if (crmTicket != null && crmTicket != String.Empty)
                tokenXmlLiteral += "<CrmTicket xmlns='http://schemas.microsoft.com/crm/2007/CoreTypes'>"
                + crmTicket
                + "</CrmTicket>";

            tokenXmlLiteral += "<OrganizationName xmlns='http://schemas.microsoft.com/crm/2007/CoreTypes'>"
            + organizationName
            + "</OrganizationName>"
            + "<CallerId xmlns='http://schemas.microsoft.com/crm/2007/CoreTypes'>"
            + callerId
            + "</CallerId>";

            writer.WriteRaw(tokenXmlLiteral);
        }

        public override void WriteStartObject(XmlDictionaryWriter writer, object graph)
        {
            return;
        }

        #endregion
    }
}

So from the code that you want to make the call, you just create the client by using

CrmAuthenticationToken authToken = new CrmAuthenticationToken();
authToken.AuthenticationType = 0;
authToken.OrganizationName = "MyCompany";
authToken.CallerId = new Guid("00000000-0000-0000-0000-000000000000");

CrmServiceInstance instance = new CrmServiceInstance();
CrmServiceSoapClient client = instance.CreateCrmService("http://mycrmservername/MSCrmServices/2007/CrmService.asmx", authToken, BasicHttpSecurityMode.TransportCredentialOnly);

The solution to the 2nd problem was resolved in this blog post. Here are the steps I made to get it working.

  1. Click on PROJECT -> Show all files
    Show all files
  2. In the navigation pane browse to your service reference and open References.csOpen references file
  3. Open up Quick Find/Replace and search for “(\s*Order=\d+\s*\,?\s*)|(\,?\s*Order=\d+\s*)”, replace with nothing (i.e. blank) and make sure the replace all button is pressed. Then click Replace all. Note that if you ever re-import the service reference you will need to repeat thisFindReplace