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.

14 comments:

  1. I'm new to MSCRM and just starting to learn. Using the sample codes above, if I apply this to lead and will execute DeleteCrmLead, will there be any issues if there are other entity associated to the deleted lead or let say activities or opportunity. What I'm asking, will it create some orphaned records?

    ReplyDelete
  2. The approach I am showing here is through CRM SOAP endpoint, which is fully supported. Whether CRM will create orphaned records depends on how the relationship was defined. In your case, I think you shouldn't need to worry.

    Cheers,
    Daniel

    ReplyDelete
  3. Nice tutorial. I tried your sample project and change/update the 'CrmSdk' pointing to our CRM however getting the error below when trying to build the project. Would appreciate if you can guide us. Thank you.

    Warning 1
    Custom tool warning: The following Policy Assertions were not Imported:
    XPath://wsdl:definitions[@targetNamespace='http://schemas.microsoft.com/xrm/2011/Contracts/Services']/wsdl:binding[@name='CustomBinding_IOrganizationService']

    .......

    ReplyDelete
  4. @Anonymous, not sure why that would happen to you. In fact, you don't necessarily need to update the service reference, since CRM 2011 service endpoint doesn't contain any customization-specific information like CRM 4 does. You can simply edit app.config file to point to your CRM server in order to run the code.

    Hope this helps.
    Daniel

    ReplyDelete
  5. Thanks. I will try that.

    ReplyDelete
  6. Hi
    Nice blog and very informative.Good content.Valuable information.thanks for sharing.
    Ecommerce developer

    ReplyDelete
  7. I have been having problem connecting to the HOSTED CRM online 2011 with ADFS as authentication.
    I have download and run your app but I got this error with FaultException was unhandled.
    The error message happen when it try to execute "return xrmServiceClient.Create(account);"
    and the message said "An error occurred when verifying security for the mesage."
    I did some research, it is suggested that this is because of the time differences between the HOSTED CRM time and the client time.
    Could this also be the fact that I am using ADFS as an authentication method?
    However I was able to connect and retrieve data from HOSTED CRM when i used the SDK provided by Microsoft.

    ReplyDelete
    Replies
    1. @nobee123, if you are using SDK, your problem is not really relevant to this blog post. If that's the case, you may consider using CrmConnection class from Microsoft.Xrm.Client.dll assembly which simplifies instantiating CRM organization service proxy.

      Delete
    2. @nobee123, please let me know if I misunderstood you in my previous response.

      Delete
    3. @Daniel Cai I am not using the SDK, i was saying i can connect and retrieve the data from the CRM but my goal is to use only the web service to retrieve data from CRM
      Sorry for the misunderstanding

      Delete
    4. If that's the case, you may want to have a look of sdk's sample project called wsdlbasedproxies under sdk\samplecode\cs\wsdlbasedproxies folder, which includes working code for IFD authentication.

      Delete
  8. Hi Daniel -

    I am trying to use your code, but I am getting error message

    System.NotImplementedException: the method or operation is not implemented.

    Please help me.

    Thank you.

    Hiren

    ReplyDelete
    Replies
    1. Hi Hiren, do you know which line of code is throwing this error?

      Delete
    2. Hi Daniel -

      Thank you for your replay

      This account["name"] = "ABC Inc."; line throwing error. I have CRM 2011 and rollup 13.


      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);
      }

      Delete