Llamada a un Servicio Web WCF de .Net desde Salesforce

En internet se puede conseguir información de cómo llamar un servicio web desde Salesforce, pero hay poca información de cómo llamar un servicio web creado en .Net. Los ejemplos que se consiguen están basados mayoritariamente en servicios web asmx. En esta publicación explicaré como llamar a servicios web WCF de .Net creados con Visual Studio 2013 Community.

Para hacer el ejercicio más completo realizaré la llamada a un servicio web desde un desencadenador (trigger) y explicaré cómo debe configurarse Salesforce para hacerlo posible.

El Servicio WCF

Comencemos creando el servicio web. Simularemos el siguiente escenario: Al crear un presupuesto en Salesforce y agregarle productos queremos que el precio del producto sea asignado dinámicamente por el ERP y no por Salesforce (Salesforce usa Libros de Precio para esto, pero no queremos usarlos en este ejemplo).

Inicia Visual Studio y crea un nuevo proyecto del tipo “empty ASP.Net Web Application” y llámalo ErpService. Agrégale un servicio WCF y llámalo ProductService.svc. Visual Studio agregará los assemblies requeridos al proyecto y creará tres archivos: IProductService.cs, ProductService.svc y ProductService.svc.cs.

Modifica el archivo web.config para agregar la definición del servicio web:

<?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>

Abre IProductService.cs y reemplaza el código con lo siguiente:

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

Nuestro servicio web expone sólo un método para obtener el precio del producto dado su identificador. Abre ProductService.svc.cs y reemplazalo con el siguiente código:

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

La implementación de nuestro servicio web es bastante sencilla: usaremos ADO.Net para conectarnos al ERP (una base de datos de SQLServer) y llamar a un procedimiento almacenado pasándole el identificador de producto.

Nuestro servicio web está listo. Tenemos que publicarlo en internet para que Salesforce lo pueda ver. La publicación no es parte del alcance de este artículo. En mi caso he publicado el servicio en un servidor IIS en la DMZ y el URL público es: http://yju.gec.mybluehost.me/Salesforce/ErpService/ProductService.svc (no intentes probarlo, no funcionará).

Necesitamos el WSDL del servicio. Navega al servicio web y obtén el WSDL del enlace que dice singleWsdl:

salesforce crm wsdl

Y he guardado el WSDL en un archivo en mi disco local con el nombre productService.wsdl.

Ahora que tenemos nuestro servicio web vayamos a Salesforce para crear una clase que haga la llamada.

Agregar el Servicio Web a Salesforce

En Salesforce dirígete a “Configuración->Compilación->Desarrollo->Clases de Apex”, and y pincha en el botón “Generar desde WSDL”. Salesforce preguntará por el archivo WSDL. Pincha en el botón “Choose File” y selecciona el archive productService.wsdl y luego pincha en el botón “Analizar WSDL”. Obtendrás el siguiente error:

salesforce crm apex

Aquí viene la parte con truco, necesitamos modificar nuestro WSDL para que Salesforce pueda analizarlo sin errores. Usando un editor de XML (yo uso Notepad++ con el plugin XML tools plugin), localiza y elimina el siguiente trozo de 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>

Esto parece confundir al analizador de Salesforce por lo que lo eliminaremos (no te preocupes, funcionará sin esto). Guarda el WSDL e intenta analizarlo con Salesforce de nuevo. Ahora deberías obtener algo como esto:

salesforce crm analizador

Especifica un nombre válido para la clase de Apex (yo usé ProductService). Pincha en el botón de “Generar código Apex”. Salesforce debería crear la clase de Apex sin errores. Ahora vamos a probarla antes de continuar. Abre la  Consola de desarrollador and presiona Crtl+E para abrir la ventana de “Execute Anonymous”. Introduce el siguiente código:

ProductService.Product productService = new ProductService.Product();
Decimal price = productService.GetPrice('PADAP20001');
system.debug(price);

Estamos haciendo una llamada al servicio web y si abrimos el log y buscamos por mensajes de debug deberíamos ver el precio retornado por el ERP.

Creación de un Desencadenador con Llamada a un Servicio Web

Ahora que sabemos que Salesforce puede llamar nuestro servicio web, tener un desencadenador (trigger) que lo llame no es algo obvio. Aunque se puede conseguir información al respecto en la web, voy a proceder a explicarlo aquí por completitud.

Un desencadenador no puede llamar a un servicio web directamente. Esta es la forma que tiene Salesforce de garantizar que un desencadenador no se quede pegado en una llamada externa (de la cual no son responsables) y así comprometer la ejecución del desencadenador. Para evitar esto un desencadenador puede llamar a un servicio web de forma asíncrona. Para que un desencadenador llame a un servicio web es necesario crear una clase con un método estático marcado con el atributo especial @future. En la Consola de desarrollador de Salesforce, crea una nueva clase de Apex y llámala QuoteLineItemProcesses. Introduce el siguiente código:

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

Nótese la línea 2 donde se especifica el atributo @future para el método y especificamos que el método realizará una llamada externa. El método necesita ser declarado como static void. Las líneas 12 y 13 son las que realizan la llamada externa (tal y como hicimos cuando estábamos probando el servicio). El método toma una lista de Ids como parámetros, correspondiéndose con los Ids que son procesados en el desencadenador. Para cada Id obtenemos la línea del presupuesto y el código de producto y luego realizamos la llamada externa.

Para el desencadenador, crea uno nuevo asociado a QuoteLineItem y llámalo 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);
    }
}

El desencadenador necesita ser del tipo after insert debido a que necesitamos los Ids de los registros para poder hacer una actualización asíncrona. Nótese el uso de las mejores prácticas para procesar registros de forma batch. Creamos un arreglo de Ids y lo pasamos  a la clase que creamos anteriormente.

¡Y ya está, listos para probarlo!: crea una oportunidad y agréga un presupuesto (debes tener presupuestos habilitados en tu organización), luego agrega una línea al presupuesto. Después de agregar la línea, refresca el presupuesto (recuerda que la actualización es asíncrona) y deberías poder ver el precio asignado por el ERP.

“Estos artículos están publicados en inglés en http://www.giovannimodica.com/

Grupo Lanka es partner certificado para Salesforce España.