一个具有 DRL 的动态 Rest 客户端代理
引用自http://www.codeproject.com/Articles/762189/A-Dynamic-Rest-Client-Proxy-with-the-DLR
介绍
当我碰到一个感兴趣的 Rest web 服务,想要探索或者可能集成入一个 app 中。 第一件需要做的事就是创建一群 http 通讯 包装类,这样服务就可以调用了. 通常是这样的:
- 阅读 API 文档
- 查看预提供的 .NET 库 (如果有) 并判定它不适合余下的一些编程模型, 所以写一个包装库
- 创建一些服务类来映射 API 的终结点
- 创建一群 POCO 对象来表示往来的数据
- 不停的做这些直到数据开始流动
- 实际用这个 API 做些有趣的东西
即使有像 RestSharp 和 Json2CSharp这样很好的工具,我发觉自己在开始享受乐趣前总要写许多样板代码。
这个小工程缘于我对样板的无聊, 加上希望探索 动态语言运行时 (DLR). 结果是一个基于约定, 动态 rest 客户端和可以使用 RestSharp 或微软用于传输的可移植HttpClient 的透明代理. 目的是它与 Rest 服务交互的启动开销最小.
背景
基本的前提是 RestProxy 是一个 DynamicObject 它把属性和方法调用转化为 Rest 终结点 Uri 并允许基本的 http 动词调用。一个 DynamicObject 在运行时产生它的成员,这个能力被用来建立请求并执行.
动态对象一个不利的方面是缺乏智能感知,因为 IDE 不知道这个对象已有或将有哪些成员. 感觉它不像C#更像是 JavaScript.
使用代码
客户端协议
- 所有通讯都通过 http 或者 https
- 数据传输总是 JSON
- 绝大多数 API 访问可以用
GET, PUT, POST, PATCH或DELETE 完成 - 未命名参数传递给一个动词调用被序列化到请求体
- 命名参数作为请求参数传递 (查询参数或者是编码的形式)
- 输出默认都是动态对象, 但是也支持序列化成一个静态类型
- 所有 Rest 调用都可以是异步的和可等待的 (它们
总是返回一个 Task)
调用协议
调用动态客户端都采用下面的模式:
client.{optional chain of dot separated property names}.verb({optional parameter list});
- 每个 property name 表示一个相对于根 url 的 Url 段
- 动词必须是 get, put, post, patch 或者 delete 中的一个
- 动词调用的未命名参数将被序列化入请求体
- 动词调用的命名参数将作为命名的形参被添加
所以开始运行一个新服务终结点分为三步:
- 创建一个
DynamicRestClient 表示根API - 客户端对象的成员链接起来建立终结点 Uri
- 调用!
示例
那让我们使用 SunLight Labs API 来试试一个简单的 GET 示例:
dynamic client = new DynamicRestClient("http://openstates.org/api/v1/"); dynamic result = await client.metadata.mn.get(apikey: "your_api_key_goes_here"); Assert.IsNotNull(result); Assert.AreEqual("Minnesota", result.name);
在这里都发生了什么? 第一行是漂亮的自解释; 创建 DynamicRestClient 指定根 Uri. 这个 DynamicRestClient 使用 BCL HttpClient 库来请求和响应通讯,很好的 隐藏了使用场景.
第二行所有动态的东西发生的地方,但是仅仅只有两行代码, 一个 rest 终结点就被定义了, 访问并且它的响应被反序列化然后返回.
我们最终调用的终结点是这个: http://openstates.org/api/v1/metadata/mn/.
明白这种模式了吗? metadata.mn 转化成 metadata/mn/ 并且 get() 定义 http 请求调用的类型. 然后这个方法的参数 apikey: "your_api_key_goes_here", 被转化成 形参. 你看到如下行所示创建的 http 请求了吗:
GET http://openstates.org/api/v1/metadata/mn/?apikey=your_api_key_goes_here HTTP/1.1
Accept: application/json, text/json, text/x-json, text/javascript
Accept-Encoding: gzip, deflate
Host: openstates.org
当一个 DynamicObject 有一个属性被访问, 一个名为 TryGetMember 的方法被调用. 在这里, 动态对象的其他重写方法, 我们创建一个 DynamicObject 链来表示完整的 Uri. 这里用类似的方法来调用一个动态方法或者像托管一样调用一个动态对象. 这些重写也用来帮助创建一个完整的终结点 Uri.
public override bool TryGetMember(GetMemberBinder binder, out object result) { // this gets invoked when a dynamic property is accessed // example: proxy.locations will invoke here with a binder named locations // each dynamic property is treated as a url segment result = CreateProxyNode(this, binder.Name); return true; }
但是,等等!Urls 可以有空格和所有各种奇怪的字符!!!
我们引入一种避免的方法. 为了增加一个不是合法 C# 标识符的 uri 段, 通过把它作为一个实参传入动态代理对象上的任何一个方法来避免. 避免的部分可以链接起来和属性部分任意混合.
dynamic client = new DynamicRestClient("http://openstates.org/api/v1/"); var result = await client.bills.mn("2013s1")("SF 1").get(apikey: "your_api_key_goes_here"); Assert.IsNotNull(result); Assert.IsTrue(result.id == "MNB00017167");
这有个额外的好处,允许我们增加数据段而不是代码段. 例如有可能终结点的一部分是由用户的选择决定 (在上面的示例中选取一个状态) 或者它是一个从前一个调用的返回的值.
string billId = GetBillIdFromUser(); var result = await client.mn("2013s1")(billId).get();
段链接
注意 proxy.bills.mn("2013s1")("SF 1") 有些古怪. 在那发生了什么?
直到5个 http 动词中的1个被调用, DynamicRestClient 上的每个方法调用或者属性访问都返回另一个链接在一起的 DynamicRestClient 实例, 形成了完整的 uri.
proxy.bills从 "bills" 属性访问器中返回一个新的代理对象bills.mn("2013s1")在 bills 实例上调用动态方法 "mn" , "2013s1" 作为一个实参传入- mn 和 "2013s1" 都作为代理对象实例返回
- 最后部分
mn("2013s1")("SF 1")是调用 2013s1 实例就好像它是一个委托. 这也返回另一个添加到链中的代理对象实例
每个客户端实例表示终结点 uri 中的一段. 段名定义成动态属性名,方法名 和/或 参数传入动态方法调用.
传入形参
命名形参使用 C# 的命名实参语法 传入动词方法,这里是使用必应的地理位置 API 示例:
dynamic client = new DynamicRestClient("http://dev.virtualearth.net/REST/v1/"); dynamic result = await client.Locations.get(postalCode: "55116", countryRegion: "US", key: "bing_key"); Assert.AreEqual(200, result.statusCode);
上面的 http 请求看起来是这样的:
GET http://dev.virtualearth.net/REST/v1/Locations?postalCode=55116&countryRegion=US&key=bing_key& HTTP/1.1 Accept: application/json, text/json, text/x-json, text/javascript Host: dev.virtualearth.net Accept-Encoding: gzip, deflate Connection: Keep-Alive
Again 你可以看到命名形参传入这个动态方法转换成终结点 uri 上的名称-值对.
避免参数名
参数名不总是合法的 C# 标识符 (虽然实际中它们大多是). 自从我们对请求参数使用 C# 的命名参数语法, 这成了一个问题. 例如这个终结点:
congress.api.sunlightfoundation.com/bills?chamber=senate&history.house_passage_result=pass它有一个带有 "." 的参数名. 为了避免参数名,它们可以传入一个字典中的调用函数. 任何命名参数都是一个 IDictionary<string, object> 将有一个作为参数添加的 键/值 对. 这段示例代码将产生上面的 rest 请求:
dynamic client = new DynamicRestClient("http://congress.api.sunlightfoundation.com"); var parameters = new Dictionar<string, object>() { { "chamber", "senate" }, { "history.house_passage_result", "pass" } }; dynamic result = await client.bills.get(paramList: parameters, apikey: "sunlight_key"); foreach (dynamic bill in result.results) { Assert.AreEqual("senate", (string)bill.chamber); Assert.AreEqual("pass", (string)bill.history.house_passage_result); }
这里同样有一个参数名与一个 C# 保留关键字冲突的实例. 它们同样可以通过把它们作为一个字典传入来避免,但你也可以采用 C# 参数标识符避免语法 使用一个 @.
dynamic client = new DynamicRestClient("http://openstates.org/api/v1/"); //escape the reserved word "long" with an @ symbol var result = await client.legislators.geo.get(apikey: "sunlight_key", lat: 44.926868, @long: -93.214049); Assert.IsNotNull(result); Assert.IsTrue(result.Count > 0);
传入内容对象
Put, patch 和 post 通常在请求体中需要一个对象. 为了完成这个对象, 传入未命名参数给这类动词 (命名和未命名参数可以一起使用但是所有未命名参数必须 proceed the named ones).
在这个示例中,使用一个 POST 方法创建了一个新的谷歌日历. 我们使用一个 ExpandoObject 因为我们不必使用静态 POCO 类型. 那样输入和输出对象可以是完全动态的. 静态类型同样可以用这种方式传入, 并且大多数对象将被序列化成 Json.
dynamic google = new DynamicRestClient("https://www.googleapis.com/calendar/v3/", null, async (request, cancelToken) => { // this demonstrates how t use the configuration callback to handle authentication var auth = new GoogleOAuth2("email profile https://www.googleapis.com/auth/calendar"); var token = await auth.Authenticate("", cancelToken); Assert.IsNotNull(token, "auth failed"); request.Headers.Authorization = new AuthenticationHeaderValue("OAuth", token); }); dynamic calendar = new ExpandoObject(); calendar.summary = "unit_testing"; var list = await google.calendars.post(calendar); Assert.IsNotNull(list); Assert.AreEqual(list.summary, "unit_testing");
在 http 请求结果中, 注意请求体中德序列化对象:
POST https://www.googleapis.com/calendar/v3/calendars HTTP/1.1 Authorization: OAuth xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Accept: application/json, text/json, text/x-json, text/javascript Content-Type: application/json; charset=utf-8 Host: www.googleapis.com Expect: 100-continue Accept-Encoding: gzip, deflate Connection: Keep-Alive Content-Length: 26 {"summary":"unit_testing"}
默认情况下, 传入到一个动词调用的任何未命名对象都将被序列化成 Json 到内容体. 这里有一些例外:
指定内容类型
这里有一些对象类型要用特别的方式序列化.
- HttpContent: 因为这个客户端内部使用可移植 HttpClient, 一个预配置的 HttpContent 对象将被直接传入给请求结果
- Stream: 一个流对象将以一个流内容类型被序列化. 这对于文件上传 api 非常有用.
- Byte array: 一个字节数组将以一个字节数组内容传入
- string: 一个字符串将以字符串内容被传入
- ContentInfo 和 StreamInfo: 这些自定义类封装一个内容对象并允许一个特殊的内容 MIME 类型和其他要设置的内容头
- IEnumerable<object>: 一个对象集合将以多个部分的内容发送, 每个构成对象通过上面的规则被序列化
保留类型
这里有少数类型, 当一个未命名参数被传入那种类型, 将不会做为内容序列化但是会在请求期间触发特殊行为.
- CancellationToken: 因为所有 rest 请求都是异步的, 通过传入一个未命名的 CancellationToken 可以支持取消
- JsonSerializationSettings: 反序列化响应内容通过 json.net 内部处理。 一个这种类型的对象可以自定义传入的内容如何被反序列化
- System.Type: 通过默认内容以一个动态对象返回,来反序列化响应成一个静态类型, 以一个未命名参数传入期望的返回类型 (见下文)
返回类型
通过默认以一个动态对象返回响应. 我发现这很容易使用,而不需要一起修补静态 DTO 类型来匹配 api. 不管怎样, 静态类型反序列化也是支持的. 为了指定反序列化响应的类型, 传入一个期望 Type 的实例给 rest 调用:
public class Bucket { public string kind { get; set; } public string id { get; set; } public string selfLink { get; set; } public string name { get; set; } public DateTime timeCreated { get; set; } public int metageneration { get; set; } public string location { get; set; } public string storageClass { get; set; } public string etag { get; set; } } [TestMethod] public async Task DeserializeToStaticType() { dynamic google = new DynamicRestClient("https://www.googleapis.com/"); dynamic bucketEndPoint = google.storage.v1.b("uspto-pair"); // by default a dynamic object is returned dynamic dynamicBucket = await bucketEndPoint.get(); Assert.IsNotNull(dynamicBucket); Assert.AreEqual(dynamicBucket.name, "uspto-pair"); // but if we really want a static type that works too Bucket staticBucket = await bucketEndPoint.get(typeof(Bucket)); Assert.IsNotNull(staticBucket); Assert.AreEqual(staticBucket.name, "uspto-pair"); }
为什么不使用通用语法?
动态对象支持通用类型参数, 使用那种语法来指定返回类型看起来更自然. 实际上这样写也是可以的:
dynamic google = new DynamicRestClient("https://www.googleapis.com/"); dynamic bucketEndPoint = google.storage.v1.b("uspto-pair"); Bucket staticBucket = await bucketEndPoint.get<Bucket>(); Assert.IsNotNull(staticBucket); Assert.AreEqual(staticBucket.name, "uspto-pair");
问题是, 当实现一个自定义 DynamicObject, 这个通用类型参数对于你期望的类是不可见的. 为了在一个动态对象中提供自定义方法处理, 这个库这样做达到它的核心功能, 你重写 TryInvokeMember. 这个方法传入一个 InvokerMemberBinder 实例。如果你查看传入的这个实例, 你会发现它是一个 DLR 内部类型 CSharpInvokeMemberBinder. 这个类型有一个私有字段,它保有这个通用类型参数, 并且那个字段没有公共的访问器.
我不明白为什么它们在 DRL 外选择支持通用动态方法而不是类, 但是获得类型参数的唯一办法是通过映射到私有字段. 我已经试过它是有效的, 但是它在一个更新到这个 DLR 的实现的时候会随时被打断. 因此比 typeof 语法更麻烦.
其他支持的返回类型
有可能会在一个没有序列化格式中通过传入一个 string, byte[], Stream 或者 HttpResponseMessage 类型参数撤回响应. 因为 string 和 byte[] 的全部内容将被读取并且返回. 返回 Stream 和 HttpResponseMessage 要求调用者在使用后处理它们. 检索 HttpResponseMessage 是另一种允许监视自身返回而不仅是内容的避免机制.
默认
也有可能用一个默认对象初始化 DynamicRestClient 让你指定身份令牌, 一个用户代理字符串和其他默认请求参数还有请求头. 这些默认将用于任何请求使用客户实例.
在这个示例中, 我们首先使用一个帮助类认证一个谷歌账户并获得一个 OAuth 令牌. 然后在一个 DynamicRestClientDefaults 对象中置入并且任何随后对客户端的调用都将通过此令牌来认证. 下面的代码上传一个文件到谷歌云存储的特定篮中.
var auth = new GoogleOAuth2("email profile https://www.googleapis.com/auth/devstorage.read_write"); var token = await auth.Authenticate(""); Assert.IsNotNull(token, "auth failed"); var defaults = new DynamicRestClientDefaults() { AuthScheme = "OAuth", AuthToken = token }; dynamic google = new DynamicRestClient("https://www.googleapis.com/", defaults); using (var stream = new StreamInfo(File.OpenRead(@"D:\temp\test2.png"), "image/png")) { dynamic metaData = new ExpandoObject(); metaData.name = "test2"; dynamic result = await google.upload.storage.v1.b.unit_tests.o.post(metaData, stream, uploadType: new PostUrlParam("multipart")); Assert.IsNotNull(result); }
动词调用
动词调用在动态方法调用中德结果, 它最终调用 TryInvokeMember. 传入给 TryInvokeMember 的参数包含详细的动态方法, 它的名称和参数. 它是这个方法里请求创建,调用和响应反序列化的地方. 几乎所有繁重的工作都发生在这个方法里.
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { if (binder.IsVerb()) // the method name is one of our http verbs - invoke as such { var unnamedArgs = binder.GetUnnamedArgs(args); // filter our sentinel types out of the unnamed args to be passed on the request var requestArgs = unnamedArgs.Where(arg => !arg.IsOfType(_reservedTypes)); // these are the objects that can be passed as unnamed args that // we use intenrally and do not pass to the request var cancelToken = unnamedArgs.OfType<CancellationToken>().FirstOrDefault(CancellationToken.None); var serializationSettings = unnamedArgs.OfType<JsonSerializerSettings>().FirstOrNewInstance(); #if EXPERIMENTAL_GENERICS // dig the generic type argument out of the binder // evil exists within that method var returnType = binder.GetGenericTypeArguments().FirstOrDefault(); #else var returnType = unnamedArgs.OfType<Type>().FirstOrDefault(); #endif // if no return type argument provided there is no need for late bound method dispatch if (returnType == null) { // no return type argumentso return result deserialized as dynamic // parse out the details of the invocation and have the derived class create a Task result = CreateVerbAsyncTask<dynamic>(binder.Name, requestArgs, binder.GetNamedArgs(args), cancelToken, serializationSettings); } else { // we got a type argument (like this: client.get(typeof(SomeType)); ) // make and invoke the generic implementaiton of the CreateVerbAsyncTask method var methodInfo = this.GetType().GetTypeInfo().GetDeclaredMethod("CreateVerbAsyncTask"); var method = methodInfo.MakeGenericMethod(returnType); result = method.Invoke(this, new object[] { binder.Name, requestArgs, binder.GetNamedArgs(args), cancelToken, serializationSettings }); } } else // otherwise the method is yet another uri segment { if (args.Length != 1) throw new InvalidOperationException("The escape sequence can have 1 unnamed parameter"); // this is for when we escape a url segment by passing it as an argument to a method invocation // example: proxy.segment1("escaped") // here we create two new dynamic objects, 1 for "segment1" which is the method name // and then we create one for the escaped segment passed as an argument // - "escaped" in the example var tmp = CreateProxyNode(this, binder.Name); result = CreateProxyNode(tmp, args[0].ToString()); } return true; }
透明代理
以上所有的示例使用一个 DynamicRestClient 封装 HttpClient 来通讯. 这也限制了 http 该库实现的通话对象的可配置性.
这里有一个抽象的 RestProxy 基础类它实现了动态 Uri 创建和调用逻辑但是没有指明用于通讯的 http 客户端库. 用于其他 http 客户端库的一个透明代理可以从这个基础类被创建. 附录的代码包括一个使用用于实例的 RestSharp 透明代理. 这个工程实际上由 RestSharp 开始但是由于它没有一个可移植的版本, 我切换到 HttpClient 替代我常用的版本.
一个使用 RestSharp 的示例:
var client = new RestClient("http://openstates.org/api/v1"); client.AddDefaultHeader("X-APIKEY", "you_api_key"); dynamic proxy = new RestSharpProxy(client); dynamic result = await proxy.metadata.mn.get(); Assert.IsNotNull(result); Assert.IsTrue(result.name == "Minnesota");
兴趣点
DynamicObject 真有一些有趣的可能性而且一旦你理解了是非常容易使用的. 所有上述其实并不需要那么多的代码.
单元测试
单元测试有不同动词的深入示例, 动态 OAuth2 和各式的东西. 如果你尝试运行单元测试看下单元测试工程中的 CredentialStore 类. 它非常直接并且你可以使用它提供你拥有的 api 密钥同时从代码中隔离. 事实上所有集成测试需要一个用于打开终结点 api 的密钥, 所以没有它们大多会失败.
posted on 2015-10-24 19:17 Sky.Y.Chen 阅读(553) 评论(0) 收藏 举报
浙公网安备 33010602011771号