Contract (契約) 是 WCF 服務與用戶端之間的「合約」,定義了「提供什麼操作」、「交換什麼資料」、「如何序列化」。掌握五大契約屬性 (ServiceContract、OperationContract、DataContract、DataMember、ServiceKnownType) 是設計健壯、可版本演進、跨平台互通服務的基石。本篇透過「人員管理」實例,完整示範契約定義、繼承多型序列化、WSDL 對應關係與用戶端呼叫。
| 屬性 | 作用層級 | 核心功能 | 關鍵屬性 |
|---|---|---|---|
| ServiceContract | 介面/類別 | 定義服務邊界、命名空間、名稱、回呼契約 | Namespace, Name, ConfigurationName, CallbackContract |
| OperationContract | 方法 | 定義單一作業、名稱、單向、非同步模式 | Name, IsOneWay, AsyncPattern, ProtectionLevel |
| DataContract | 類別/結構 | 宣告型別參與序列化、命名空間、名稱 | Namespace, Name, IsReference |
| DataMember | 屬性/欄位 | 標示序列化成員、順序、必要性、預設值處理 | Name, Order, IsRequired, EmitDefaultValue |
| ServiceKnownType | 服務/作業/資料契約 | 宣告多型衍生型別,讓序列化器識別子類別 | Type, Method, DeclaringType |
using System.Runtime.Serialization;
namespace WCF.ServiceLib.Contract
{
/// 基底人員類別
[DataContract(Name = "PersonModel", Namespace = "https://webabcd.cnblogs.com")]
public class Person
{
[DataMember(Name = "PersonName", Order = 0, IsRequired = false, EmitDefaultValue = false)]
public string Name { get; set; }
[DataMember(Name = "PersonAge", Order = 1, IsRequired = false, EmitDefaultValue = false)]
public int Age { get; set; }
}
/// 學生類別,繼承 Person
[DataContract(Name = "StudentModel", Namespace = "https://webabcd.cnblogs.com")]
public class Student : Person
{
[DataMember(Name = "School", Order = 0, IsRequired = false, EmitDefaultValue = false)]
public string School { get; set; }
}
}
using System.ServiceModel;
using System.Runtime.Serialization;
namespace WCF.ServiceLib.Contract
{
///
/// 服務契約:指定命名空間、名稱、配置名稱
/// ServiceKnownType:告知序列化器 Student 為 Person 的已知衍生型別
///
[ServiceContract(
Namespace = "https://webabcd.cnblogs.com",
Name = "IPersonManager",
ConfigurationName = "ConfigurationNameTest")]
[ServiceKnownType(typeof(Student))] // 關鍵:缺少此行,傳入 Student 會序列化失敗
public interface IPersonManager
{
///
/// OperationContract.Name 對應 WSDL 中的 operation name
/// MessageParameter.Name 對應訊息參數名稱
///
[OperationContract(Name = "GetPersonName")]
string GetName([MessageParameter(Name = "person")] Person p);
}
}
namespace WCF.ServiceLib.Contract
{
public class PersonManager : IPersonManager
{
public string GetName(Person p) => $"Name: {p.Name}";
}
}
<!-- PersonManager.svc -->
<%@ ServiceHost Language="C#" Debug="true" Service="WCF.ServiceLib.Contract.PersonManager" %>
<!-- Web.config -->
<?xml version="1.0"?>
<configuration>
<system.serviceModel>
<services>
<service name="WCF.ServiceLib.Contract.PersonManager" behaviorConfiguration="ContractBehavior">
<endpoint address=""
binding="basicHttpBinding"
contract="ConfigurationNameTest" /> <!-- 對應 ServiceContract.ConfigurationName -->
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="ContractBehavior">
<serviceMetadata httpGetEnabled="true" />
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
</configuration>
// PersonManager.aspx.cs
public partial class Contract_PersonManager : System.Web.UI.Page
{
protected void btnGetName_Click(object sender, EventArgs e)
{
var proxy = new Contract.PersonManagerClient();
// 建立 Student 實例 (衍生自 Person)
var student = new Contract.StudentModel
{
PersonName = txtName.Text,
School = "資訊工程學系"
};
// 呼叫時 Proxy 會自動處理多型序列化 (因服務端已宣告 ServiceKnownType)
var result = proxy.GetPersonName(student);
ClientScript.RegisterStartupScript(GetType(), "msg", $"alert('{result}');", true);
proxy.Close();
}
}
<client>
<endpoint address="https://localhost:3502/ServiceHost/Contract/PersonManager.svc"
binding="basicHttpBinding"
contract="Contract.IPersonManager" />
</client>
WCF 使用 DataContractSerializer,預設依字母序排序。透過 Order 可強制指定順序,基底類別成員 Order 必須小於衍生類別,否則反序列化會失敗或資料錯位。
[DataMember(Order = 0)] public string Name { get; set; } // 基底先序列化
[DataMember(Order = 1)] public int Age { get; set; }
// 衍生類別從 0 重新開始,但實際 XML 會接在基底之後
[DataMember(Order = 0)] public string School { get; set; }
[ServiceKnownType(typeof(Student))] 在服務契約上 (最常用)<serviceKnownType type="Namespace.Student, Assembly" /> 在 `[ServiceKnownType("GetKnownTypes", typeof(Helper))] 由靜態方法回傳 `IEnumerableKnownTypeAttribute:裝飾在 資料契約類別 上,影響該類別所有使用情境ServiceKnownTypeAttribute:裝飾在 服務契約/作業 上,僅影響特定服務範圍| 屬性 | 預設值 | 建議設定 | 說明 |
|---|---|---|---|
| IsRequired | false | false | true 會強制用戶端必須提供,版本升級新增欄位會破壞相容 |
| EmitDefaultValue | true | false | false 可減少傳輸體積 (不傳輸 null/0/false),且利於版本容錯 |
ServiceKnownType(typeof(Student)) 或 KnownType。Order 設定是否遞增。OperationContract(Name="...") 與 MessageParameter(Name="...") 設定。ServiceContract.Namespace、DataContract.Namespace、用戶端 Proxy 命名空間需一致,否則會產生多餘的 `xmlns` 前綴。[DataContract],成員標記 [DataMember]Name、Namespace、Order,避免依賴預設值KnownType / ServiceKnownTypeIsRequired = false、EmitDefaultValue = false 利於版本演進