步骤 3d:启用 BizTalk Server 从 Salesforce 发送和接收消息

在使用 REST 接口发送消息时,我们必须使用 Salesforce 进行身份验证。 Salesforce 支持的 REST 调用的身份验证方法在 WCF-WebHttp 适配器中不可用,我们将用它来调用 Salesforce 的 REST 接口。 因此,我们将创建自定义 WCF 终结点行为,然后将其附加到 WCF-WebHttp 发送适配器,我们将该适配器配置为调用 Salesforce REST 接口。

同样,从 Salesforce 收到的 XML 响应将不包含任何命名空间。 要使 BizTalk Server 能够针对响应架构处理响应消息,响应消息必须包含目标命名空间。 因此,我们将使用 Microsoft BizTalk ESB 工具包创建自定义管道,以便向响应消息添加命名空间。 然后,我们将使用此自定义管道作为请求-响应 WCF-WebHttp 发送端口的接收管道。

为 Salesforce 身份验证添加自定义 WCF 行为

Salesforce 为客户端应用程序提供了使用 Salesforce 进行身份验证的不同选项。 我们将使用 Salesforce 所称的 用户名密码 流。 在客户端,WCF 使你能够创建 消息检查器 来更改消息,然后再发送消息或接收消息。 消息检查器是客户端运行时的扩展,配置为行为。 一个行为进而被添加到行为扩展元素。 此行为扩展元素是在 machine.config 中以指定元素名称注册的,从而使其可以在 WCF 端口上配置为自定义行为。 有关详细信息,请参阅 使用自定义行为扩展 WCF。 我们将使用此方法合并用户名身份验证流,以便通过 Salesforce 进行身份验证。 我们将遵循的大致步骤如下:

  • 创建一个消息检查器,该检查器对 Salesforce 身份验证终结点进行 HTTP POST 调用并接收访问令牌。 然后,消息检查器将身份验证令牌添加为发送到 Salesforce 的查询消息的 Authorization 标头的值。 消息检查器还会将 Accept 标头添加到请求消息,以便收到的响应采用 XML 格式。 否则,Salesforce 会以默认 JSON 格式发送消息。

  • 创建终结点行为以将消息检查器应用到客户端终结点。

  • 创建行为扩展元素以启用终结点行为的配置。

创建消息检查器

  1. 作为 Visual Studio 中 BtsSalesforceIntegration 解决方案的一部分,创建 C# 类库。 为其 Microsoft.BizTalk.Adapter.Behaviors 命名并添加以下命名空间:

    using System.ServiceModel.Description;
    using System.ServiceModel.Dispatcher;
    using System.ServiceModel.Channels;
    using System.Xml;
    using System.Net;
    using System.IO;
    using System.ServiceModel.Configuration;
    using System.Configuration;
    using System.Web.Script.Serialization;
    
  2. 在解决方案资源管理器中,添加对 System.ServiceModelSystem.Web.ExtensionsSystem.Configuration 的引用。

  3. 添加具有以下属性的类 SalesforceOAuthToken 。 此类表示从 Salesforce 获取的授权令牌。

    public class SalesforceOAuthToken
    {
        public string id { get; set; }
        public string issued_at { get; set; }
        public string refresh_token { get; set; }
        public string instance_url { get; set; }
        public string signature { get; set; }
        public string access_token { get; set; }
    }
    
  4. 添加一个实现IClientMessageInspectorIEndpointBehavior的类SalesforceRESTSecurityBehavior。 此类包含用于对 Salesforce 身份验证终结点进行 HTTP POST 调用以检索授权令牌的HttpPost()FetchOAuthToken()方法。

    SalesforceRESTSecurityBehavior由于类实现IClientMessageInspector接口,因此它必须实现这两种方法,AfterReceiveReply()并且BeforeSendRequest()。 对于此方案,我们不需要在 AfterReceiverReply() 方法中执行任何操作。 但是,该方法 BeforeSendRequest() 调用 FetchOAuthToken() 该方法,该方法又调用该方法 HttpPost() 。 然后,该方法 BeforeSendRequest() 会将授权令牌添加为消息标头的一部分。 它还添加了 Accept 标头,以确保从 Salesforce 收到的响应采用 XML 格式。

    IEndpointBehavior 的实现方式只是将消息检查器类添加到客户端终结点行为中。

    谨慎

    此示例或指南引用敏感信息,例如连接字符串或用户名和密码。 切勿在代码中硬编码这些值,并确保使用最安全的身份验证来保护机密数据。 有关详细信息,请参阅以下文档:

    public class SalesforceRESTSecurityBehavior : IClientMessageInspector, IEndpointBehavior
    {
        // Some constants
        private static string SalesforceAuthEndpoint = "https://login.salesforce.com/services/oauth2/token";
    
        // Configuration Properties
        private string username_;
        private string password_;
        private string consumerKey_;
        private string consumerSecret_;
        private int sessionTimeout_;
    
        // Private Properties
        private SalesforceOAuthToken token_;
        private DateTime tokenExpiryTime_;
    
        public SalesforceRESTSecurityBehavior(
            string username,
            string password,
            string consumerKey,
            string consumerSecret,
            int sessionTimeout)
        {
            this.username_ = username;
            this.password_ = password;
            this.consumerKey_ = consumerKey;
            this.consumerSecret_ = consumerSecret;
            this.sessionTimeout_ = sessionTimeout;
        }
    
        private void FetchOAuthToken()
        {
            if ((tokenExpiryTime_ == null) || (tokenExpiryTime_.CompareTo(DateTime.Now) <= 0))
            {
                StringBuilder body = new StringBuilder();
                body.Append("grant_type=password&")
                    .Append("client_id=" + consumerKey_ + "&")
                    .Append("client_secret=" + consumerSecret_ + "&")
                    .Append("username=" + username_ + "&")
                    .Append("password=" + password_);
    
                string result;
    
                try
                {
                    result = HttpPost(SalesforceAuthEndpoint, body.ToString());
                }
                catch (WebException)
                {
                    // do something
                    return;
                }
    
                // Convert the JSON response into a token object
                JavaScriptSerializer ser = new JavaScriptSerializer();
                this.token_ = ser.Deserialize<SalesforceOAuthToken>(result);
                this.tokenExpiryTime_ = DateTime.Now.AddSeconds(this.sessionTimeout_);
            }
        }
    
        public string HttpPost(string URI, string Parameters)
        {
            WebRequest req = WebRequest.Create(URI);
            req.ContentType = "application/x-www-form-urlencoded";
            req.Method = "POST";
    
            // Add parameters to post
            byte[] data = Encoding.ASCII.GetBytes(Parameters);
            req.ContentLength = data.Length;
            System.IO.Stream os = req.GetRequestStream();
            os.Write(data, 0, data.Length);
            os.Close();
    
            // Do the post and get the response.
            System.Net.WebResponse resp = null;
            resp = req.GetResponse();
    
            StreamReader sr = new StreamReader(resp.GetResponseStream());
            return sr.ReadToEnd().Trim();
        }
        #region IClientMessageInspector
    
        public void AfterReceiveReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
        {
            // do nothing
        }
        public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel)
        {
            // We are going to send a request to Salesforce
            // Overview:
            // This behavior will do the following:
            // (1) Fetch Token from Salesforce, if required
            // (2) Add the token to the message
            // (3) Insert an Http Accept header so we get XML data in response, instead of JSON, which is default
            // Reference: http://www.salesforce.com/us/developer/docs/api_rest/index.htm
            //
            // (1) Authentication with Salesforce
            // (2) The token is added to the HTTP Authorization Header
            // (3) Add the Accept Header
            //
    
            HttpRequestMessageProperty httpRequest = null;
    
            if (request.Properties.ContainsKey(HttpRequestMessageProperty.Name))
            {
                httpRequest = request.Properties[HttpRequestMessageProperty.Name] as HttpRequestMessageProperty;
            }
    
            if (httpRequest == null)
            {
                // NOTE: Ideally, we shouldn’t get here for WebHttp
                httpRequest = new HttpRequestMessageProperty()
                {
                    Method = "GET",
                    SuppressEntityBody = true
                };
                request.Properties.Add(HttpRequestMessageProperty.Name, httpRequest);
            }
    
            WebHeaderCollection headers = httpRequest.Headers;
            FetchOAuthToken();
    
            headers.Add(HttpRequestHeader.Authorization, "OAuth " + this.token_.access_token);
            headers.Add(HttpRequestHeader.Accept, "application/xml");
    
            return null;
        }
    
        #endregion IClientMessageInspector
    
        #region IEndpointBehavior
    
        public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
        {
            // do nothing
        }
    
        public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
        {
            clientRuntime.MessageInspectors.Add(this);
        }
    
        public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
        {
            // do nothing
        }
    
        public void Validate(ServiceEndpoint endpoint)
        {
            // do nothing
        }
    
        #endregion IEndpointBehavior
    }
    
  5. 在上一步中,我们创建了一个行为类,该类将消息检查器应用于客户端终结点。 现在,我们需要将此行为提供给将请求消息发送到 Salesforce 的 WCF-WebHttp 适配器的配置体验。 若要使此行为可用于配置,需要执行以下两项作:

    • 创建一个派生自BehaviorExtensionElement的类

    • 在 machine.config 的 <extensions>\<behaviorExtensions> 元素中使用元素名称注册 BehaviorExtensionElement。

      我们还将配置属性添加到此类,以便它们可从 WCF-WebHttp 适配器配置 UI 获取。

    public class SalesforceRESTBehaviorElement : BehaviorExtensionElement
    {
        public override Type BehaviorType
        {
            get { return typeof(SalesforceRESTSecurityBehavior); }
        }
    
        protected override object CreateBehavior()
        {
            return new SalesforceRESTSecurityBehavior(Username, Password, ConsumerKey, ConsumerSecret, SessionTimeout);
        }
    
        [ConfigurationProperty("username", IsRequired = true)]
        public string Username
        {
            get { return (string)this["username"]; }
            set { this["username"] = value; }
        }
    
        [ConfigurationProperty("password", IsRequired = true)]
        public string Password
        {
            get { return (string)this["password"]; }
            set { this["password"] = value; }
        }
    
        [ConfigurationProperty("consumerKey", IsRequired = true)]
        public string ConsumerKey
        {
            get { return (string)this["consumerKey"]; }
            set { this["consumerKey"] = value; }
        }
    
        [ConfigurationProperty("consumerSecret", IsRequired = true)]
        public string ConsumerSecret
        {
            get { return (string)this["consumerSecret"]; }
            set { this["consumerSecret"] = value; }
        }
    
        [ConfigurationProperty("sessionTimeout", IsRequired = false, DefaultValue = 300)]
        public int SessionTimeout
        {
            get { return (int)this["sessionTimeout"]; }
            set { this["sessionTimeout"] = value; }
        }
    }
    
  6. 向项目添加强名称密钥文件并编译项目。 项目成功生成后,将程序集 Microsoft.BizTalk.Adapter.Behaviors 添加到 GAC。

  7. 在 System.ServiceModel\Extensions\BehaviorExtensions 下的 machine.config 中添加以下条目。

    <add name="Microsoft.BizTalk.Adapter.Behaviors.Demo.Salesforce" type="Microsoft.BizTalk.Adapter.Behaviors.SalesforceRESTBehaviorElement, Microsoft.BizTalk.Adapter.Behaviors, Version=1.0.0.0, Culture=neutral, PublicKeyToken=45bd7fe67ef032db"/>
    

    可以从全局程序集缓存 (GAC) 中检索到程序集的公钥标识。 此外,如果要在 64 位计算机上创建此应用程序,则必须将此条目添加到 machine.config的 32 位和 64 位版本。

添加自定义管道以将命名空间添加到 Salesforce 响应

从 Salesforce 收到的响应不包括命名空间。 但是,若要针对响应架构(QueryResult.xsd)处理响应消息,必须在 Salesforce 响应中包含命名空间。 在本部分中,我们将创建自定义管道,并使用随 Microsoft BizTalk ESB 工具包随附的自定义管道组件向响应消息添加命名空间。 此自定义管道随 BizTalk Server 应用程序一起部署,将在配置 WCF-WebHttp 适配器时用作接收管道。

在执行此过程中的步骤之前,请确保在创建此应用程序的计算机上安装和配置 Microsoft BizTalk ESB 工具包。

添加自定义管道

  1. BtsSalesforceIntegration 解决方案中,创建新的 BizTalk Server 项目。 将项目命名为 CustomPipeline

  2. CustomPipeline 项目中,添加新的接收管道。 将管道命名为 AddNamespace.btp.

  3. 在管道的 解码 阶段中,从工具箱中拖放 ESB 添加命名空间 管道组件。 在 反汇编 阶段中,拖放 XML 反汇编程序 管道组件。

    注释

    如果工具箱中未列出 ESB 添加命名空间 管道组件,则可以添加它。 右键单击 “BizTalk 管道组件 ”选项卡,然后单击“ 选择项”。 在“ 选择工具箱项 ”对话框中,单击 BizTalk 管道组件 选项卡,选中 ESB 添加命名空间 组件的复选框,然后单击“ 确定”。

  4. 将强名称密钥文件添加到项目并保存项目。

  5. 右键单击 CustomPipeline 项目并选择“ 属性”。 在 “部署 ”选项卡中,对于 应用程序名称,请输入 SalesforceIntegration

  6. 保存对项目的更改。

    在本主题中,添加了一个自定义行为,用于使用 Salesforce 进行身份验证,以及一个自定义管道,用于向 Salesforce 响应添加命名空间。 在 BizTalk Server 管理控制台中配置物理端口时,我们将使用这些自定义组件。

另请参阅

步骤 3:在 Visual Studio 中创建 BizTalk Server 解决方案