WebService调用
这页只讨论 Broker 的 WebService 调用方式,也就是 .svc、.asmx 这类 SOAP 服务的接入。如果你当前面对的是医院存量系统、遗留平台接口,或者对方提供的是 ServiceContract 风格契约,这页就是直接可用的接入手册。
什么时候该用这一页
适合场景:
- 对接 WCF
.svc服务 - 对接 ASMX
.asmx服务 - 服务契约包含
ServiceContract、OperationContract - 接口文档描述的是 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 一般更依赖明确的命名空间、XmlSerializerFormat 和 MessageParameter 映射。
简单方法
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 不只是要求 DataContract 或 XmlSerializerFormat,还会直接要求请求报文的外层节点名、Body 节点顺序和字段名精确匹配。这类场景通常要补 MessageContract 和 MessageBodyMember。
下面这类写法适合对接“按车次查询详情”这类典型老 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不要随意调整 - 返回结果不一定是普通对象,也可能是
DataSet、DataTable这类老结构
如果你碰到“明明地址通了,但对方一直说参数不对”的情况,这一类契约定义就是优先排查对象。
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 实现
- 返回结构里仍然包含
DataSet、DataTable这类旧式数据结构
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 报文匹配的一部分。
参数名和返回名
如果服务端明确给出了 ComplexInput、ComplexOutput 这类节点名,记得用 MessageParameter 对齐。
类型选错
这些问题最常见:
- 服务端是数组,你写成单对象
- 服务端允许
null,你写成不可空值类型 - 服务端返回 XML 片段,你却按普通字符串处理
- 服务端返回
DataSet/DataTable,你却按普通 DTO 去接
先用简单方法验证链路
不要一上来就拿最复杂的 SOAP 方法联调。更稳的顺序是:
- 先验证一个无参或简单入参的方法
- 再验证数组或复杂对象
- 最后再上
out、可空值、XML 片段、SOAP 1.2 这类细节场景
WebService 怎么调试更高效
WebService 联调最难的地方通常不是“有没有代码”,而是“你发出去的 SOAP 报文到底和对方预期差了哪里”。比起盲改契约,更稳的做法是先把正确报文建立出来,再和框架实际发送内容逐项对比。
推荐调试路径
- 先用 SoapUI 把目标 WebService 调通
- 在 SoapUI 里确认地址、SOAPAction、命名空间、Body 节点名和字段顺序都是正确的
- 再用 Broker 契约发同一个请求
- 用 Wireshark 抓 Broker 发出的 HTTP / SOAP 包
- 把 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返回结构是否建模错误
接入完成后怎么确认成功
至少确认这些点:
- 契约接口上的
ServiceContract、OperationContract是否完整 - 模型命名空间、参数名、返回名是否与服务端一致
- ASMX 场景下是否补了
XmlSerializerFormat - 如果对方要求精确报文结构,是否补了
MessageContract/MessageBodyMember - 如果服务要求 SOAP 1.2,是否补了
SoapContract - 简单方法、复杂方法、数组或可空值方法至少各验证过一类
下一步看哪里
- 组件级入口:远程调用客户端(Aegis.Net.Broker)
- HTTP 场景:HTTP请求组件