最近在写一个SL的小工具,用于图形化编辑一些东西。刚好调用的服务是WCF的Rest方式,于是就碰到了在SL里面直接调用Rest服务的问题,本来Rest服务就是只要有url和内容就可以直接调用的,事实上如果搜索该主题,也可以得到漫山遍野的WebClient方案。不过看看Framework下的WebChannelFactory<TChannel>这个类(这个类型在SL下面不支持...),又感觉用WebClient方式太寒酸了点。。。
这里讨论的前提是:
期望的结果应该是类似与调用WebService的方式。
然后,就慢慢开始达成我们的目标吧。
第一步,Copy契约。。。废话,而且只要会按Ctrl+C和Ctrl+V的人都会做,问题是Copy过来的契约不能直接用,SL没提供这样的类,为了方便示例,就准备一个Sample契约:
[ServiceContract]
public
interface
ISample
{
[WebInvoke(UriTemplate
=
"
/echo/?name={name}
"
,
BodyStyle
=
WebMessageBodyStyle.Bare, RequestFormat
=
WebMessageFormat.Json, ResponseFormat
=
WebMessageFormat.Json)]
[OperationContract]
EchoResponse Echo(
string
name,
string
message);
}
[DataContract]
public
class
EchoResponse
{
[DataMember]
public
string
Name {
get
;
set
; }
[DataMember]
public
string
Message {
get
;
set
; }
}
第二步,实现契约,等等,这里是客户端怎么冒出来个实现契约了?这里实现契约,实际上是指做一个代理类,只不过平时WebService的时候是自动生成代理类的,而Rest服务没有生成代理类的手段,只能人工做了。。。
当然,立即想到的是,代理类底层一定是调用WebClient,毕竟服务是Rest方式提供的,问题是形式,如果全部人工翻译,这个代价有点大,更重要的是:我很懒,能只写一次的代码,绝不写两次,有任何共性的代码结构+无限可能的类型组合,都倾向于使用代码生成来实现。
代理类代码准备
首先,准备好基本的代码:
基础工作
public
class
RestClient
<
T
>
where
T :
class
{
private
static
readonly
Type s_proxyType
=
CreateProxyType();
private
readonly
T m_proxy;
public
RestClient(
string
baseUrl)
{
m_proxy
=
Activator.CreateInstance(s_proxyType, baseUrl)
as
T;
}
private
static
Type CreateProxyType()
{
//
todo : 生成代理类型
return
null
;
}
public
T Channel {
get
{
return
m_proxy; } }
这里的T需要是契约类型,也就是示例中的ISample,泛型约束不给力,不能约束到T必须是接口,只能退而求其次,约束必须是引用类型(接口类型一定是引用类型,如果想到了值类型可以实现接口,那是因为值类型的装箱形式实现了接口,值类型的装箱形式也是引用类型)
那为什么要给一个BaseUri哪?别忘了,契约接口里面的(例如:ISample)只提供了相对地址,没有基地址的话,根本找不到终结点。
然后,开始添加类型生成代码:
创建类型-part-1
private
static
Type CreateProxyType()
{
if
(
!
typeof
(T).IsInterface)
throw
new
NotSupportedException();
if
(
!
Attribute.IsDefined(
typeof
(T),
typeof
(ServiceContractAttribute)))
throw
new
NotSupportedException();
var interfaces
=
Enumerable.Repeat(
typeof
(T),
1
).Concat(
typeof
(T).GetInterfaces()).ToArray();
return
CreateProxyType(interfaces);
}
private
static
Type CreateProxyType(Type[] interfaces)
{
var assembly
=
AppDomain.CurrentDomain.DefineDynamicAssembly(
new
AssemblyName(
"
^_^.
"
+
typeof
(T).FullName), AssemblyBuilderAccess.Run);
var module
=
assembly.DefineDynamicModule(
"
^_^
"
);
var tb
=
module.DefineType(
"
$_$.
"
+
typeof
(T).FullName, TypeAttributes.Public
|
TypeAttributes.Class,
typeof
(
object
), interfaces);
var field
=
tb.DefineField(
"
f
"
,
typeof
(
string
), FieldAttributes.Private
|
FieldAttributes.InitOnly);
CreateProxyCtor(tb, field);
int
methodCount
=
0
;
foreach
(var i
in
interfaces)
foreach
(var m
in
i.GetMethods(BindingFlags.Public
|
BindingFlags.Instance))
CreateProxyMethod(tb, m, field,
ref
methodCount);
return
tb.CreateType();
这里正好可以把泛型约束的遗憾给不上,还可以变本加厉的要求接口上必须标注了ServiceContract,不满足要求的也泛型过来的话,直接给个不支持(对了别忘了接口是可以多继承的,所以实现的时候要实现全部接口,别只实现其中的一两个哦)
现在,问题变成了如何生成这么一个类型来实现这一个或多个接口,不过,这个问题先放一下,考虑一下,如果已经提取到了地址和Method,以及可能有的Post内容,如何获得返回值哪?问题的解决方案晚上到处都有——用WebClient,先把这个实现了吧(这里仅仅实现Json的):
对WebClient的封装
public
static
TResp PostJson
<
TReq, TResp
>
(
string
baseUri,
string
uri,
string
method, TReq data)
{
string
req;
{
DataContractJsonSerializer jsonSerializer
=
new
DataContractJsonSerializer(
typeof
(TReq));
var ms
=
new
MemoryStream();
jsonSerializer.WriteObject(ms, data);
ms.Seek(
0L
, SeekOrigin.Begin);
req
=
new
StreamReader(ms).ReadToEnd();
}
ManualResetEvent mre
=
new
ManualResetEvent(
false
);
UploadStringCompletedEventArgs acArgs
=
null
;
WebClient client
=
new
WebClient();
client.UploadStringCompleted
+=
(sender, e)
=>
{
acArgs
=
e;
mre.Set();
};
client.Headers[HttpRequestHeader.ContentType]
=
"
application/json; charset=UTF-8
"
;
client.UploadStringAsync(
new
Uri(baseUri
+
uri), method, req);
mre.WaitOne();
if
(acArgs.Cancelled)
throw
new
TimeoutException();
if
(acArgs.Error
!=
null
)
throw
new
WebException(
"
Rest Error:
"
+
acArgs.Error.Message, acArgs.Error);
var str
=
acArgs.Result;
{
DataContractJsonSerializer respSerializer
=
new
DataContractJsonSerializer(
typeof
(TResp));
var ms
=
new
MemoryStream();
var sw
=
new
StreamWriter(ms);
sw.Write(acArgs.Result);
sw.Flush();
ms.Seek(
0L
, SeekOrigin.Begin);
return
(TResp)respSerializer.ReadObject(ms);
}
}
public
static
TResp GetJson
<
TResp
>
(
string
baseUri,
string
uri,
string
method)
{
ManualResetEvent mre
=
new
ManualResetEvent(
false
);
DownloadStringCompletedEventArgs acArgs
=
null
;
WebClient client
=
new
WebClient();
client.DownloadStringCompleted
+=
(sender, e)
=>
{
acArgs
=
e;
mre.Set();
};
client.DownloadStringAsync(
new
Uri(baseUri
+
uri), method);
mre.WaitOne();
if
(acArgs.Cancelled)
throw
new
TimeoutException();
if
(acArgs.Error
!=
null
)
throw
new
WebException(
"
Rest Error:
"
+
acArgs.Error.Message, acArgs.Error);
var str
=
acArgs.Result;
{
DataContractJsonSerializer respSerializer
=
new
DataContractJsonSerializer(
typeof
(TResp));
var ms
=
new
MemoryStream();
var sw
=
new
StreamWriter(ms);
sw.Write(acArgs.Result);
sw.Flush();
ms.Seek(
0L
, SeekOrigin.Begin);
return
(TResp)respSerializer.ReadObject(ms);
}
现在可以继续思考前面的问题,我们有了契约的信息,有了如何请求Rest服务的原始代码,剩下的工作就是:
- 所有参数的名称+他们的值+UriTemplate=实际的Uri+post内容的参数
看起来就是个数组的查找和字符串替换的事情,问题已经被化解的差不多了,突然想起来一个蛋疼的东西。。。BodyStyle=WebMessageBodyStyle.Wrapped | WrappedRequest | WrapperResponse
这东西还要给他们生成一个类型才能玩,算了不陪WCF玩了,其它的几种一律不支持
既然决定不完全支持,那干脆在加几条:
- 不支持自定义类型转换器 - 不是不能支持,而是嫌其麻烦(统一用ToString代替转换器)
- 不支持Fault契约 - 还是麻烦
- 不支持KnownType - 依然是麻烦
在成功的“减赋”之后,终于真的感觉事情少了很多,现在再去实现那堆接口:
创建类型-part-2
private
static
void
CreateProxyCtor(TypeBuilder tb, FieldBuilder field)
{
var ctor
=
tb.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard,
new
Type[] {
typeof
(
string
) });
var il
=
ctor.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Call,
typeof
(
object
).GetConstructor(Type.EmptyTypes));
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Stfld, field);
il.Emit(OpCodes.Ret);
}
private
static
void
CreateProxyMethod(TypeBuilder tb, MethodInfo mi, FieldBuilder field,
ref
int
methodCount)
{
var m
=
tb.DefineMethod(
"
M
"
+
(
++
methodCount).ToString(),
MethodAttributes.Private
|
MethodAttributes.Virtual
|
MethodAttributes.Final,
mi.ReturnType,
(from pi
in
mi.GetParameters() select pi.ParameterType).ToArray());
if
(
!
Attribute.IsDefined(mi,
typeof
(OperationContractAttribute)))
{
CreateEmptyMethod(m);
}
else
{
string
template
=
null
;
string
method
=
null
;
{
var wga
=
(WebGetAttribute)Attribute.GetCustomAttribute(mi,
typeof
(WebGetAttribute));
if
(wga
!=
null
)
{
template
=
wga.UriTemplate;
method
=
"
GET
"
;
}
}
{
var wia
=
(WebInvokeAttribute)Attribute.GetCustomAttribute(mi,
typeof
(WebInvokeAttribute));
if
(wia
!=
null
)
{
template
=
wia.UriTemplate;
method
=
wia.Method
??
"
POST
"
;
}
}
if
(template
==
null
)
{
CreateEmptyMethod(m);
}
else
{
CreateCoreMethod(m, mi, template, method, field);
}
}
tb.DefineMethodOverride(m, mi);
}
private
static
void
CreateCoreMethod(MethodBuilder m, MethodInfo mi,
string
template,
string
method, FieldBuilder field)
{
var il
=
m.GetILGenerator();
il.DeclareLocal(
typeof
(
string
));
il.Emit(OpCodes.Ldstr, template);
il.Emit(OpCodes.Stloc_0);
var pis
=
mi.GetParameters();
int
postParameter
=
-
1
;
for
(
int
i
=
0
; i
<
pis.Length; i
++
)
{
if
(template.Contains(
"
{
"
+
pis[i].Name
+
"
}
"
))
{
//
template = template.Replace("{?.Name}", HttpUtility.UrlEncode(?.ToString()));
il.Emit(OpCodes.Ldloc_0);
il.Emit(OpCodes.Ldstr,
"
{
"
+
pis[i].Name
+
"
}
"
);
il.Emit(OpCodes.Ldarg, i
+
1
);
if
(pis[i].ParameterType.IsValueType)
il.Emit(OpCodes.Box, pis[i].ParameterType);
il.Emit(OpCodes.Callvirt,
typeof
(
object
).GetMethod(
"
ToString
"
));
il.Emit(OpCodes.Call,
typeof
(HttpUtility).GetMethod(
"
UrlEncode
"
));
il.Emit(OpCodes.Call,
typeof
(
string
).GetMethod(
"
Replace
"
,
new
Type[] {
typeof
(
string
),
typeof
(
string
) }));
il.Emit(OpCodes.Stloc_0);
}
else
{
if
(postParameter
>
0
)
throw
new
NotSupportedException();
postParameter
=
i;
if
(method
==
"
GET
"
)
method
=
"
POST
"
;
}
}
if
(postParameter
==
-
1
)
{
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldfld, field);
il.Emit(OpCodes.Ldloc_0);
il.Emit(OpCodes.Ldstr, method);
il.Emit(OpCodes.Call,
typeof
(RestClient).GetMethod(
"
GetJson
"
).MakeGenericMethod(mi.ReturnType));
}
else
{
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldfld, field);
il.Emit(OpCodes.Ldloc_0);
il.Emit(OpCodes.Ldstr, method);
il.Emit(OpCodes.Ldarg, postParameter
+
1
);
il.Emit(OpCodes.Call,
typeof
(RestClient).GetMethod(
"
PostJson
"
).MakeGenericMethod(pis[postParameter].ParameterType, mi.ReturnType));
}
il.Emit(OpCodes.Ret);
}
private
static
void
CreateEmptyMethod(MethodBuilder m)
{
var il
=
m.GetILGenerator();
if
(m.ReturnType
!=
typeof
(
void
))
{
if
(m.ReturnType.IsValueType)
{
il.DeclareLocal(m.ReturnType);
il.Emit(OpCodes.Ldarga_S, (
byte
)
0
);
il.Emit(OpCodes.Initobj, m.ReturnType);
}
else
{
il.Emit(OpCodes.Ldnull);
}
}
il.Emit(OpCodes.Ret);
这里,做了几件额外的事情:
- 如果接口里面有非契约的方法 - 用返回默认值来实现(当然也可以修改成throw)
- 如果发现参数中多余1个未在UriTemplate中出现的参数,直接报错 - 因为不支持Wrapper
- 如果方法为GET并且带Post信息,将方法更改为POST
不过,还有几件事情没做:
- 参数的值为null时会抛空引用 - 有空的话可以自己改,我这里反正都传空字符串的。。。而且,在后面的外壳部分也可以包装掉
使用代理类
类看看这个代理类怎么用:
var client
=
new
RestClient
<
ISample
>
(
"
http://127.0.0.1:12345/
"
);
var result
=
client.Channel.Echo(
"
Zhenway
"
,
"
Hello world!
"
);
是不是有点调用WebService的感觉?
不过还有点欠缺,多了个Channel,而且随便调用那个方法,都要出现这个Channel,感觉不爽
干脆再学一次WebService,来个外壳(当然,也可以直接拿着Channel去干活):
public
class
SampleClient
: RestClient
<
ISample
>
, ISample
{
public
SampleClient()
:
base
(
"
the default uri
"
) { }
public
SampleClient(string uri)
:
base
(uri) { }
public
EchoResponse Echo(
string
name,
string
message)
{
return
Channel.Echo(name, message);
}
}
这样用起来就会舒服一些(而且也可以做些额外的工作)
var client
=
new
SampleClient(
"
http://127.0.0.1:12345/
"
);
var result
=
client.Echo(
"
Zhenway
"
,
"
Hello world!
"
);
这样明显更舒服一些。
暗藏的危机
看起来万事俱备,但是实际上一使用才发现神马都是浮云。
如果在用户界面上调用client.Echo,那么就算等上一万年,也拿不到结果,而且整个浏览器也会因为Sliverlight插件而出现假死。
为什么会这样哪?分析一下原因:可以发现在实现PostJson和GetJson方法中的
永远等不到被Set的那一刻,看看代码逻辑似乎没什么问题,不过仔细想想,就可以发现问题:
- 首先,Sliverlight的UI线程是基于消息的
- 其次,webclient的回调事件是会回到请求Async方法的同步上下文上的
那么,是不是发现问题了,UI线程请求了client.Echo,client.Echo请求了channel.Echo,channel.Echo请求了WebClient的DownloadStringAsync,然后等待mre信号。
WebClient在开始DownloadStringAsync时,抓取了当时的同步上下文,也就是UI的同步上下文,再开始异步下载,下载完成时,告诉同步上下文,可以执行回调事件了。
而此时,同步上下文-也就是UI线程的消息处理机制却无法工作,之前的一个消息尚未处理完成(被迫在等待mre的信号中),于是出现了死锁。
发现了问题所在,要排除问题,也就很简单了,只要破坏这个死锁中的一个环节,自然就能让UI活起来。
最简单的方式是从入口下手:UI线程不直接请求client.Echo,而是修改成UI线程新开个线程(直接用线程池就可以了),请求client.Echo,这样就把同步上下文从UI线程的上下文切换到了另一个上下文。
来看看修改后的代码:
ThreadPool.QueueUserWorkItem(_
=>
{
var client
=
new
SampleClient(
"
http://127.0.0.1:12345/
"
);
var result
=
client.Echo(
"
Zhenway
"
,
"
Hello world!
"
);
//
do something ...
});
看起来不错,不过要修改界面的话(90%的情况下都要修改界面的吧),还要回归到UI线程,干脆根据Silverlight的基于事件的异步方式,重写我们的Client
重写后的Client
public
void
EchoAsync(
string
name,
string
message)
{
EchoAsync(name, message,
null
);
}
public
void
EchoAsync(
string
name,
string
message,
object
userState)
{
var sc
=
SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem(_
=>
{
EchoResponse result
=
null
;
Exception ex1
=
null
;
try
{
result
=
Channel.Echo(name, message);
}
catch
(Exception ex)
{
ex1
=
ex;
}
try
{
var handler
=
EchoCompleted;
if
(handler
!=
null
)
sc.Post(__
=>
handler(
this
,
new
EchoAsyncCompletedEventArgs(result, ex1,
false
, userState)),
null
);
}
catch
(Exception) { }
});
}
public
event
EventHandler
<
EchoAsyncCompletedEventArgs
>
EchoCompleted;
public
class
EchoAsyncCompletedEventArgs
: AsyncCompletedEventArgs
{
public
EchoAsyncCompletedEventArgs(EchoResponse response,
Exception error,
bool
cancelled,
object
userState)
:
base
(error, cancelled, userState)
{
Response
=
response;
}
public
EchoResponse Response {
get
;
private
set
; }
这样,Echo就可以很好的工作了(在UI线程中调用EchoAsync后,EchoCompleted事件也会在UI线程中执行):
var client
=
new
SampleClient(
"
http://127.0.0.1:12345/
"
);
client.EchoCompleted
+=
(sender, e)
=>
{
//
update UI elements, here.
};
var result
=
client.EchoAsync(
"
Zhenway
"
,
"
Hello world!
"
);
是不是看起来还不错,不过,想想如果一个服务有10来个方法,每个这么搞一下,这个量依然很高。。。
再次使用动态代理
遇到这类重复性工作,第一反应,就是再动态一把,把这些重复工作消除成一次性的静态行为,这里就可以通过再次动态代理,来消除原来动态代理的种种不爽之处。
只不过,这次的动态代理不是用动态生成类型,而是换dynamic,也给大家换换口味
这次动态代理的功能自然是完成“重写后的Client”的功能,这里分两大块,一个是异步方法,一个是事件
动态异步代理
public
class
DynamicAsyncClient
: DynamicObject
{
private
readonly
object
m_channel;
private
readonly
HashSet
<
string
>
m_methods;
private
readonly
Dictionary
<
string
, DynamicAsyncCompletedEventHandler
>
m_events;
public
DynamicAsyncClient(
object
channel)
{
if
(channel
==
null
)
throw
new
ArgumentNullException(
"
channel
"
);
m_channel
=
channel;
m_methods
=
new
HashSet
<
string
>
(from m
in
channel.GetType().GetMethods(BindingFlags.Public
|
BindingFlags.Instance)
select m.Name);
m_events
=
new
Dictionary
<
string
, DynamicAsyncCompletedEventHandler
>
();
}
public
override
bool
TryInvokeMember(InvokeMemberBinder binder,
object
[] args,
out
object
result)
{
if
(m_methods.Contains(binder.Name))
{
result
=
m_channel.GetType().InvokeMember(binder.Name, BindingFlags.InvokeMethod
|
BindingFlags.Public
|
BindingFlags.Instance,
null
, m_channel, args);
return
true
;
}
if
(binder.Name.EndsWith(
"
Completed
"
))
{
lock
(m_events)
m_events[binder.Name]
=
(DynamicAsyncCompletedEventHandler)args[
0
];
result
=
null
;
return
true
;
}
if
(binder.Name.EndsWith(
"
Async
"
))
{
try
{
AsyncInvoke(binder.Name.Remove(binder.Name.Length
-
"
Async
"
.Length), args,
null
);
result
=
null
;
return
true
;
}
catch
(MissingMethodException) { }
}
return
base
.TryInvokeMember(binder, args,
out
result);
}
private
void
AsyncInvoke(
string
name,
object
[] args)
{
var sc
=
SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem(_
=>
{
dynamic result
=
null
;
Exception ex1
=
null
;
try
{
result
=
m_channel.GetType().InvokeMember(name, BindingFlags.InvokeMethod
|
BindingFlags.Public
|
BindingFlags.Instance,
null
, m_channel, args);
}
catch
(Exception ex)
{
ex1
=
ex;
}
try
{
DynamicAsyncCompletedEventHandler handler;
lock
(m_events)
if
(m_events.TryGetValue(name
+
"
Completed
"
,
out
handler))
sc.Post(__
=>
handler(
this
,
new
DynamicAsyncCompletedEventArgs(result, ex1,
false
,
null
)),
null
);
}
catch
(Exception) { }
});
}
}
public
delegate
void
DynamicAsyncCompletedEventHandler(
object
sender, DynamicAsyncCompletedEventArgs e);
public
class
DynamicAsyncCompletedEventArgs
: AsyncCompletedEventArgs
{
public
DynamicAsyncCompletedEventArgs(dynamic response, Exception error,
bool
cancelled,
object
userState)
:
base
(error, cancelled, userState)
{
Response
=
response;
}
public
dynamic Response {
get
;
private
set
; }
再删除SampleClient中前面添加的两个异步方法后,来看看如何跑起来:
var client
=
new
DynamicAsyncClient(
new
SampleClient(
"
http://127.0.0.1:12345/
"
));
client.EchoCompleted
+=
(sender, e)
=>
{
//
update UI elements, here.
textbox1.Text
=
e.Response.Name;
textbox2.Text
=
e.Response.Message;
};
var result
=
client.EchoAsync(
"
Zhenway
"
,
"
Hello world!
"
);
小节
到这里,整个主题也告一段落,当然这里面可以改良的东西还有很多,例如,对WCF的Rest服务更多的支持,动态异步代理支持userState等,众多改良尚可去做,不过这些暂时省略了