Tuesday, June 28, 2011

Step-by-step Walkthrough: Use CRM 2011 Organization (SOAP) Service from a Console Application

[DISCLAIMER] This blog post should be used for reference only, I wouldn't recommend using this approach in your production code, since many default options generated by WCF service reference don't actually work for CRM2011, and also it would require significant effort to make it work for CRM Online and IFD deployments. In addition to this blog post, you should check out Girish Raja's TechEd session[END OF DISCLAIMER]

This blog post describes how to consume CRM 2011 Organization (SOAP) services from a console application, which can also be used for your Windows Service, Windows Form application, and probably even another WCF service that you may want to develop in order to delegate the service calls to CRM Server. This can be considered as a supplementary guideline of CRM SDK document, which is currently missing as of the latest SDK v5.0.4.

This blog post is primarily inspired by and based on CRM document - Walkthrough: Use the SOAP Endpoint for Web Resources with Silverlight.

Let's get started.

Create the Silverlight Project in Visual Studio 2010

In Visual Studio 2010 create a Console application. This walkthrough will use the name SoapFromConsoleApp, but you can use whatever name you wish. You will need to make changes as necessary because the name of the project is also the default namespace for the application.

Add a Service Reference to the Organization Service.

In the SoapFromConsoleApp project, right-click References and select Add Service Reference from the context menu.
  1. In the Add Service Reference dialog box type the URL to the Organization service and click Go.
    The URL to the service is located on the Developer Resources page of Microsoft Dynamics CRM 2011. In the Settings area select Customizations and then select Developer Resources.
    The URL has the format <organization URL>/XRMServices/2011/Organization.svc
  2. Enter a namespace in the Namespace field and then click OK.
    This walkthrough will use the namespace CrmSdk.
Add Supporting Classes and Edit files
  1. In the SoapFromConsoleApp project, add a new file with a class called XrmExtensionMethods.cs with the following code. The namespace for this class must match the namespace of your project.


    using System;
    using System.Collections.Generic;
    using System.Runtime.Serialization;
    
    namespace SoapFromConsoleApp.CrmSdk
    {
        partial class Entity
        {
            public Entity()
            {
                this.FormattedValuesField = new FormattedValueCollection();
                this.RelatedEntitiesField = new RelatedEntityCollection();
            }
    
            public T GetAttributeValue<T>(string attributeLogicalName)
            {
                if (null == this.Attributes) { this.Attributes = new AttributeCollection(); };
    
                object value;
                if (this.Attributes.TryGetValue(attributeLogicalName, out value))
                {
                    return (T)value;
                }
    
                return default(T);
            }
    
            public object this[string attributeName]
            {
                get
                {
                    if (null == this.Attributes) { this.Attributes = new AttributeCollection(); };
                    return this.Attributes.GetItem(attributeName);
                }
    
                set
                {
                    if (null == this.Attributes) { this.Attributes = new AttributeCollection(); };
                    this.Attributes.SetItem(attributeName, value);
                }
            }
        }
    
        [KnownType(typeof(AppointmentRequest))]
        [KnownType(typeof(AttributeMetadata))]
        [KnownType(typeof(ColumnSet))]
        [KnownType(typeof(DateTime))]
        [KnownType(typeof(Entity))]
        [KnownType(typeof(EntityCollection))]
        [KnownType(typeof(EntityFilters))]
        [KnownType(typeof(EntityMetadata))]
        [KnownType(typeof(EntityReference))]
        [KnownType(typeof(EntityReferenceCollection))]
        [KnownType(typeof(Label))]
        [KnownType(typeof(LookupAttributeMetadata))]
        [KnownType(typeof(ManyToManyRelationshipMetadata))]
        [KnownType(typeof(OneToManyRelationshipMetadata))]
        [KnownType(typeof(OptionSetMetadataBase))]
        [KnownType(typeof(OptionSetValue))]
        [KnownType(typeof(PagingInfo))]
        [KnownType(typeof(ParameterCollection))]
        [KnownType(typeof(PrincipalAccess))]
        [KnownType(typeof(PropagationOwnershipOptions))]
        [KnownType(typeof(QueryBase))]
        [KnownType(typeof(Relationship))]
        [KnownType(typeof(RelationshipMetadataBase))]
        [KnownType(typeof(RelationshipQueryCollection))]
        [KnownType(typeof(RibbonLocationFilters))]
        [KnownType(typeof(RollupType))]
        [KnownType(typeof(StringAttributeMetadata))]
        [KnownType(typeof(TargetFieldType))]
        partial class OrganizationRequest
        {
            public object this[string key]
            {
                get
                {
                    if (null == this.Parameters) { this.Parameters = new ParameterCollection(); };
    
                    return this.Parameters.GetItem(key);
                }
    
                set
                {
                    if (null == this.Parameters) { this.Parameters = new ParameterCollection(); };
    
                    this.Parameters.SetItem(key, value);
                }
            }
        }
    
        [KnownType(typeof(AccessRights))]
        [KnownType(typeof(AttributeMetadata))]
        [KnownType(typeof(AttributePrivilegeCollection))]
        [KnownType(typeof(AuditDetail))]
        [KnownType(typeof(AuditDetailCollection))]
        [KnownType(typeof(AuditPartitionDetailCollection))]
        [KnownType(typeof(DateTime))]
        [KnownType(typeof(Entity))]
        [KnownType(typeof(EntityCollection))]
        [KnownType(typeof(EntityMetadata))]
        [KnownType(typeof(EntityReferenceCollection))]
        [KnownType(typeof(Guid))]
        [KnownType(typeof(Label))]
        [KnownType(typeof(ManagedPropertyMetadata))]
        [KnownType(typeof(OptionSetMetadataBase))]
        [KnownType(typeof(OrganizationResources))]
        [KnownType(typeof(ParameterCollection))]
        [KnownType(typeof(QueryExpression))]
        [KnownType(typeof(RelationshipMetadataBase))]
        [KnownType(typeof(SearchResults))]
        [KnownType(typeof(ValidationResult))]
        partial class OrganizationResponse
        {
            public object this[string key]
            {
                get
                {
                    if (null == this.Results) { this.Results = new ParameterCollection(); };
    
                    return this.Results.GetItem(key);
                }
            }
        }
    
        public static class CollectionExtensions
        {
            public static TValue GetItem<TKey, TValue>(this IList<KeyValuePair<TKey, TValue>> collection, TKey key)
            {
                TValue value;
                if (TryGetValue(collection, key, out value))
                {
                    return value;
                }
    
                throw new KeyNotFoundException("Key = " + key);
            }
    
            public static void SetItem<TKey, TValue>(this IList<KeyValuePair<TKey, TValue>> collection, TKey key, TValue value)
            {
                int index;
                if (TryGetIndex<TKey, TValue>(collection, key, out index))
                {
                    collection.RemoveAt(index);
                }
    
                //If the value is an array, it needs to be converted into a List. This is due to how Silverlight serializes
                //Arrays and IList<T> objects (they are both serialized with the same namespace). Any collection objects will
                //already add the KnownType for IList<T>, which means that any parameters that are arrays cannot be added
                //as a KnownType (or it will throw an exception).
                Array array = value as Array;
                if (null != array)
                {
                    Type listType = typeof(List<>).GetGenericTypeDefinition().MakeGenericType(array.GetType().GetElementType());
                    object list = Activator.CreateInstance(listType, array);
                    try
                    {
                        value = (TValue)list;
                    }
                    catch (InvalidCastException)
                    {
                        //Don't do the conversion because the types are not compatible
                    }
                }
    
                collection.Add(new KeyValuePair<TKey, TValue>() { Key = key, Value = value });
            }
    
            public static bool ContainsKey<TKey, TValue>(this IList<KeyValuePair<TKey, TValue>> collection, TKey key)
            {
                int index;
                return TryGetIndex<TKey, TValue>(collection, key, out index);
            }
    
            public static bool TryGetValue<TKey, TValue>(this IList<KeyValuePair<TKey, TValue>> collection, TKey key, out TValue value)
            {
                int index;
                if (TryGetIndex<TKey, TValue>(collection, key, out index))
                {
                    value = collection[index].Value;
                    return true;
                }
    
                value = default(TValue);
                return false;
            }
    
            private static bool TryGetIndex<TKey, TValue>(IList<KeyValuePair<TKey, TValue>> collection, TKey key, out int index)
            {
                if (null == collection || null == key)
                {
                    index = -1;
                    return false;
                }
    
                index = -1;
                for (int i = 0; i < collection.Count; i++)
                {
                    if (key.Equals(collection[i].Key))
                    {
                        index = i;
                        return true;
                    }
                }
    
                return false;
            }
        }
    
        [KnownType(typeof(QueryBase))]
        [KnownType(typeof(Relationship))]
        [KnownType(typeof(EntityCollection))]
        [DataContract(Namespace = "http://schemas.datacontract.org/2004/07/System.Collections.Generic")]
        public sealed class KeyValuePair<TKey, TValue>
        {
            #region Properties
            [DataMember(Name = "key")]
            public TKey Key { get; set; }
    
            [DataMember(Name = "value")]
            public TValue Value { get; set; }
            #endregion
        }
    
        #region Collection Instantiation
        partial class EntityCollection
        {
            public EntityCollection()
            {
                this.EntitiesField = new Entity[]{};
            }
        }
    
        partial class Label
        {
            public Label()
            {
                this.LocalizedLabelsField = new LocalizedLabelCollection();
            }
        }
    
        #endregion
    }
    
    
  2. Edit the SoapFromConsoleApp\Service References\CrmSdk\Reference.svcmap\Reference.cs file. Change each instance of "System.Collections.Generic.KeyValuePair<" to "KeyValuePair<". This will change the reference from System.Collections.Generic.KeyValuePair to the class defined in the XrmExtensionMethods.cs file.

    You should find 22 instances.

    If you do not see the Reference.cs file, in the Solution Explorer, click the Show All Files button.
Consume the Organization Services
  1. Double click Program.cs to open the file.
  2. Paste the following code to the file.
    using System;
    using SoapFromConsoleApp.CrmSdk;
    
    namespace SoapFromConsoleApp
    {
        class Program
        {
            static int MaxRecordsToReturn = 1;
    
            static void Main(string[] args)
            {
                using (var xrmServiceClient = InstantiateXrmService())
                {
                    var accountId = CreateCrmAccount(xrmServiceClient);
    
                    var account = QueryCrmAccount(xrmServiceClient, accountId);
    
                    UpdateCrmAccount(xrmServiceClient, account);
    
                    DeleteCrmAccount(xrmServiceClient, accountId);
                }
            }
    
            private static Guid CreateCrmAccount(OrganizationServiceClient xrmServiceClient)
            {
                var account = new Entity
                {
                    LogicalName = "account"
                };
    
                account["name"] = "ABC Inc.";
                account["telephone1"] = "111-222-3333";
    
                return xrmServiceClient.Create(account);
            }
    
            private static Entity QueryCrmAccount(OrganizationServiceClient xrmServiceClient, Guid accountId)
            {
                var query = new QueryExpression
                                {
                                    EntityName = "account",
                                    ColumnSet = new ColumnSet { Columns = new string[] {"name"}},
                                    Orders = new []
                                                 {
                                                     new OrderExpression() {AttributeName = "name", OrderType = OrderType.Ascending}
                                                 },
                                    Criteria = new FilterExpression()
                                                   {
                                                       Conditions = new []
                                                                        {
                                                                            new ConditionExpression
                                                                                {
                                                                                    AttributeName = "accountid",
                                                                                    Operator = ConditionOperator.Equal,
                                                                                    Values = new object[] {accountId}
                                                                                }
                                                                        }
                                                   },
                                    PageInfo = new PagingInfo {Count = MaxRecordsToReturn, PageNumber = 1, PagingCookie = null},
                                };
    
                var request = new OrganizationRequest() { RequestName = "RetrieveMultiple" };
                request["Query"] = query;
    
                OrganizationResponse response = xrmServiceClient.Execute(request);
                var results = (EntityCollection)response["EntityCollection"];
    
                return results.Entities[0];
            }
    
            private static void UpdateCrmAccount(OrganizationServiceClient xrmServiceClient, Entity account)
            {
                account["name"] = "ABC Ltd.";
                xrmServiceClient.Update(account);
            }
    
            private static void DeleteCrmAccount(OrganizationServiceClient xrmServiceClient, Guid accountId)
            {
                xrmServiceClient.Delete("account", accountId);
            }
    
            private static OrganizationServiceClient InstantiateXrmService()
            {
                var xrmServiceClient = new OrganizationServiceClient();
    
                // Uncomment the following line if you want to use an explicit CRM account to make the service calls
                // xrmServiceClient.ClientCredentials.Windows.ClientCredential = new NetworkCredential("administrator", "admin", "Contoso");
    
                return xrmServiceClient;
            }
        }
    }
  3. Compile and run the application.
In the above console application, I have shown you how to CRM account record, query it, then update it by change its name, and delete it at last.

Note that this is an old-fashioned way to make service calls to CRM server (which might be the reason that this is not in CRM SDK document), but I believe there are scenarios that you might need this approach.
Finally, if you run into any problem using the code, please let me know. I realized that I might need to look into the SetItem method a little further to make sure the serialization is done properly, since the code was taken from CRM SDK Silverlight project. If you have already spotted any problem, please kindly let me know.

I have also provided a download link of the entire project that I compiled (on SkyDrive).

Hope this helps.