RESTful 简化 - 一个通用的 REST 服务客户端库
本文介绍了一个通用的 REST 客户端库以及如何使用它的示例。
下载 LightCaseClient.zip - 7.97 KB
下载 LightCaseClientExample.zip - 29.65 KB
引言
本文介绍了一个通用的 REST 服务客户端库以及如何使用它的示例。
背景
当从 REST 服务 从 Web 浏览器发出调用时,我们有设计精良的 jQuery 来简化我们的工作。但如果我们从非浏览器客户端调用 REST 服务,工作可能会变得更复杂。例如,如果我们想对一个 REST 方法发出 POST 调用,该调用涉及发送和接收数据,并且我们想使用 HttpWebRequest 对象,我们需要完成以下步骤来完成调用:
- 创建和配置 HttpWebRequest 对象;
- 将数据序列化为服务可识别的格式;
- 从 HttpWebRequest 对象获取 请求流,并将序列化后的数据发送到服务;
- 从 HttpWebRequest 对象获取 HttpWebResponse 对象,并进一步获取 响应流。
- 从响应流中读取数据,并将其反序列化为所需数据类型的对象。
如果您查看 我之前的某个示例,您会发现调用 REST 方法非常麻烦。但如果您仔细查看每个 REST 服务调用,您可能会发现所有 REST 调用都非常相似。唯一的区别是发送到服务的数据类型和从服务接收的数据类型在不同调用中可能不同。这使得使用“泛型”来创建 REST 客户端库以简化 REST 调用成为理想选择。本文将介绍这个通用的 REST 服务客户端库,我将称之为“LightCase”客户端。在我编写这个客户端库之前,我查看了“Web API”,并希望找到类似的东西,但我简短的搜索并没有给我想要的答案。“LightCase”客户端具有以下特性:
- 对 REST 方法的调用可以通过配置对象轻松进行配置;
- 该库为应用程序提供了选择所需序列化器的选项,用于将数据发送到服务和从服务接收数据。
- 该库为应用程序提供了将 cookie 发送到 REST 服务以使用 Http 会话状态或通过服务进行某些安全检查的选项;
- 该库支持同步和异步调用;
- 该库是“几乎”线程安全的。它不是完全线程安全的,因为我认为实现完全线程安全可能会使用更多的运行时资源。只要您在 REST 调用进行期间不对配置对象进行更改,您就不会遇到线程安全问题。
LightCase 客户端库
客户端库在上述 Visual Studio 2010 解决方案中实现为一个类库。“SupportClasses.cs”文件实现了某些支持类和接口。通用的 REST 客户端在其余文件中实现。让我们先看看支持类。
支持类
“SupportClasses.cs”文件实现如下:
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Text; using System.Web.Script.Serialization; namespace LightCaseClient.Support { public class CookiedHttpWebRequestFactory { private static Object synchLock = new Object(); // This dictionary keeps the cookie container for each domain. private static Dictionary<string, CookieContainer> containers = new Dictionary<string, CookieContainer>(); public static HttpWebRequest Create(string url) { // Create a HttpWebRequest object var request = (HttpWebRequest)WebRequest.Create(url); // this get the dmain part of from the url string domain = (new Uri(url)).GetLeftPart(UriPartial.Authority); // try to get a container from the dictionary, if it is in the // dictionary, use it. Otherwise, create a new one and put it // into the dictionary and use it. CookieContainer container; lock (synchLock) { if (!containers.TryGetValue(domain, out container)) { container = new CookieContainer(); containers[domain] = container; } } // Assign the cookie container to the HttpWebRequest object request.CookieContainer = container; return request; } } // Defines an adapter interface for the serializers public interface ISerializerAdapter { string Serialize(object obj); T Deserialize<T>(string input); } // An implementation of ISerializerAdapter based on the JavaScriptSerializer public class JavaScriptSerializerAdapter : ISerializerAdapter { private JavaScriptSerializer serializer; public JavaScriptSerializerAdapter() { serializer = new JavaScriptSerializer(); } public string Serialize(object obj) { return serializer.Serialize(obj); } public T Deserialize<T>(string input) { return serializer.Deserialize<T>(input); } } // The configuration class defines how the rest call is made. public class ClientConfiguration { public string ContentType { get; set; } public string Accept { get; set; } public bool RequrieSession { get; set; } public ISerializerAdapter OutBoundSerializerAdapter { get; set; } public ISerializerAdapter InBoundSerializerAdapter { get; set; } } // The delegates use for asychronous calls public delegate void RestCallBack<T>(Exception ex, T result); public delegate void RestCallBackNonQuery(Exception ex); }
- “CookiedHttpWebRequestFactory”类是一个类工厂,用于创建在进行 REST 服务调用时发送 cookie 的 HttpWebRequest 对象。如果您想了解更多关于在进行服务调用时发送 cookie 的信息,可以查看本文。
- “ISerializerAdapter”是一个包装序列化器的接口。如果您想在进行服务调用时使用您选择的序列化器,您需要实现此接口来包装您所需的序列化器。“JavaScriptSerializerAdapter”包装了 JavaScriptSerializer。它将是“LightCase”客户端库默认使用的序列化器。
- “ClientConfiguration”类将用于配置如何进行服务调用。
- “RestCallBack”和“RestCallBackNonQuery” 委托用于进行异步服务调用。
LightCase 通用客户端代理
“GenericProxies”类是我们用于进行 REST 服务调用的代理类。它在三个 部分类中实现。“GenericProxies.cs”文件初始化了代理类的默认行为并实现了一些实用方法。
using System.IO; using System.Net; using System.Text; using LightCaseClient.Support; namespace LightCaseClient { public static partial class GenericProxies { public static ClientConfiguration DefaultConfiguration { get; set; } // static Constructtor static GenericProxies() { // Initiate the default configuration DefaultConfiguration = new ClientConfiguration(); DefaultConfiguration.ContentType = "application/json"; DefaultConfiguration.Accept = "application/json"; DefaultConfiguration.RequrieSession = false; DefaultConfiguration.OutBoundSerializerAdapter = new JavaScriptSerializerAdapter(); DefaultConfiguration.InBoundSerializerAdapter = new JavaScriptSerializerAdapter(); } // Create a request object according to the configuration private static HttpWebRequest CreateRequest(string url, ClientConfiguration clientConfig) { return (clientConfig.RequrieSession) ? CookiedHttpWebRequestFactory.Create(url) : (HttpWebRequest)WebRequest.Create(url); } // Post data to the service private static void PostData<T>(HttpWebRequest request, ClientConfiguration clientConfig, T data) { var jsonRequestString = clientConfig.OutBoundSerializerAdapter.Serialize(data); var bytes = Encoding.UTF8.GetBytes(jsonRequestString); using (var postStream = request.GetRequestStream()) { postStream.Write(bytes, 0, bytes.Length); } } // Receive data from the service private static T ReceiveData<T>(HttpWebRequest request, ClientConfiguration clientConfig) { string jsonResponseString; using (var response = (HttpWebResponse)request.GetResponse()) { var stream = response.GetResponseStream(); if (stream == null) { return default(T); } using (var streamReader = new StreamReader(stream)) { jsonResponseString = streamReader.ReadToEnd(); } } return clientConfig.InBoundSerializerAdapter.Deserialize<T>(jsonResponseString); } } }
- 静态构造函数“GenericProxies”初始化了 REST 服务调用的默认行为。“ContentType”属性是发送到服务的数据格式,“Accept”属性是客户端期望从服务接收的数据格式。“RequrieSession”属性定义了调用时是否将 cookie 发送到服务。“OutBoundSerializerAdapter”和“InBoundSerializerAdapter”定义了用于序列化数据的序列化器。
- “PostData”和“ReceiveData”方法是两个实用方法,稍后将用于简化代理方法的编码。
用于支持同步 REST 服务调用的代理方法在“GenericProxies.synchronous.cs”文件中实现。
using LightCaseClient.Support; namespace LightCaseClient { // Synchronous proxy implementation public static partial class GenericProxies { // ********************** Synchronous GET ************************* public static TR RestGet<TR>(string url, ClientConfiguration configuration) { var clientConfig = configuration ?? DefaultConfiguration; var request = CreateRequest(url, clientConfig); request.Accept = clientConfig.Accept; request.Method = "GET"; return ReceiveData<TR>(request, clientConfig); } // Overload method public static TR RestGet<TR>(string url) { return RestGet<TR>(url, DefaultConfiguration); } // ******** Synchronous GET, no response expected ******* public static void RestGetNonQuery(string url, ClientConfiguration configuration) { var clientConfig = configuration ?? DefaultConfiguration; var request = CreateRequest(url, clientConfig); request.Method = "GET"; request.GetResponse().Close(); } // Overload method public static void RestGetNonQuery(string url) { RestGetNonQuery(url, DefaultConfiguration); } // ***************** Synchronous POST ******************** public static TR RestPost<TR, TI>(string url, TI data, ClientConfiguration configuration) { var clientConfig = configuration ?? DefaultConfiguration; var request = CreateRequest(url, clientConfig); request.ContentType = clientConfig.ContentType; request.Accept = clientConfig.Accept; request.Method = "POST"; PostData(request, clientConfig, data); return ReceiveData<TR>(request, clientConfig); } // Overload method public static TR RestPost<TR, TI>(string url, TI data) { return RestPost<TR, TI>(url, data, DefaultConfiguration); } // ****** Synchronous GET, no respons expected ****** public static void RestPostNonQuery<TI>(string url, TI data, ClientConfiguration configuration) { var clientConfig = configuration ?? DefaultConfiguration; var request = CreateRequest(url, clientConfig); request.ContentType = clientConfig.ContentType; request.Accept = clientConfig.Accept; request.Method = "POST"; PostData(request, clientConfig, data); request.GetResponse().Close(); } // Overload method public static void RestPostNonQuery<TI>(string url, TI data) { RestPostNonQuery(url, data, DefaultConfiguration); } } }
- “RestGet”和“RestGetNonQuery”方法将对 REST 服务进行 GET 调用。如果您只将数据发送到服务,但不期望从服务获得响应,您应该使用“RestGetNonQuery”方法。
- “RestPost”和“RestPostNonQuery”方法将对 REST 服务进行 POST 调用。如果您只将数据发送到服务,但不期望从服务获得响应,您应该使用“RestPostNonQuery”方法。
- 每个方法都有一个重载。如果调用时未向方法提供配置对象,则将使用默认配置。
基于同步方法,相应的异步方法在“GenericProxies.asynchronous.cs”文件中实现。
using System; using System.Runtime.Remoting.Messaging; using LightCaseClient.Support; namespace LightCaseClient { // Asynchronous proxy implementation public static partial class GenericProxies { private delegate TR GetDelegate<TR>(string url, ClientConfiguration configuration); private delegate void GetNonQueryDelegate(string url, ClientConfiguration configuration); private delegate TR PostDelegate<TR, TI>(string url, TI data, ClientConfiguration configuration); private delegate void PostNonQueryDelegate<TI>(string url, TI data, ClientConfiguration configuration); // *************** Asynchronous Get *************************** public static void RestGetAsync<TR>(string url, RestCallBack<TR> callback, ClientConfiguration configuration) { var get = new GetDelegate<TR>(RestGet<TR>); get.BeginInvoke(url, configuration, ar => { var result = (AsyncResult)ar; var del = (GetDelegate<TR>)result.AsyncDelegate; var value = default(TR); Exception e = null; try { value = del.EndInvoke(result); } catch (Exception ex) { e = ex; } if (callback != null) { callback(e, value); } }, null); } // Overload method public static void RestGetAsync<TR>(string url, RestCallBack<TR> callback) { RestGetAsync<TR>(url, callback, DefaultConfiguration); } // *********** Asynchronous Get, no response expected ************* public static void RestGetNonQueryAsync(string url, RestCallBackNonQuery callback, ClientConfiguration configuration) { var get = new GetNonQueryDelegate(RestGetNonQuery); get.BeginInvoke(url, configuration, ar => { var result = (AsyncResult)ar; var del = (GetNonQueryDelegate)result.AsyncDelegate; Exception e = null; try { del.EndInvoke(result); } catch (Exception ex) { e = ex; } if (callback != null) { callback(e); } }, null); } // Overload method public static void RestGetNonQueryAsync(string url, RestCallBackNonQuery callback) { RestGetNonQueryAsync(url, callback, DefaultConfiguration); } // *************** Asynchronous Post ********************* public static void RestPostAsync<TR, TI>(string url, TI data, RestCallBack<TR> callback, ClientConfiguration configuration) { var post = new PostDelegate<TR, TI>(RestPost<TR, TI>); post.BeginInvoke(url, data, configuration, ar => { var result = (AsyncResult)ar; var del = (PostDelegate<TR, TI>)result.AsyncDelegate; var value = default(TR); Exception e = null; try { value = del.EndInvoke(result); } catch (Exception ex) { e = ex; } if (callback != null) { callback(e, value); } }, null); } // Overload method public static void RestPostAsync<TR, TI>(string url, TI data, RestCallBack<TR> callback) { RestPostAsync<TR, TI>(url, data, callback, DefaultConfiguration); } // ********* Asynchronous Post, not response expected ********* public static void RestPostNonQueryAsync<TI>(string url, TI data, RestCallBackNonQuery callback, ClientConfiguration configuration) { var post = new PostNonQueryDelegate<TI>(RestPostNonQuery); post.BeginInvoke(url, data, configuration, ar => { var result = (AsyncResult)ar; var del = (PostNonQueryDelegate<TI>)result.AsyncDelegate; Exception e = null; try { del.EndInvoke(result); } catch (Exception ex) { e = ex; } if (callback != null) { callback(e); } }, null); } // Overload method public static void RestPostNonQueryAsync<TI>(string url, TI data, RestCallBackNonQuery callback) { RestPostNonQueryAsync(url, data, callback, DefaultConfiguration); } } }
- “RestGetAsync”和“RestGetNonQueryAsync”方法将对 REST 服务进行 GET 调用。如果您只将数据发送到服务,但不期望从服务获得响应,您应该使用“RestGetNonQueryAsync”方法。
- “RestPostAsync”和“RestPostNonQueryAsync”方法将对 REST 服务进行 POST 调用。如果您只将数据发送到服务,但不期望从服务获得响应,您应该使用“RestPostNonQueryAsync”方法。
- 每个方法都有一个重载。如果调用时未向方法提供配置对象,则将使用默认配置。
调用这些方法中的任何一个时,您都需要提供一个回调函数。如果您期望从服务接收数据,您需要创建匹配“RestCallBack”委托的回调函数。否则,您需要创建匹配“RestCallBackNonQuery”委托的回调函数。根据本文,如果您在发布模式下编译代码,这些异步方法将不会抛出异常。如果在服务调用过程中遇到异常,异常将作为参数传递到回调函数中,您可以根据需要进行处理。如果没有遇到异常,则传递 null 给此参数。如果您在 Visual Studio 中以调试模式运行应用程序,您可能会看到应用程序因异常而中断,具体取决于您的 Visual Studio 环境设置。
其他 HTTP 方法呢?
您可能已经注意到,“LightCase”客户端库仅支持 GET 和 POST 方法。我是否应该添加对其他方法支持?理论上我应该,但实际上,GET 和 POST 方法可以满足交换服务和客户端数据的所有实际需求。根据我与网络工程师的经验,他们通常强烈反对 PUT 和 DELETE 等其他方法。这些方法不被认为是安全的方法。我们可能会争辩说,即使使用这些方法,我们也可以使我们的 REST 服务安全,但我们不能保证其他人也会使用这些方法来确保其服务的安全。在许多公司,这些方法在公司防火墙处被完全阻止。所以我们只有“GET”和“POST”选项。如果您发现您绝对需要支持其他 HTTP 方法,您可以自己扩展此库来支持它们。
现在我们完成了“LightCase” REST 服务客户端代理库,我们可以看看如何使用它的示例。
示例应用程序
为了演示如何使用“LightCase”客户端库,我创建了一个小型示例应用程序。附带的 Visual Studio 2010 解决方案包含三个项目。此示例将仅向您展示如何使用“LightCase”库发出 POST 调用。GET 调用更简单,可以类似地进行。
- “ShareLibraries”项目定义了一个供服务和客户端应用程序使用的类;
- “Service”项目实现了一个简单的 REST 服务;
- “Client”项目实现了该服务的客户端。
“ShareLibraries”中的“Student.cs”文件定义了一个简单的类。
using System; namespace ShareLibraries { public class Student { public int ID { get; set; } public string Name { get; set; } public int Score { get; set; } public DateTime EvaluationTime { get; set; } } }
“ShareLibraries”项目将被“Service”和“Client”项目引用,“Student”类将被服务和客户端用于交换数据。
REST 服务
实现 REST 服务有几种方法。在此示例中,我将使用我从这里学到的我最喜欢的方法。如果您想查看更多关于创建 REST 服务的不同方法的讨论,可以查看本文。“Service”项目是一个简单的 ASP.Net 项目。我将使用它来构建一个 REST 服务,而不是构建 ASP.Net 页面。服务实现在“StudentService.cs”文件中。
using System; using System.Collections.Generic; using System.ServiceModel; using System.ServiceModel.Activation; using System.ServiceModel.Web; using System.Threading; using ShareLibraries; namespace Service { [ServiceContract] [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] public class StudentService { [OperationContract] [WebInvoke(Method = "POST", UriTemplate = "StudentService/EvaluateStudents")] public List<Student> EvaluateStudents(List<Student> students) { var rand = new Random(); foreach (var student in students) { student.Score = 60 + (int)(rand.NextDouble() * 40); student.EvaluationTime = DateTime.Now; } Thread.Sleep(3 * 1000); return students; } } }
- “OperationContract”"EvaluateStudents" 接收来自客户端的“Student”对象列表,并为每个学生分配一个随机分数。
- 为了演示同步调用和异步调用之间的区别,添加了三秒钟的人工延迟。
我将使用此服务向您展示如何使用“LightCase”客户端库。为了使此服务正常工作,我们需要在“Global.asax”文件中添加以下映射:
void Application_Start(object sender, EventArgs e) { RouteTable.Routes.Add(new ServiceRoute("", new WebServiceHostFactory(), typeof(StudentService))); }
我们还需要在“Web.config”文件中添加以下配置:
<system.serviceModel> <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" /> <standardEndpoints> <webHttpEndpoint> <standardEndpoint name="" helpEnabled="true" automaticFormatSelectionEnabled="true" /> </webHttpEndpoint> </standardEndpoints> </system.serviceModel>
如果我们将“”项目设置为 Visual Studio 中的启动项目并运行它,当我们在浏览器中输入 URL "https://:62152/help" 时,我们将看到以下帮助页面:
此帮助页面显示了访问“EvaluateStudents” REST 方法的 URL。它还显示了访问“EvaluateStudents”方法的方法是POST。如此简单,我们就创建了一个功能齐全的 REST 服务。现在让我们编写一个使用“LightCase”客户端来消费此服务的客户端。
REST 服务客户端
“Client”项目是一个 Windows Forms 应用程序。为了使用“LightCase”客户端库,此应用程序引用了“LightCaseClient”项目创建的 DLL。它还引用了“LightCaseClient”DLL 引用的一些 .Net 程序集。
为了简单起见,此应用程序只有一个在“frmMain.cs”文件中实现的形式。以下是此 Windows 窗体的设计视图:
- 有三个按钮。一个按钮将触发对服务的同步调用。另一个将触发对服务的异步调用。“Click Me Anyway”按钮只是弹出一个消息框。我将使用此按钮来显示 UI 被同步调用阻塞,但不会被异步调用阻塞。
- “DataGridView”将在 REST 服务分配分数后显示学生列表。
此 Windows 窗体的代码隐藏文件如下:
using System; using System.Collections.Generic; using System.Configuration; using System.Windows.Forms; using LightCaseClient; using ShareLibraries; namespace Client { public partial class frmMain : Form { private List<Student> students; private string serviceUrl; public frmMain() { InitializeComponent(); // Get the service url serviceUrl = ConfigurationManager.AppSettings["ServiceUrl"]; // Initialize a list of students students = new List<Student>(); for(var i = 1; i <= 10; i++) { var student = new Student() { ID = i, Name = "Student Name No." + i.ToString() }; students.Add(student); } } private void btnSynchronousCall_Click(object sender, EventArgs e) { dgStudents.DataSource = null; dgStudents.Refresh(); // Make a synchronous POST Rest service call try { var evaluatedStudent = GenericProxies .RestPost<List<Student>, List<Student>>(serviceUrl, students); dgStudents.DataSource = evaluatedStudent; dgStudents.Refresh(); } catch (Exception ex) { MessageBox.Show("Failed to call the service - " + ex.Message); } } private void btnAsynchronousCall_Click(object sender, EventArgs e) { dgStudents.DataSource = null; dgStudents.Refresh(); // Make an asynchronous POST Rest service call GenericProxies.RestPostAsync<List<Student>, List<Student>>(serviceUrl, students, (ex, evaluatedStudent) => { if (ex != null) MessageBox.Show("Failed to call the service - " + ex.Message); else dgStudents.Invoke((Action) delegate { dgStudents.DataSource = evaluatedStudent; dgStudents.Refresh(); }); } ); } private void btnClickAnyway_Click(object sender, EventArgs e) { MessageBox.Show("I am clicked!"); } } }
- 在构造函数“frmMain”中,创建了一个“Student”对象的列表。
- 在“btnSynchronousCall_Click”方法中,对 REST 服务进行了同步调用。学生列表被 POST 到服务进行评估。当服务调用完成时,带有分数的学生会显示在 DataGridView 中。
- 在“btnAsynchronousCall_Click”方法中,使用异步 API 进行了相同的服务调用。评估后的学生在回调函数中分配给 DataGridView。为了简单起见,我使用了一个 Lambda 表达式来实现回调函数。在“LightCase”客户端进行异步服务调用时,我没有使用异常处理。如果调用过程中发生任何异常,异常对象将作为参数传递给 Lambda 回调函数。否则,此参数将为 null。
- “btnClickAnyway_Click”方法只是弹出一个消息框。我将使用此消息框向您展示 UI 被同步调用阻塞,但不会被异步调用阻塞。
如果您查看我之前的文章,您会发现我花费了相当多的重复步骤来完成 REST 调用。但在上面的示例中,通过使用“LightCase”客户端库,REST 调用变成了简单的函数调用。访问 REST 方法的 URL 保存在“App.config”文件中。
<appSettings> <add key="ServiceUrl" value="https://:62152/StudentService/EvaluateStudents"/> </appSettings>
运行示例应用程序
如果您将“Client”项目设置为启动项目,您可以在 Visual Studio 中调试运行示例应用程序。如果您点击“Synchronous Call”或“Asynchronous Call”按钮,在人工三秒延迟后,DataGridView 将用来自服务的数据填充。如果您在服务调用完成之前点击“Click Me Anyway”按钮,您会注意到 UI 被同步调用阻塞,但不会被异步调用阻塞。
关注点
- 本文介绍了一个通用的 REST 服务客户端库以及如何使用它的示例。
- “LightCase”客户端库仅支持 GET 和 POST 方法,这应该满足大多数实际需求。但如果您觉得需要使用其他方法,您可以自己扩展此库来添加对它们的支持。
- “LightCase”客户端库是“几乎”线程安全的。只要您在服务调用进行期间不对配置对象进行更改,而且我看不出您要这样做的理由,您就不会遇到线程安全问题。
- 如果进行了异步调用并发生异常,异常对象将作为参数传递给您的回调函数。您需要检查此对象并进行相应的处理。
- 我希望您喜欢我的文章,希望本文能以某种方式帮助您。
历史
首次修订 - 2012年2月19日。