使用浏览器调用服务是处理测试的一种简单方法。客户端常常使用JavaScript(这是JSON的优点)和.NET客户端。下面创建一个Console App(.NET Core)项目来调用服务。
BookServiceClientApp的示例代码使用了以下依赖项和名称空间:
依赖项
Microsoft.Extensions.DependencyInjection
Microsoft.Extensions.Loggingh.Console
Newtonsoft.Json
名称空间
Microsoft.Extensions.Logging
Newtonsoft.Json
System
System.Collections.Generic
System.Linq
System.Net.Http
System.Net.Http.Headers
System.Runtime.CompilerServices
System.Text
System.Threading.Tasks
System.Xml.Linq
1. 发送GET请求
要发送HTTP请求,应使用HttpClient类。在本章中,HttpClient类用来发送不同的HTTP请求。要使用HttpClient类,需要添加NuGet包System.Net.Http,打开名称空间System.Net.Http。要将JSON数据转换为.NET类型,应添加NuGet包Newtonsoft.Json。
为了把需要的所有URL放在一个地方,UrlService类为需要的URL定义了属性:
public class UrlService
{
public string BaseAddress => "https://localhost:5001";
public string BookaPI => "api/BookChapters/";
}
注意:
需要将UrlService类中的BaseAddress更改为服务的主机和端口号。当启动服务主机时,可以在浏览器中看到端口号。
在示例项目中,泛型类HttpClientService创建为对于不同的数据类型只有一种实现方式。构造函数需要通过DI获得UrlService,使用从UrlService中检索的基地址创建HttpClient:
public class HttpClientService<T>:IDisposable where T:class
{
private HttpClient _httpClient;
private readonly UrlService _urlService;
private readonly ILogger<HttpClientService<T>> _logger;
public HttpClientService(UrlService urlService,ILogger<HttpClientService<T>> logger)
{
_urlService = urlService ?? throw new ArgumentNullException(nameof(urlService));
_logger = logger ??throw new ArgumentNullException(nameof(logger));
_httpClient = new HttpClient();
_httpClient.BaseAddress = new Uri(urlService.BaseAddress);
}
public void Dispose()
{
_httpClient.Dispose();
}
}
注意:
在示例代码中,ILogger接口用于向控制台写入日志信息。
方法GetInternalAsync发出一个GET请求来接收一组项。该方法调用HttpClient的GetAsync方法来发送GET请求。HttpResponseMessage包含收到的信息。响应的状态码写入控制台来显示结果。如果服务器返回一个错误,则GetAsync方法不抛出异常。异常在方法EnsureSuccessStatus中抛出,该方法在返回的HttpResponseMessage实例上调用。如果HTTP状态码是错误类型,该方法就抛出一个异常。响应体包含返回的JSON数据。这个JSON信息读取为字符串并返回:
private async Task<string> GetInternalAsync(string requestUri)
{
if (string.IsNullOrEmpty(requestUri))
{
throw new ArgumentNullException(nameof(requestUri));
}
if (_objectDisposed)
{
throw new ObjectDisposedException(nameof(_httpClient));
}
HttpResponseMessage resp = await _httpClient.GetAsync(requestUri);
LogInformation($"status from GET {resp.StatusCode}");
resp.EnsureSuccessStatusCode();
return await resp.Content.ReadAsStringAsync();
}
private void LogInformation(string message,[CallerMemberName]string callerName = null)
{
_logger.LogInformation($"{nameof(HttpClientService<T>)}.{callerName}: {message}");
}
服务器控制器用GET请求定义了两个方法:一个方法返回所有章,另一个方法只返回一个章。但是需要章的标识符和URL。方法GetAllAsync调用GetInternalAsync方法,把返回的JSON信息转换为一个集合,而方法GetAsync将结果转换成单个项这些方法声明为虚拟的,允许在派生类中重写它们:
public async virtual Task<T> GetAsync(string requestUri)
{
if (string.IsNullOrEmpty(requestUri))
{
throw new ArgumentNullException(requestUri);
}
string json = await GetInternalAsync(requestUri);
return JsonConvert.DeserializeObject<T>(json);
}
public async virtual Task<IEnumerable<T>> GetAllAsync(string requestUri)
{
if (string.IsNullOrEmpty(requestUri))
{
throw new ArgumentNullException(nameof(requestUri));
}
string json = await GetInternalAsync(requestUri);
return JsonConvert.DeserializeObject<IEnumerable<T>>(json);
}
在客户端代码中不使用泛型类HttpClientService,而用BookChapterClientService类进行专门的处理。这个类派生于HttpClientService,为泛型参数传递BookChapter。这个类还重写了基类中的GetAllAsync方法,按章号给返回的章排序:
public class BookChapterClientService:HttpClientService<BookChapter>
{
public BookChapterClientService(UrlService urlService, ILogger<BookChapterClientService> logger)
: base(urlService, logger) { }
public override async Task<IEnumerable<BookChapter>> GetAllAsync(string requestUri)
{
IEnumerable<BookChapter> chapters = await base.GetAllAsync(requestUri);
return chapters.OrderBy(c => c.Number);
}
}
BookChapter类包含的属性是用JSON内容得到的:
public class BookChapter
{
public Guid Id { get; set; }
public int Number { get; set; }
public string Title { get; set; }
public int Page { get;set }
}
客户端应用程序的Main()方法调用不同的方法来显示GET、POST、PUT和DELETE请求,这些请求使用了SampleRequest类中的方法,在此之前,通过调用ConfigureService方法,注册用于DI的服务:
static async Task Main(string[] args)
{
Console.WriteLine("Client app, wait for service");
Console.ReadLine();
ConfgureService();
var test = ApplicationService.GetService<SampleRequest>();
//await test.ReadChaptersAsync();
//Console.WriteLine("enter the BookChapter Id:");
//await test.ReadChapterAsync(Console.ReadLine());
//await test.AddChapterAsync();
//await test.UpdateChapterAsync();
//await test.DeleteChapterAsync();
//await test.ReadXmlAsync();
await test.ReadNotExistingChapterAsync();
}
ConfigureService()方法在Microsoft.Extensions.DependencyInjection容器中注册所需的服务,并配置日志记录,写入控制台:
static void ConfgureService()
{
var services = new ServiceCollection();
services.AddSingleton<UrlService>();
services.AddSingleton<BookChapterClientService>();
services.AddSingleton<SampleRequest>();
services.AddLogging(logger=>
logger.AddConsole());
ApplicationService = services.BuildServiceProvider();
}
public static IServiceProvider ApplicationService { get; set; }
类SampleRequest实现了所有的示例方法来调用BookChapterClientService的方法。在构造函数中,注入UrlService和BookChapterClientService:
private readonly UrlService _urlService;
private BookChapterClientService _bookChapterClientService;
public SampleRequest(UrlService urlService,BookChapterClientService bookChapterClientService)
{
_urlService = urlService ?? throw new ArgumentNullException(nameof(urlService));
_bookChapterClientService = bookChapterClientService ?? throw new ArgumentNullException(nameof(bookChapterClientService));
}
ReadChaptersAsync()方法从BookChapterClientService中调用GetAllAsync()方法来检索所有章,并在控制台显示章节标题:
public async Task ReadChaptersAsync()
{
Console.WriteLine(nameof(ReadChapterAsync));
IEnumerable<BookChapter> chapters = await _bookChapterClientService.GetAllAsync(_urlService.BookApi);
foreach (var chapter in chapters)
{
Console.WriteLine($"{chapter.Id} {chapter.Title}");
}
}
运行应用程序(启动服务和客户端应用程序),ReadChaptersAsync()方法显示了OK状态码和章的标题:
ReadChapterAsync
info: BookServiceClientApp.Services.BookChapterClientService[0]
HttpClientService.GetInternalAsync: status from GET OK
143d2244-8535-449b-a5af-0cdbd4cc9a36 .NET Application Architectures
fa54304b-fb7e-448f-988c-2f3007705ccc WXG11111111111111111111
0e5449fe-936b-4cfb-a343-8cdcb0437ede Objects and Types
007dd49d-93ce-4d04-a9c1-417fddd2397f Object-Oriented Programming with C#
95ccc4c7-49a8-41ef-8041-c68681a6a247 Generics
24fa666c-ae56-4a33-a1c1-d8f8230bbbb0 Operators and Casts
bddc22e2-88b5-470b-87d3-0279fa062eeb Arrays
f89f6045-76e6-473c-afcf-69cb1f224136 Deelgates, Lambdas, and Events
ReadChapterAsync()方法显示了GET请求来检索单章。这样,这一章的标识符就添加到URI字符串中:
public async Task ReadChapterAsync(string Id)
{
Console.WriteLine(nameof(ReadChapterAsync));
//var chapters = await _bookChapterClientService.GetAllAsync(_urlService.BookApi);
//Guid id = chapters.First().Id;
var chapter = await _bookChapterClientService.GetAsync(_urlService.BookApi+Id);
Console.WriteLine($"{chapter.Id} {chapter.Title} {chapter.Number} {chapter.Page}");
ReadChapterAsync()方法的结果如下所示。它显示了两次OK状态,因为第一次是这个方法检索所有的章,之后发送对一章的请求:
ReadChapterAsync
info: BookServiceClientApp.Services.BookChapterClientService[0]
HttpClientService.GetInternalAsync: status from GET OK
0169ac24-0482-4716-82a2-f59e6de2e21f .NET Application Architectures
03d942d4-68f3-44eb-971d-222bf06545c7 WXG11111111111111111111
3fbe87bc-a9f1-4694-9b0a-ed1f351199be Objects and Types
2dcbe3d3-81f3-44fd-9764-b6f0d44865b1 Object-Oriented Programming with C#
7149757c-6635-4949-97c1-4a881d205707 Generics
bddd006a-1343-48bf-b162-5e76bf38494e Operators and Casts
cc479435-84b6-4864-96a3-8251f37526fb Arrays
b2291efd-1448-412c-b81f-021b5bf9f693 Deelgates, Lambdas, and Events
enter the BookChapter Id:
03d942d4-68f3-44eb-971d-222bf06545c7
ReadChapterAsync
info: BookServiceClientApp.Services.BookChapterClientService[0]
HttpClientService.GetInternalAsync: status from GET OK
03d942d4-68f3-44eb-971d-222bf06545c7 WXG11111111111111111111 2 0
如果不存在的章标识符发送GET请求,该怎么办?具体的处理如ReadNotExistingChapterAsync()方法所示。调用GetAsync()方法类似于前面的代码段,但会把不存在的标识符添加到URI。在HttpClient类的实现中,HttpClient帮助类(GetInternalAsync)中的GetAsync()方法不会抛出异常。然而,EnsureSuccessStatusCode会抛出异常。这个异常用HttpRequestException类型的Catch块捕获。在这里,使用了一个只处理异常码404(未找到)的异常过滤器:
public async Task ReadNotExistingChapterAsync()
{
Console.WriteLine(nameof(ReadNotExistingChapterAsync));
Guid requestedIdentifier = Guid.NewGuid();
try
{
var chapter = await _bookChapterClientService.GetAsync(_urlService.BookApi+requestedIdentifier);
Console.WriteLine($"{chapter.Id} {chapter.Title}");
}
catch (HttpRequestException ex) when (ex.Message.Contains("404"))
{
Console.WriteLine($"book chapter with the identifier {requestedIdentifier} not found.");
}
}
方法的结果显示了从服务返回的NotFound结果:
ReadNotExistingChapterAsync
info: BookServiceClientApp.Services.BookChapterClientService[0]
HttpClientService.GetInternalAsync: status from GET NotFound
book chapter with the identifier 2935788d-3624-4454-a441-3d708dbb4b5a not found
2. 从服务中接收XML
在前面"修改响应格式"小节中,XML格式被添加到服务中。将服务设置为返回XML和JSON,添加Accept标题值来接受application/xml内容,就可以显示地请求XML内容。
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().AddXmlSerializerFormatters();
services.AddControllers();
services.AddSingleton<IBookChaptersService, BookChaptersService>();
services.AddSingleton<SampleChapters>();
}
具体操作如下面的代码段所示。其中,指定application/xml的MediaTypeWithQualityHeaderValue被添加到Accept标题集合中。然后,结果使用XElement类解析为XML:
public async Task<XDocument> GetAllXmlAsync(string requestUri)
{
if (string.IsNullOrEmpty(requestUri))
{
throw new ArgumentNullException(nameof(requestUri));
}
_httpClient.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/xml"));
var resp = await _httpClient.GetAsync(requestUri);
LogInformation($"status fron Get {resp.StatusCode}");
resp.EnsureSuccessStatusCode();
string xml = await resp.Content.ReadAsStringAsync();
XDocument chapters = XDocument.Parse(xml);
return chapters;
}
在SampleRequest类中,调用GetAllXmlAsync()方法直接把XML结果写到控制台:
public async Task ReadXmlAsync()
{
Console.WriteLine(nameof(ReadXmlAsync));
var chapters = await _bookChapterClientService.GetAllXmlAsync(_urlService.BookApi);
Console.WriteLine(chapters);
}
运行这个方法,可以看到现在服务返回了XML:
Client app, wait for service
ReadXmlAsync
info: BookServiceClientApp.Services.BookChapterClientService[0]
HttpClientService.GetAllXmlAsync: status fron Get OK
<ArrayOfBookChapter xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<BookChapter>
<Id>935895cb-ff9d-493d-8f9e-5f13988b1e18</Id>
<Number>2</Number>
<Title>WXG11111111111111111111</Title>
<Pages>42</Pages>
</BookChapter>
<BookChapter>
<Id>afe51712-e4fb-4442-a30a-b1512a391b75</Id>
<Number>3</Number>
<Title>Objects and Types</Title>
<Pages>33</Pages>
</BookChapter>
<BookChapter>
<Id>cfad9fef-5ab8-4ac6-966b-8ce5e4ee3c02</Id>
<Number>4</Number>
<Title>Object-Oriented Programming with C#</Title>
<Pages>20</Pages>
</BookChapter>
<BookChapter>
<Id>78ed2e65-3869-4f0a-a548-80d4c461f331</Id>
<Number>1</Number>
<Title>.NET Application Architectures</Title>
<Pages>35</Pages>
</BookChapter>
<BookChapter>
<Id>78853842-02f9-40a0-b082-72fe180d76eb</Id>
<Number>8</Number>
<Title>Deelgates, Lambdas, and Events</Title>
<Pages>32</Pages>
</BookChapter>
<BookChapter>
<Id>27adca79-f5bd-4525-a120-983968a1f2f3</Id>
<Number>7</Number>
<Title>Arrays</Title>
<Pages>20</Pages>
</BookChapter>
<BookChapter>
<Id>f8bb4e80-941e-4072-832e-2cb3809bdd2b</Id>
<Number>6</Number>
<Title>Operators and Casts</Title>
<Pages>38</Pages>
</BookChapter>
<BookChapter>
<Id>23a474da-e0e2-4335-bb6a-dfade0c003ff</Id>
<Number>5</Number>
<Title>Generics</Title>
<Pages>24</Pages>
</BookChapter>
</ArrayOfBookChapter>
3. 发送POST请求
下面使用HTTP POST请求向服务发送新对象。HTTP POST请求的工作方式与GET请求类似。这个请求会创建一个新的服务器端对象。HttpClient类的PostAsync方法需要用第二个参数添加的对象。使用Json.NET的JsonConvert类把对象序列化为JSON。成功返回后,Headers.Location属性包含一个链接,其中,对象可以再次从服务中检索。响应还包含一个带有返回对象的响应体。在服务中修改对象时,Id属性在创建对象时在服务代码中填充。反序列化JSON代码后,这个新消息由PostAsync方法返回:
public async virtual Task<T> PostAsync(string requestUri,T item)
{
if (string.IsNullOrEmpty(requestUri))
{
throw new ArgumentNullException(nameof(requestUri));
}
if (item is null)
{
throw new ArgumentNullException(nameof(item));
}
if (_objectDisposed)
{
throw new ObjectDisposedException(nameof(_httpClient));
}
string json = JsonConvert.SerializeObject(item);
HttpContent content = new StringContent(json,Encoding.UTF8,"application/json");
var resp = await _httpClient.PostAsync(requestUri,content);
LogInformation($"status form POST {resp.StatusCode}");
resp.EnsureSuccessStatusCode();
LogInformation($"added resource at {resp.Headers.Location}");
json = await resp.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<T>(json);
}
在SampleRequest类中,可以看到添加到服务的章。调用BookChapterClient的PostAsync()方法后,返回的Chapter包含新的标识符:
public async Task AddChapterAsync()
{
Console.WriteLine(nameof(AddChapterAsync));
BookChapter chapter = new BookChapter
{
//Id = Guid.NewGuid(), //id可以直接在服务器中填充
Number = 34,
Title = "ASP.NET Core Web API",
Page=35
};
chapter = await _bookChapterClientService.PostAsync(_urlService.BookApi,chapter);
Console.WriteLine($"added chapter {chapter.Id} {chapter.Title}");
}
AddChapterAsync()方法的结果显示了创建对象的一次成功运行:
Client app, wait for service
AddChapterAsync
info: BookServiceClientApp.Services.BookChapterClientService[0]
HttpClientService.PostAsync: status form POST Created
info: BookServiceClientApp.Services.BookChapterClientService[0]
HttpClientService.PostAsync: added resource at https://localhost:5001/api/BookChapters/8d16925f-5119-4d6c-8599-257d4eedb2cb
added chapter 8d16925f-5119-4d6c-8599-257d4eedb2cb ASP.NET Core Web API
4. 发送PUT请求
HTTP PUT请求用于更新记录,使用HttpClient方法PutAsync()来发送。PutAsync()需要第二个参数中的更新内容和第一个参数中服务的URL,其中包括标识符:
public async virtual Task PutAsync(string requestUri,T item)
{
if (string.IsNullOrEmpty(requestUri))
{
throw new ArgumentNullException(nameof(requestUri));
}
if (item is null)
{
throw new ArgumentNullException(nameof(item));
}
if (_objectDisposed)
{
throw new ObjectDisposedException(nameof(_httpClient));
}
string json = JsonConvert.SerializeObject(item);
HttpContent content = new StringContent(json,Encoding.UTF8,"application/json");
var resp = await _httpClient.PutAsync(requestUri,content);
LogInformation($"status form PUT {resp.StatusCode}");
resp.EnsureSuccessStatusCode();
}
在SampleRequest类中,章“WXG”更新为另一个标题"WXG! Hello World!":
public async Task UpdateChapterAsync()
{
Console.WriteLine(nameof(UpdateChapterAsync));
var chapters = await _bookChapterClientService.GetAllAsync(_urlService.BookApi);
var chapter = chapters.SingleOrDefault(c=>c.Title == "WXG");
if (chapter != null)
{
chapter.Title = "WXG! Hello World!";
await _bookChapterClientService.PutAsync(_urlService.BookApi+chapter.Id,chapter);
Console.WriteLine($"updated chapter {chapter.Title}");
}
}
UpdateChapterAsync()方法的控制台输出显示了HTTP NoCootent结果和更新的章标题:
Client app, wait for service
UpdateChapterAsync
info: BookServiceClientApp.Services.BookChapterClientService[0]
HttpClientService.GetInternalAsync: status from GET OK
info: BookServiceClientApp.Services.BookChapterClientService[0]
HttpClientService.PutAsync: status form PUT NoContent
updated chapter WXG! Hello World!
5. 发送DELETE请求
示例客户端的最后一个请求是HTTP DELETE请求。调用HttpClient类的GetAsync、PostAsync和PutAsync后,显然发送DELETE请求的方法是DeleteAsync。在下面的代码段中,DeleteAsync()方法只需要一个URI参数来识别要删除的对象:
public async Task DeleteAsync(string requestUri)
{
if (string.IsNullOrEmpty(requestUri))
{
throw new ArgumentNullException(nameof(requestUri));
}
var resp = await _httpClient.DeleteAsync(requestUri);
LogInformation($"status form DELETE {resp.StatusCode}");
resp.EnsureSuccessStatusCode();
}
SampleRequest类定义了RemoveChapterAsync()方法:
public async Task RemoveChapterAsync()
{
Console.WriteLine(nameof(RemoveChapterAsync));
var chapters = await _bookChapterClientService.GetAllAsync(_urlService.BookApi);
var chapter = chapters.SingleOrDefault(c=>c.Title == "WXG");
if (chapter != null)
{
await _bookChapterClientService.DeleteAsync(_urlService.BookApi+chapter.Id);
Console.WriteLine($"removed chapter {chapter.Title}");
}
}
运行程序时,RemoveChapterAsync()方法首先显示了 HTTP GET 方法的状态,因为先是发出GET请求来检索所有的章,然后发出DELETE请求来删除"WXG"章节后并返回 DELETE OK 状态:
Client app, wait for service
RemoveChapterAsync
info: BookServiceClientApp.Services.BookChapterClientService[0]
HttpClientService.GetInternalAsync: status from GET OK
removed chapter WXG
info: BookServiceClientApp.Services.BookChapterClientService[0]
HttpClientService.DeleteAsync: status form DELETE OK