跳到主要内容
版本:3.0.0

WebService调用

这页只讨论 Broker 的 WebService 调用方式,也就是 .svc.asmx 这类 SOAP 服务的接入。如果你当前面对的是医院存量系统、遗留平台接口,或者对方提供的是 ServiceContract 风格契约,这页就是直接可用的接入手册。

什么时候该用这一页

适合场景:

  • 对接 WCF .svc 服务
  • 对接 ASMX .asmx 服务
  • 服务契约包含 ServiceContractOperationContract
  • 接口文档描述的是 SOAP 1.1 / SOAP 1.2,而不是 REST

如果你要对接的是普通 Web API,请直接看 HTTP请求组件

先分清两类服务

Broker 当前在 WebService 场景里,最常见的是两种服务:

类型常见地址特点
WCF*.svc常见于较新的 SOAP 服务,契约通常偏 DataContract 风格
ASMX*.asmx更早期的 WebService,通常更依赖 XML 名称和 SOAP Action

两者都通过 BrokerClient.Get<T>(...) 调用,但契约和模型写法会有一些差异。

最小可运行路径

第一步:准备服务地址

{
"RemoteServices": {
"SoapDemo": "http://localhost:5050/Service.svc",
"LegacyWebservice": "http://localhost:58471/WebService1.asmx"
}
}

第二步:定义带 ServiceContract 的契约

using System.ServiceModel;

[ServiceContract]
public interface ISampleService
{
[OperationContract]
string Ping(string message);
}

第三步:获取代理并调用

var service = BrokerClient.Get<ISampleService>(
ConfigManager.Get("RemoteServices:SoapDemo"));

var result = service.Ping("Hello, SOAP Service!");

WCF 契约到底要怎么建

WCF 场景里,调用能不能成功,往往不在“有没有拿到地址”,而在契约定义是否和服务端完全对齐。最核心的四个点是:

  • 契约接口要有 [ServiceContract]
  • 可调用的方法要有 [OperationContract]
  • 复杂模型尽量显式写 [DataContract] / [DataMember]
  • 命名空间、方法名、参数名不能随意改

WCF .svc 怎么写

简单返回

using System.ServiceModel;

[ServiceContract]
public interface ISampleSoapService
{
[OperationContract]
string Ping(string s);

[OperationContract]
int[] IntArray();
}
var service = BrokerClient.Get<ISampleSoapService>(
ConfigManager.Get("RemoteServices:SoapDemo"));

var message = service.Ping("hello");
var numbers = service.IntArray();

复杂入参与复杂返回

using System.Runtime.Serialization;
using System.ServiceModel;

[DataContract(Namespace = "http://schemas.datacontract.org/2004/07/Models")]
public class ComplexModelInput
{
[DataMember]
public string StringProperty { get; set; }

[DataMember]
public int IntProperty { get; set; }

[DataMember]
public List<string> ListProperty { get; set; }

[DataMember]
public DateTimeOffset DateTimeOffsetProperty { get; set; }

[DataMember]
public List<ComplexObject> ComplexListProperty { get; set; }
}

[DataContract]
public class ComplexObject
{
[DataMember]
public string StringProperty { get; set; }

[DataMember]
public int IntProperty { get; set; }
}

[DataContract(Namespace = "http://schemas.datacontract.org/2004/07/Models")]
public class ComplexModelResponse
{
[DataMember]
public float FloatProperty { get; set; }

[DataMember]
public string StringProperty { get; set; }

[DataMember]
public List<string> ListProperty { get; set; }
}
[ServiceContract]
public interface ISampleSoapService
{
[OperationContract]
ComplexModelResponse PingComplexModel(ComplexModelInput inputModel);
}
var service = BrokerClient.Get<ISampleSoapService>(
ConfigManager.Get("RemoteServices:SoapDemo"));

var response = service.PingComplexModel(new ComplexModelInput
{
StringProperty = "TestString",
IntProperty = 42,
ListProperty = new List<string> { "A", "B" },
DateTimeOffsetProperty = DateTimeOffset.UtcNow,
ComplexListProperty = new List<ComplexObject>
{
new ComplexObject { StringProperty = "Complex1", IntProperty = 100 }
}
});

数组、复杂对象数组和可空值

[ServiceContract]
public interface ISampleSoapService
{
[OperationContract]
int[] IntArray();

[OperationContract]
ComplexReturnModel[] ComplexReturnModel();

[OperationContract]
int? NullableMethod(bool? arg);
}

这类接口适合用来验证:

  • 基础类型数组能否正常反序列化
  • 复杂对象列表是否和服务端模型一致
  • 可空值是否与服务端约定一致

out 参数

有些旧服务不会把返回值放到方法返回类型,而是通过 out 参数回传内容,这时契约必须保持同样签名。

[ServiceContract]
public interface ISampleSoapService
{
[OperationContract]
void VoidMethod(out string s);
}
service.VoidMethod(out var message);

Task<T> 异步返回

[ServiceContract]
public interface ISampleSoapService
{
[OperationContract]
Task<int> AsyncMethod();
}

var number = await service.AsyncMethod();

如果服务端契约本身支持这类调用方式,可以直接按异步方式消费;否则优先以服务端实际契约为准,不要自己把同步方法改成异步签名。

传 XML 节点

有些 SOAP 服务会把一整个 XML 片段作为入参,这时可以直接用 XElement

using System.Xml.Linq;

[ServiceContract]
public interface ISampleSoapService
{
[OperationContract]
void XmlMethod(XElement xml);
}

var xml = XElement.Parse("""
<Request>
<PatientId>P001</PatientId>
<VisitNo>V1001</VisitNo>
</Request>
""");

service.XmlMethod(xml);

ASMX .asmx 怎么写

ASMX 一般更依赖明确的命名空间、XmlSerializerFormatMessageParameter 映射。

简单方法

using Aegis.Net.Broker;
using System.ServiceModel;

[SoapContract(SoapVersion = "soap12")]
[ServiceContract]
[XmlSerializerFormat]
public interface IWebserviceContract
{
[OperationContract(Action = "http://tempuri.org/HelloWorld")]
string HelloWorld();

[OperationContract(Action = "http://tempuri.org/GetMax")]
int GetMax(int num1, int num2);
}
var service = BrokerClient.Get<IWebserviceContract>(
ConfigManager.Get("RemoteServices:LegacyWebservice"));

var hello = service.HelloWorld();
var max = service.GetMax(12, 11);

复杂对象

using System.Runtime.Serialization;

[DataContract(Namespace = "http://tempuri.org/")]
public class InnerClass
{
[DataMember]
public string InnerValue { get; set; }
}

[DataContract(Namespace = "http://tempuri.org/")]
public class ComplexInput
{
[DataMember]
public int IntValue { get; set; }

[DataMember]
public string StringValue { get; set; }

[DataMember]
public DateTime DateTimeValue { get; set; }

[DataMember]
public InnerClass InnerObject { get; set; }
}

[DataContract(Namespace = "http://tempuri.org/")]
public class ComplexOutput
{
[DataMember]
public double DoubleValue { get; set; }

[DataMember]
public bool BooleanValue { get; set; }

[DataMember]
public string ResultMessage { get; set; }

[DataMember]
public InnerClass InnerObject { get; set; }
}
[SoapContract(SoapVersion = "soap12")]
[ServiceContract]
[XmlSerializerFormat]
public interface IWebserviceContract
{
[OperationContract(Action = "http://tempuri.org/ProcessComplexType")]
[return: MessageParameter(Name = "ComplexOutput")]
ComplexOutput ProcessComplexType(
[MessageParameter(Name = "ComplexInput")] ComplexInput input);
}
var result = service.ProcessComplexType(new ComplexInput
{
IntValue = 42,
StringValue = "Test String from Client",
DateTimeValue = DateTime.Now,
InnerObject = new InnerClass
{
InnerValue = "Inner Value from Client"
}
});

MessageContract 风格的老 WebService

有些更老的 WebService 不只是要求 DataContractXmlSerializerFormat,还会直接要求请求报文的外层节点名、Body 节点顺序和字段名精确匹配。这类场景通常要补 MessageContractMessageBodyMember

下面这类写法适合对接“按车次查询详情”这类典型老 WebService:

using System.ServiceModel;

[ServiceContract(Namespace = "http://WebXml.com.cn/")]
[XmlSerializerFormat]
public interface ITrainTimeWebServiceContract
{
[OperationContract(Action = "http://WebXml.com.cn/getDetailInfoByTrainCode")]
TrainCodeDetailInfoResponse getDetailInfoByTrainCode(TrainCodeDetailInfo request);
}
using System.ServiceModel;

[MessageContract(
WrapperName = "getDetailInfoByTrainCode",
WrapperNamespace = "http://WebXml.com.cn/")]
public class TrainCodeDetailInfo
{
[MessageBodyMember(Order = 0, Name = "TrainCode")]
public string TrainCode { get; set; }

[MessageBodyMember(Order = 1, Name = "UserID")]
public string UserID { get; set; }
}
var service = BrokerClient.Get<ITrainTimeWebServiceContract>(
"http://www.webxml.com.cn/WebServices/TrainTimeWebService.asmx");

var response = service.getDetailInfoByTrainCode(new TrainCodeDetailInfo
{
TrainCode = "G1",
UserID = ""
});

foreach (DataTable table in response.Result.Tables)
{
Console.WriteLine(table.TableName);
}

这类接口通常有几个特点:

  • 方法名、WrapperName、命名空间都要严格一致
  • Body 字段顺序可能敏感,所以 Order 不要随意调整
  • 返回结果不一定是普通对象,也可能是 DataSetDataTable 这类老结构

如果你碰到“明明地址通了,但对方一直说参数不对”的情况,这一类契约定义就是优先排查对象。

WCF 里这些细节什么时候必须补

DataContract / DataMember

复杂模型建议默认写上,尤其是在下面这些场景:

  • 服务端文档已经明确给出命名空间
  • 结构里有嵌套对象、列表、可空值
  • 你需要稳定控制 XML 节点名

如果模型能调通但字段一直丢失,先检查有没有漏掉 [DataMember]

XmlSerializerFormat

更偏 ASMX 风格的服务,或者对 XML 结构要求非常严格的服务,通常需要显式补上:

[XmlSerializerFormat]

如果你碰到下面这些问题,优先怀疑这一项:

  • 请求报文结构和对方示例长得不一样
  • 明明字段都对,但服务端提示反序列化失败
  • 入参或返回值名字不匹配

MessageParameter

当服务端要求 SOAP 报文里的参数名、返回名必须精确匹配时,要用它显式对齐。

[OperationContract(Action = "http://tempuri.org/ProcessComplexType")]
[return: MessageParameter(Name = "ComplexOutput")]
ComplexOutput ProcessComplexType(
[MessageParameter(Name = "ComplexInput")] ComplexInput input);

它最常见的用途有两个:

  • 对方文档给出的请求节点名和你的参数名不一致
  • 返回节点名不是默认的 MethodNameResult

MessageContract / MessageBodyMember

如果服务端连最外层请求节点名、字段顺序都要求严格一致,就要从 DataContract 提升到 MessageContract

[MessageContract(
WrapperName = "getDetailInfoByTrainCode",
WrapperNamespace = "http://WebXml.com.cn/")]
public class TrainCodeDetailInfo
{
[MessageBodyMember(Order = 0, Name = "TrainCode")]
public string TrainCode { get; set; }

[MessageBodyMember(Order = 1, Name = "UserID")]
public string UserID { get; set; }
}

它更适合这种场景:

  • SOAP Body 的外层节点名必须固定
  • 参数顺序不能变
  • 服务端是比较老的 ASMX / WebService 实现
  • 返回结构里仍然包含 DataSetDataTable 这类旧式数据结构

OperationContract(Action = "...")

ASMX 和部分老 WCF 服务对 SOAP Action 非常敏感。
如果对方文档里给出了明确的 Action URI,就不要省略。

[OperationContract(Action = "http://tempuri.org/GetMax")]
int GetMax(int num1, int num2);

SOAP 1.1 和 SOAP 1.2 怎么区分

默认情况下,Broker 会按常见 SOAP 方式创建代理。如果目标服务要求 SOAP 1.2,可以在契约上显式声明:

[SoapContract(SoapVersion = "soap12")]
[ServiceContract]
[XmlSerializerFormat]
public interface IWebserviceContract
{
[OperationContract(Action = "http://tempuri.org/HelloWorld")]
string HelloWorld();
}

这个写法在 ASMX 场景里尤其重要,因为遗留系统经常会严格区分协议版本。如果对方已经明确写了 SOAP 1.2,就不要继续按默认方式试错。

写 WebService 契约时最容易漏的点

命名空间

DataContract(Namespace = "...")OperationContract(Action = "...") 这些值必须和服务端保持一致。它们不是装饰性写法,而是 SOAP 报文匹配的一部分。

参数名和返回名

如果服务端明确给出了 ComplexInputComplexOutput 这类节点名,记得用 MessageParameter 对齐。

类型选错

这些问题最常见:

  • 服务端是数组,你写成单对象
  • 服务端允许 null,你写成不可空值类型
  • 服务端返回 XML 片段,你却按普通字符串处理
  • 服务端返回 DataSet / DataTable,你却按普通 DTO 去接

先用简单方法验证链路

不要一上来就拿最复杂的 SOAP 方法联调。更稳的顺序是:

  1. 先验证一个无参或简单入参的方法
  2. 再验证数组或复杂对象
  3. 最后再上 out、可空值、XML 片段、SOAP 1.2 这类细节场景

WebService 怎么调试更高效

WebService 联调最难的地方通常不是“有没有代码”,而是“你发出去的 SOAP 报文到底和对方预期差了哪里”。比起盲改契约,更稳的做法是先把正确报文建立出来,再和框架实际发送内容逐项对比。

推荐调试路径

  1. 先用 SoapUI 把目标 WebService 调通
  2. 在 SoapUI 里确认地址、SOAPAction、命名空间、Body 节点名和字段顺序都是正确的
  3. 再用 Broker 契约发同一个请求
  4. 用 Wireshark 抓 Broker 发出的 HTTP / SOAP 包
  5. 把 Broker 请求和 SoapUI 成功请求逐项对比

优先比这些字段:

  • 请求地址是否一致
  • Content-Type 是否一致
  • SOAP 版本是否一致
  • SOAPAction 是否一致
  • Envelope / Body 的命名空间是否一致
  • 外层节点名、参数名、字段顺序是否一致
  • 空值字段是否被发出,发出的格式是否一致

SoapUI 适合做什么

SoapUI 更适合先回答“服务端到底接受什么报文”这个问题。
一旦你在 SoapUI 里调通,就说明服务端要求的最小正确报文已经明确了。

这一步建议固定下来这些内容:

  • 成功请求的原始 XML
  • 对应的 Header
  • 对应的 SOAPAction
  • 使用的是 SOAP 1.1 还是 SOAP 1.2

Wireshark 适合做什么

Wireshark 更适合回答“框架实际发出去的包和成功样本差在哪里”。
它不是替代 SoapUI,而是用来做最终对比。

如果你已经用 Broker 发起请求,但对方返回下面这些错误,就很适合抓包:

  • Action 不匹配
  • 参数缺失
  • 命名空间错误
  • 反序列化失败
  • 服务端提示请求格式不正确

这套方式最适合排查什么问题

  • [XmlSerializerFormat] 是否遗漏
  • MessageParameter 名称是否没对齐
  • MessageContract 的 WrapperName / Namespace / Order 是否不一致
  • SOAP 1.1 / 1.2 是否选错
  • 复杂对象、数组、DataSet 返回结构是否建模错误

接入完成后怎么确认成功

至少确认这些点:

  • 契约接口上的 ServiceContractOperationContract 是否完整
  • 模型命名空间、参数名、返回名是否与服务端一致
  • ASMX 场景下是否补了 XmlSerializerFormat
  • 如果对方要求精确报文结构,是否补了 MessageContract / MessageBodyMember
  • 如果服务要求 SOAP 1.2,是否补了 SoapContract
  • 简单方法、复杂方法、数组或可空值方法至少各验证过一类

下一步看哪里