4. 创建.NET客户端

本文档详述了如何在.NET Core中创建一个Console App客户端,利用HttpClient类发送HTTP请求,包括GET、POST、PUT、DELETE。同时展示了如何处理XML响应,以及依赖注入和服务配置。示例代码使用了HttpClient、Newtonsoft.Json等库,并通过DI管理和日志记录。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

使用浏览器调用服务是处理测试的一种简单方法。客户端常常使用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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值