Call a .Net WCF Service from Salesforce
- Posted in:
- .net
- integration
- salesforce
(Puedes ver este artículo en español aquí)
In the web you can find information on how to call a web service from Salesforce, but there is little information on how to call web service created in .Net. The examples you find are based on asmx web services mostly. In this article I will explain how to call a .Net WCF web service created with Visual Studio 2013 Community.
To make things more exciting I will call this web service from a trigger and explain how to code things in Salesforce to make this possible.
The WCF Service
Let’s start by creating the web service. We will simulate the following scenario: when we create a quote in Salesforce and add products to it we want the product price to be dynamically assigned by the ERP and not by Salesforce (Salesforce manages Price Books for this, but we don’t want this in this example).
Start Visual Studio and create a new empty ASP.Net Web Application and call it ErpService. Add a WCF Service to it and call it ProductService.svc. Visual Studio will add the required assemblies to the project and create three files: IProductService.cs, ProductService.svc and ProductService.svc.cs.
Modify your web.config file to add the service definition:
<?xml version="1.0" encoding="utf-8"?> <configuration> <connectionStrings> <add name="ERP" connectionString="Data Source=localhost;Initial Catalog=ERP;Integrated Security=True" providerName="System.Data.SqlClient"/> </connectionStrings> <system.web> <webServices> <protocols> <clear/> <add name="HttpSoap" /> <add name="Documentation"/> </protocols> </webServices> <compilation debug="true" targetFramework="4.5" /> <httpRuntime targetFramework="4.5" /> </system.web> <system.serviceModel> <services> <service name="ErpService.ProductService"> <endpoint binding="basicHttpBinding" name="Product" contract="ErpService.IProductService"/> </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>
Open IProductService.cs and replace it with the following code:
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using System.ServiceModel; using System.Text; namespace ErpService { [ServiceContract] public interface IProductService { [OperationContract] decimal GetPriceForCustomer(string productId); } }
Our web service exposes just one method to get the price of a product given its Id. Open ProductService.svc.cs and replace it with the following code:
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; namespace ErpService { public class ProductService : IProductService { public decimal GetPriceForCustomer(string productId) { try { ConnectionStringSettings connectionString = ConfigurationManager.ConnectionStrings["ERP"]; using (SqlConnection cn = new SqlConnection(connectionString.ConnectionString)) { using (SqlCommand command = new SqlCommand("salesforce_getProductPrice", cn)) { command.CommandType = CommandType.StoredProcedure; command.Parameters.Add("@productId", SqlDbType.VarChar).Value = (object)productId ?? DBNull.Value; SqlParameter priceParameter = command.Parameters.Add("@price", SqlDbType.Money); priceParameter.Direction = ParameterDirection.Output; cn.Open(); command.ExecuteNonQuery(); return (decimal)command.Parameters["@price"].Value; } } } catch (Exception e) { Trace.WriteLine(e.Message); return 0; } } } }
The implementation of our web service is pretty straightforward: it uses ADO.Net to connect to our ERP (a SQLServer database) and call a stored procedure passing the id of the product.
Our web service is ready. You need to publish this web service on the internet for Salesforce to see. The publication is outside the scope of this article. In my case I published it on an IIS server in our DMZ and the URL to reach it is: http://www.grupolanka.com/Salesforce/ErpService/ProductService.svc (don’t try it, it won’t work).
We need the WSDL for the web service. Go to the web service and click on the link for the singleWsdl:
I saved the WSDL locally with the name of productService.wsdl.
Now that we have our web service, let’s go back to Salesforce to create a class to call it.
Adding the Web Service in Salesforce
In Salesforce go to “Setup->Build->Develop->Apex Classes”, and click on the button “Generate from WSDL”. Salesforce will ask you for the WSDL file. Click on the “Choose File” button and select the productService.wsdl file and then click on “Parse WSDL”. You will get the following error:
Here comes the tricky part, we need to modify our WSDL for Salesforce to parse it without errors. Using an XML editor (I use Notepad++ with the XML tools plugin), locate and remove the following chunck of XML:
<xs:schema attributeFormDefault="qualified" elementFormDefault="qualified" targetNamespace="http://schemas.microsoft.com/2003/10/Serialization/" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://schemas.microsoft.com/2003/10/Serialization/"> <xs:element name="anyType" nillable="true" type="xs:anyType"/> <xs:element name="anyURI" nillable="true" type="xs:anyURI"/> <xs:element name="base64Binary" nillable="true" type="xs:base64Binary"/> <xs:element name="boolean" nillable="true" type="xs:boolean"/> <xs:element name="byte" nillable="true" type="xs:byte"/> <xs:element name="dateTime" nillable="true" type="xs:dateTime"/> <xs:element name="decimal" nillable="true" type="xs:decimal"/> <xs:element name="double" nillable="true" type="xs:double"/> <xs:element name="float" nillable="true" type="xs:float"/> <xs:element name="int" nillable="true" type="xs:int"/> <xs:element name="long" nillable="true" type="xs:long"/> <xs:element name="QName" nillable="true" type="xs:QName"/> <xs:element name="short" nillable="true" type="xs:short"/> <xs:element name="string" nillable="true" type="xs:string"/> <xs:element name="unsignedByte" nillable="true" type="xs:unsignedByte"/> <xs:element name="unsignedInt" nillable="true" type="xs:unsignedInt"/> <xs:element name="unsignedLong" nillable="true" type="xs:unsignedLong"/> <xs:element name="unsignedShort" nillable="true" type="xs:unsignedShort"/> <xs:element name="char" nillable="true" type="tns:char"/> <xs:simpleType name="char"> <xs:restriction base="xs:int"/> </xs:simpleType> <xs:element name="duration" nillable="true" type="tns:duration"/> <xs:simpleType name="duration"> <xs:restriction base="xs:duration"> <xs:pattern value="\-?P(\d*D)?(T(\d*H)?(\d*M)?(\d*(\.\d*)?S)?)?"/> <xs:minInclusive value="-P10675199DT2H48M5.4775808S"/> <xs:maxInclusive value="P10675199DT2H48M5.4775807S"/> </xs:restriction> </xs:simpleType> <xs:element name="guid" nillable="true" type="tns:guid"/> <xs:simpleType name="guid"> <xs:restriction base="xs:string"> <xs:pattern value="[\da-fA-F]{8}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{12}"/> </xs:restriction> </xs:simpleType> <xs:attribute name="FactoryType" type="xs:QName"/> <xs:attribute name="Id" type="xs:ID"/> <xs:attribute name="Ref" type="xs:IDREF"/> </xs:schema>
This seems to confuse the Salesforce parser so we will remove it (don’t worry, it will work even without this). Save the WSDL and try to parse it wit Salesforce again. Now you should get something like this:
Specify a valid name for the Apex Class (I used ProductService as the class name). Click on “Generate Apex code” button. Salesforce should create the apex class with no errors. Now let’s test it before we continue. Open the Developer Console and press Crtl+E to open the Execute Anonymous window. Enter the following code:
ProductService.Product productService = new ProductService.Product(); Decimal price = productService.GetPrice('PADAP20001'); system.debug(price);
We’re calling our web service and if you open the log and watch for debug messages you should see the price we got back from the ERP.
Creating the Trigger with a Web Service Callback
Now we now that Salesforce can call our web service. Having a trigger to call it might not be obvious. Although you can find information on the web on how to do this, I will explain it here for completeness.
A trigger cannot directly call a web service. This is a way for Salesforce to guarantee that a trigger will not get stuck in some external call (for which they’re not responsible) and thus compromise the execution of the trigger. To avoid this a trigger can call a web service in an asynchronous way. For a trigger to call a web service you need to create a class with a static method marked with the special @future tag. Using the Developer Console in Salesforce, create a new Apex Class and call it QuoteLineItemProcesses. Enter the following code:
global with sharing class QuoteLineItemProcesses { @future (callout = true) public static void updateLinePrice(List<Id> lineItemIds) { Boolean changed = false; QuoteLineItem[] lineItems = [select QuoteId,Product2Id,UnitPrice from QuoteLineItem where Id in :lineItemIds]; for(QuoteLineItem lineItem : lineItems) { Product2 product = [select ProductCode from Product2 where Id = :lineItem.Product2Id]; String productCode = product.ProductCode; ProductService.Product productService = new ProductService.Product(); Decimal price = productService.GetPrice(productCode); if(price > 0) { lineItem.UnitPrice = price; changed = true; } } if(changed) update lineItems; } }
Notice line 2 where we specify the @future tag for the method and we tell that the method will do a callout. The method needs to be a static void method. Lines 12 and 13 are doing the callout (just as we did when testing the web service). The method takes a list of Ids as a parameter, corresponding to the Ids that are being processed in the trigger. For each Id we get the line item and the product code for the line and then do the callout.
For the trigger, create a new trigger associated to QuoteLineItem and call it OnQuoteLineItemAdded:
trigger OnQuoteLineItemAdded on QuoteLineItem (after insert) { List<Id> quoteLineItemIds = new List<Id>(); for (QuoteLineItem quoteLineItem: Trigger.new) { quoteLineItemIds.add(quoteLineItem.Id); } if (quoteLineItemIds.size() > 0) { QuoteLineItemProcesses.updateLinePrice(quoteLineItemIds); } }
The trigger needs to be an after insert trigger since we need the Id of the record in order to do the asynchronous update after the callout. Notice how we use the best practices to process batch records. We create an array of Ids and pass it to the class we created above.
And this is it! we are now ready to test it: create an opportunity and add a quote to it, then add a line item to the quote. After adding the line, refresh the quote (remember that it is asynchronous) you should now see the price from the ERP.
Here you can find the project I used to create the web service: