同事回報一起案例,某WCF服務的[OperationContract]方法宣告為void Blah(ref int i, ref string s),以傳址方式(By Reference)傳遞參數(延伸閱讀:Self Test - Value Type vs Reference Type),程式運作多時,詢問我-WCF可以使用 ref 傳參數嗎?
一隻黑天鵝默默在我面前飛過,看得我瞠目結舌…
令人訝異之處在於,依我的理解WCF的Client/Server身處不同程序(Process),不管輸入參數還是傳回結果,都得序列化、反序列化才能正確傳遞,既然不屬於同一程序,記憶體地址空間不同,何來傳「址」?
爬文後,在MSDN找到一段說明:
Out and Ref Parameters
In most cases, you can use in parameters (ByVal in Visual Basic) and out and ref parameters (ByRef in Visual Basic). Because both out and ref parameters indicate that data is returned from an operation, an operation signature such as the following specifies that a request/reply operation is required even though the operation signature returns void.
由此可知,WCF真的支援在參數使用 ref、out!而隨後的說明暗示,使用 out 或 ref 時,即使傳回型別為 void,實際仍會傳回資料。意味WCF在背後做了一些處置,偷偷傳回標為 out、ref 的參數到客戶端,模擬傳值參數行為。
哼!我冷笑一聲,這種「偽」傳值的做法,應該三兩下就破功吧?為什麼WCF要提供容易踩雷的黑心規格?
那就做個實驗踢爆「偽」傳值參數的黑暗面吧!
我設計以下的資料物件,共有三個成員,屬性 PubProp 標註 [DataMember] 可序列化,NonSerProp 則是一般欄位,預期不在序列化範圍,與 PubProp 形成對照。另外, Check() 方法可印出 PubProp 及 NonSerProp 偵查資料內容。
using System.Runtime.Serialization;
namespace WcfDto
{
[DataContract]
publicclass Foo
{
privatestring _PubProp;
[DataMember]
publicstring PubProp {
get
{
return _PubProp;
}
set
{
_PubProp = value;
}
}
publicstring NonSerProp = "Default";
publicstring Check()
{
returnstring.Format("PubProp={0}, NonSerProp={1}",
PubProp, NonSerProp);
}
}
}
建立一個 ByRefCall.svc,豪邁地使用 ref 傳址宣告 Test1(ref int i, ref stirng s) 及 Test2(ref Foo f) 兩個作業方法,被呼叫時修 i、s 及 f 值。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using WcfDto;
namespace WcfWas
{
[ServiceContract]
publicinterface IByRefCall
{
[OperationContract]
void Test1(refint i, refstring s);
[OperationContract]
void Test2(ref WcfDto.Foo f);
}
publicclass ByRefCall : IByRefCall
{
publicvoid Test1(refint i, refstring s)
{
i = 32767;
s = "Darkthread";
}
publicvoid Test2(ref Foo f)
{
f.PubProp = "Server";
f.NonSerProp = "Server";
}
}
}
呼叫端長這樣,分別執行 Test1 及 Test2,並比對變數如何變化:
staticvoid Main(string[] args)
{
BRC.ByRefCallClient c = new BRC.ByRefCallClient();
int i = 1;
string s = "Jeffrey";
Console.WriteLine("Before: i={0}, s={1}", i, s);
c.Test1(ref i, ref s);
Console.WriteLine("After: i={0}, s={1}", i, s);
WcfDto.Foo f = new Foo();
f.PubProp = "Client";
f.NonSerProp = "Client";
Console.WriteLine("Before: {0}", f.Check());
c.Test2(ref f);
Console.WriteLine("After: {0}", f.Check());
Console.Read();
}
執行結果如下:
Before: i=1, s=Jeffrey
After: i=32767, s=Darkthread
Before: PubProp=Client, NonSerProp=Client
After: PubProp=Server, NonSerProp=
Test1(ref i, ref s) 沒什麼意外,i 與 s 都被正確更新。但 Test2(ref f) 就精彩了,原本 PubProp 跟 NonSerProp 都是"Client",呼叫 Test2 後,PubProp 正確換成"Server",但 NonSerProp 被改掉,既不是"Client",也不是"Server",而呈現空白。實際應用時,這種未預期結果已可認定為Bug,可能導致系統出錯。
但,事情是怎麼發生的?
為進一步調查,我修改 Foo.PubProp ,在資料被外界修改時輸出 Environment.StackTrace 追查呼叫來源:
[DataMember]
publicstring PubProp {
get
{
return _PubProp;
}
set
{
Debug.WriteLine(string.Format("Set PubProp=>{0}", value));
Debug.WriteLine(Environment.StackTrace.ToString());
_PubProp = value;
}
}
追蹤結果如下:(省略部分冗長內容)
Set PubProp=>Client
於 System.Environment.GetStackTrace(Exception e, Boolean needFileInfo)
於 System.Environment.get_StackTrace()
於 WcfDto.Foo.set_PubProp(String value) 於 X:\WorkRoom\WCFLab\WcfDto\Foo.cs: 行 24
於 WCFClient.Program.Main(String[] args) 於 X:\WorkRoom\WCFLab\WCFClient\Program.cs: 行 89
… 省略 …
於 System.Threading.ThreadHelper.ThreadStart()
Set PubProp=>Client
於 System.Environment.GetStackTrace(Exception e, Boolean needFileInfo)
於 System.Environment.get_StackTrace()
於 WcfDto.Foo.set_PubProp(String value) 於 X:\WorkRoom\WCFLab\WcfDto\Foo.cs: 行 24
於 ReadFooFromXml(XmlReaderDelegator , XmlObjectSerializerReadContext , XmlDictionaryString[] , XmlDictionaryString[] )
於 System.Runtime.Serialization.ClassDataContract.ReadXmlValue(XmlReaderDelegator xmlReader, XmlObjectSerializerReadContext context)
於 System.Runtime.Serialization.XmlObjectSerializerReadContext.ReadDataContractValue(DataContract dataContract, XmlReaderDelegator reader)
… 省略 …
於 System.ServiceModel.Dispatcher.DataContractSerializerOperationFormatter.DeserializeParameterPart(XmlDictionaryReader reader, PartInfo part, Boolean isRequest)
於 System.ServiceModel.Dispatcher.DataContractSerializerOperationFormatter.DeserializeParameter(XmlDictionaryReader reader, PartInfo part, Boolean isRequest)
於 System.ServiceModel.Dispatcher.DataContractSerializerOperationFormatter.DeserializeParameters(XmlDictionaryReader reader, PartInfo[] parts, Object[] parameters, Boolean isRequest)
… 省略 …
於 System.ServiceModel.Channels.HttpPipeline.EmptyHttpPipeline.BeginProcessInboundRequest(ReplyChannelAcceptor replyChannelAcceptor, Action dequeuedCallback, AsyncCallback callback, Object state)
於 System.ServiceModel.Channels.HttpChannelListener`1.HttpContextReceivedAsyncResult`1.ProcessHttpContextAsync()
於 System.ServiceModel.Channels.HttpChannelListener`1.BeginHttpContextReceived(HttpRequestContext context, Action acceptorCallback, AsyncCallback callback, Object state)
…省略…
Set PubProp=>Server
於 System.Environment.GetStackTrace(Exception e, Boolean needFileInfo)
於 System.Environment.get_StackTrace()
於 WcfDto.Foo.set_PubProp(String value) 於 X:\WorkRoom\WCFLab\WcfDto\Foo.cs: 行 24
於 WcfWas.ByRefCall.Test2(Foo& f) 於 X:\WorkRoom\WCFLab\WcfWas\ByRefCall.svc.cs: 行 31
於 SyncInvokeTest2(Object , Object[] , Object[] )
於 System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(Object instance, Object[] inputs, Object[]& outputs)
… 省略 …
Set PubProp=>Server
於 System.Environment.GetStackTrace(Exception e, Boolean needFileInfo)
於 System.Environment.get_StackTrace()
於 WcfDto.Foo.set_PubProp(String value) 於 X:\WorkRoom\WCFLab\WcfDto\Foo.cs: 行 24
於 ReadFooFromXml(XmlReaderDelegator , XmlObjectSerializerReadContext , XmlDictionaryString[] , XmlDictionaryString[] )
… 省略 …
於 System.ServiceModel.Channels.ServiceChannelProxy.InvokeService(IMethodCallMessage methodCall, ProxyOperationRuntime operation)
於 WCFClient.BRC.ByRefCallClient.WCFClient.BRC.IByRefCall.Test2(Test2Request request) 於 X:\WorkRoom\WCFLab\WCFClient\Service References\BRC\Reference.cs: 行 152
於 WCFClient.BRC.ByRefCallClient.Test2(Foo& f) 於 X:\WorkRoom\WCFLab\WCFClient\Service References\BRC\Reference.cs: 行 158
於 WCFClient.Program.Main(String[] args) 於 X:\WorkRoom\WCFLab\WCFClient\Program.cs: 行 92
… 省略 …
由 StackTrace 的程式位置,PubProp 總共被設定四次:
第一次,Main() f.PubProp = "Client"
第二次,有個 ReadFooFromXml() 函式由SOAP傳送內容讀出Foo,轉成輸入參數交給 ByRefCall.Test2
第三次,ByRefCall.Text2() 執行 f.PubProp = "Server"
第四次,Test2() 執行結果傳回客戶端,再用 ReadFooFromXml() 由 SOAP 讀取結果更新回Foo物件
第四次 StackTrace 有兩個程式行數值得注意。Reference.cs 158行呼叫152行,追到原始碼(如下圖),謎底揭曉。
使用 ref 時,WCF Proxy 會在背後宣告一個 inValue,將 Foo 放進 inValue.f,呼叫 Test2() 取得 retVal,再將原本的 inVal.f 換成 retVal.f。
在這種邏輯,retVal.f 有可能是另一顆新建立的 Foo,也有可能是原來的 f 物件,透過 ReadFooFromXml() 將屬性更新為 SOAP傳回內容。試過為 Foo 加入建構式,發現在 Client 端 Foo 只被建立一次,故可排除另建 Foo 的假設,物件 f 應該還是同一顆,藉由 ReadFooFromXml() 更新 PupProp 屬性,至於為什麼 NonSerProp 會被改成空白,得深入 ReadFooFromXml 邏輯一探究竟。至少,我們得到一項結論:
使用 By Reference 傳遞參數時,不在序列化範圍的屬性或欄位有可能出現非預期結果。
基於以上行為,我認為用 ref 傳遞傳遞Value Type(int、string、decimal…)還好,不致出問題。但用 ref 傳送物件參數是件危險的事,序列化範圍以外的屬性、欄位就有錯亂的風險,跟大家想像的傳統 ref 傳址行為不完全相同,再加上 By Referecne 傳遞違反 WCF 傳輸的本質,不如大家就忘掉「WCF 可以用 ref」這件事吧!