2 Comments

(Puedes ver este artículo en español aquí)

In a previous post Salesforce Workflow Outbound Messages Handled in .Net with WCF I explained how to create a .Net WCF service to handle workflow outbound messages from Salesforce. In this article I will add a twist to it by making the web service call back to Salesforce to retrieve additional data.

The Requirements

Allow me to explain the concepts by using a concrete example: one of our client required to integrate their Salesforce org with their ERP. The integration is based on the following rule:

  • Accounts (creations and modifications) are maintained in Salesforce
  • Accounts will be created in the ERP only when an opportunity associated with the account is won

The above rules translate to the following

  • We can create a workflow on opportunity that triggers only when the stage of the opportunity is “Closed Won”
  • The workflow will have an outbound message that calls a web service in the ERP
  • Since the outbound message can only send fields of the object it is based on, we cannot send account fields using an outbound message for the opportunity. We can send the account Id (a field in the opportunity) and then have the web service in the ERP to connect back to Salesforce and retrieve the account fields using the account Id.

Let’s dive into code!

The Workflow Outbound Message

Let’s start with the workflow definition. I won’t provide too many details on how to create the workflow since I already talked about this in the article Salesforce Workflow Outbound Messages Handled in .Net with WCF (please read this first if you need more details).

In Salesforce go to “Worflow & Approvals” and create a new workflow rule based on the Opportunity object. Give it a name (I called CreateAccountOnERP) and make sure that the evaluation criteria is set to “created, and any time it’s edited to subsequently meet criteria”. We want this option since we only want to call the web service if the opportunity stage is “Closed Won”. Now specify the rule criteria and make sure that both “Opportunity : Closed” and “Opportunity : Won” fields are true. Your workflow rule should  look like this:

Opportunity workflow rule

Now, add an outbound message action to the workflow. When creating the outbound message, make sure you mark “Send Session Id” (I will explain this later), and also to include the AccountId field as the fields to send. We still don’t have the web service url so put anything in the “Endpoint URL” field:

Opportunity outbound message

Web Service in Visual Studio (using WCF)

Now, get the WSDL for the outbound message and save it locally on disk (I named the file opportunityWorkflowOutboundMessage.wsdl), we will use this later to create our service definition inside Visual Studio.

In Visual Studio, create a new empty ASP.Net Web Application and name it WorkflowNotificationServices (the following steps are basically the same explained in the article Salesforce Workflow Outbound Messages Handled in .Net with WCF). Add the WSDL to the project. Add a new “WCF Service” to the project and name it OpportunityNotificationService.svc. Visual Studio will add three files to your project: IOpportunityNotificationService.cs, OpportunityNotificationService.svc and the implementation OpportunityNotificationService.svc.cs.

Now open a command prompt (if you have the Productivity Power Tools extension you can right click on the project in Solution Explorer and choose “Power Commands->Open Command Prompt”) and type the following:

svcutil /noconfig /out:IOpportunityNotificationService.cs opportunityWorkflowOutboundMessage.wsdl

Open the IOpportunityNotificationService.cs file and make sure you make these changes:

  • Put the whole class inside the namespace of the project (in my case is the name of the project WorkflowNotificationServices)
  • Change the name of the interface from NotificationPort to IOpportunityNotificationService.
  • Change the ConfigurationName parameter of the ServiceContractAttribute attribute from NotificationPort to OpportunityNotificationService.
  • Remove the parameter ReplyAction=”*” from the OperationContractAttribute attribute.
  • At the end of the file, remove the interface NotificationPortChannel and the class NotificationPortClient (we don’t need these since they are used by a client consuming the web service).

The interface should now look as the following (highlighted lines are the one that changed):

namespace WorkflowNotificationServices
{
    [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
    [System.ServiceModel.ServiceContractAttribute(Namespace = "http://soap.sforce.com/2005/09/outbound", ConfigurationName = "OpportunityNotificationService")]
    public interface IOpportunityNotificationService
    {
        [System.ServiceModel.OperationContractAttribute(Action = "")]
        [System.ServiceModel.XmlSerializerFormatAttribute()]
        [System.ServiceModel.ServiceKnownTypeAttribute(typeof(sObject))]
        notificationsResponse1 notifications(notificationsRequest request);
    }

    /// rest of the code below
}

Next, open the file OpportunityNotificationService.svc.cs, remove the DoWork method and implement the IOpportunityNotificationService interface (place the cursor on text for the name of the interface and press Crtl+. and choose “Implement interface IOpportunityNotificationService”).

And finally, edit the web.config and replace it with the following:

<configuration>
    <connectionStrings>
        <add name="ERP" connectionString="Data Source=localhost;Initial Catalog=Hiperantena;Integrated Security=True" providerName="System.Data.SqlClient"/>
    </connectionStrings>
    <system.web>
        <compilation debug="true" targetFramework="4.5" />
        <httpRuntime targetFramework="4.5" />
        <webServices>
            <protocols>
                <clear/>
                <add name="HttpSoap" />
                <add name="Documentation"/>
            </protocols>
        </webServices>
    </system.web>
    <system.serviceModel>
        <services>
            <service name="WorkflowNotificationServices.OpportunityNotificationService">
                <endpoint binding="basicHttpBinding" contract="OpportunityNotificationService"/>
            </service>
        </services>
        <behaviors>
            <serviceBehaviors>
                <behavior name="">
                    <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" />
                    <serviceDebug includeExceptionDetailInFaults="false" />
                </behavior>
            </serviceBehaviors>
        </behaviors>
        <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
    </system.serviceModel>
</configuration>

The project should compile at this point and if we run it we should be able to get the WSDL for this web service.

The Call Back to Salesforce

The outbound message we defined earlier for the opportunity will send two important pieces of information: the id of the account, and the session id. We will use these to connect to Salesforce and get the data for the account that is required in the ERP. For this we will use the Salesforce SOAP API.

Go back to Salesforce, and go to “Setup->Build->Develop->API”, you will get the API WSDL screen:

Salesforce API

We need to get the Enterprise WSDL (for a difference between Enterprise and Partner WSDL see this article). Save the WSDL on disk (I named it enterprise.wsdl) and add it to your Visual Studio project. We will create a proxy for this WSDL using Visual Studio.

In the Solution Explorer, right click the References node and choose “Add Service Reference”. Specify the full path to the enterprise WSDL and click Go. Make sure you specify Salesforce as the namespace:

Add service reference

Click OK. Visual Studio will create the proxy to call the Salesforce SOAP API and make the required modifications in web.config. Open the OpportunityNotificationService.cs file and replace the code with the following:

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using WorkflowNotificationServices.Salesforce;

namespace WorkflowNotificationServices
{
    public class OpportunityNotificationService : IOpportunityNotificationService
    {

        public notificationsResponse1 notifications(notificationsRequest request)
        {
            bool result = true;

            notifications notifications1 = request.notifications;

            string sessionId = notifications1.SessionId;
            string url = notifications1.EnterpriseUrl;

            OpportunityNotification[] opportunityNotifications = notifications1.Notification;
            foreach (OpportunityNotification opportunityNotification in opportunityNotifications)
            {
                WorkflowNotificationServices.Opportunity opportunity = (WorkflowNotificationServices.Opportunity)opportunityNotification.sObject;

                try
                {
                    if (!CreateAccount(url, sessionId, opportunity))
                        result = false;
                }
                catch (Exception e)
                {
                    Trace.TraceError(e.Message);
                    result = false;
                }
            }

            notificationsResponse response = new notificationsResponse();
            response.Ack = result;

            return new notificationsResponse1() { notificationsResponse = response };
        }

        private bool CreateAccount(string url, string sessionId, WorkflowNotificationServices.Opportunity opportunity)
        {
            int recordsAffected = 0;

            EndpointAddress address = new EndpointAddress(url);
            SoapClient soapClient = new SoapClient("Soap", address);
            SessionHeader session = new SessionHeader() { sessionId = sessionId };

            string query = String.Format("select Id, Name, AccountNumber, BillingStreet, BillingCity, BillingState, BillingPostalCode, BillingCountry from account where id = '{0}'", opportunity.AccountId);

            QueryResult result;
            soapClient.query(session, null, null, null, query, out result);

            if (result.size > 0)
            {
                Account account = result.records[0] as Account;

                ConnectionStringSettings connectionString = ConfigurationManager.ConnectionStrings["ERP"];
                using (SqlConnection cn = new SqlConnection(connectionString.ConnectionString))
                {
                    using (SqlCommand command = new SqlCommand("salesforce_createAccount", cn))
                    {
                        command.CommandType = CommandType.StoredProcedure;

                        command.Parameters.Add("@idSalesforce", SqlDbType.VarChar).Value = account.Id;
                        command.Parameters.Add("@name", SqlDbType.VarChar).Value = account.Name;
                        command.Parameters.Add("@number", SqlDbType.VarChar).Value = (object)account.AccountNumber ?? DBNull.Value;
                        command.Parameters.Add("@address", SqlDbType.VarChar).Value = (object)account.BillingStreet ?? DBNull.Value;
                        command.Parameters.Add("@city", SqlDbType.VarChar).Value = (object)account.BillingCity ?? DBNull.Value;
                        command.Parameters.Add("@state", SqlDbType.VarChar).Value = (object)account.BillingState ?? DBNull.Value;
                        command.Parameters.Add("@postalCode", SqlDbType.VarChar).Value = (object)account.BillingPostalCode ?? DBNull.Value;
                        command.Parameters.Add("@country", SqlDbType.VarChar).Value = (object)account.BillingCountry ?? DBNull.Value;

                        cn.Open();
                        recordsAffected = command.ExecuteNonQuery();
                    }
                }

            }

            return recordsAffected > 0;
        }
    }
}

There are a few important things here to notice: In lines 24 and 25 we get the session id and the url that comes from the outbound message from Salesforce. These two parameters are needed to connect back to Salesforce. Remember that when we created the outbound message in Salesforce we marked the “Send Session ID” field. We use this information in lines 54-56 to create a SessionHeader object. In line 58 we build a SOQL query to get the information from account and we use the AccountId field sent in the outbound message. We then use the session header in line 61 to send the query to Salesforce. The rest of the code just process the information we got back from Salesforce and we send this to the ERP (in this example I’m calling a SQLServer stored procedure to simulate the ERP).

Testing the Call Back

We need to publish our web service and make it available on the internet. The publishing is outside of the scope of this article. In my case I published it on an IIS server in our DMZ, and the public url is http://www.grupolanka.com/Salesforce/WorkflowNotificationServices/OpportunityNotificationService.svc (don’t try it, it won’t work).

Now we need to go back to Salesforce and change the url of the outbound message we created earlier. Edit the outbound message definition (in the example is SendOportunityToERP) and edit the “Endpoint URL” field with the URL.

Now, create a new account and create an opportunity. Change the stage of the opportunity to “Closed Won” and save it. Salesforce will trigger the workflow and call the web service we defined, and this will call back to Salesforce to retrieve the information from the account.

You can get the sample project here:

Comments

Comment by Lucky Guy

Thanks for this so much for this post. I spent the last few hours trying to figure out what the hell was going on.

Lucky Guy