# HttpAgent Documentation (full text) > This file is generated automatically by the site build pipeline and contains all documentation and blog content. Index: https://http.furion.net/en/llms.txt --- # 1.1 HTTP Remote Request Overview > Source: https://http.furion.net/en/docs/getting-started/intro/ An `HTTP` remote request refers to the process by which a client (such as a `Web` browser, mobile application, and so on) sends a request to a remote server over the `HTTP` protocol to obtain the required resources. It is one of the most fundamental and core communication methods in modern internet applications. ![httpagent](/images/httpagent.jpg) [**View the high-resolution architecture diagram**](https://github.com/monksoul/HttpAgent/blob/master/drawio/HttpAgent.drawio) ### Application Scenarios `HTTP` remote requests are widely used in internet application systems, covering the following main scenarios: - **Resource retrieval**: Retrieving internet resources from servers, such as web pages, images, videos, and so on. - **Data crawling**: Used by web crawlers to crawl page data or perform data analysis. - **File transfer**: Supporting file upload and download operations. - **API integration**: Interacting with third-party `API` interfaces for data exchange. - **System integration**: Enabling interconnection and interoperability between heterogeneous systems. - **Configuration management**: Used for dynamic configuration retrieval and updates in configuration centers. - **Microservice communication**: Supporting inter-service calls in microservice architectures. - **Load balancing**: Achieving resource optimization and high availability through request distribution. - **Stress testing**: Used to simulate high-concurrency requests for system performance testing. - **Request proxying**: Implementing request proxying and forwarding to support cross-origin or secure access. - **Other scenarios**: Applicable to various scenarios that require remote communication. `HTTP` remote requests provide internet applications with efficient and flexible communication capabilities, and are an important technical foundation for building distributed systems and realizing data interaction. --- # 1.2 Installation & Service Registration > Source: https://http.furion.net/en/docs/getting-started/installation/ > **Package Installation** The `Furion` framework already includes this feature out of the box, so no additional `NuGet` package installation is required. If you are not using the `Furion` framework, you can install the `HttpAgent` or `HttpAgent.AspNetCore` package with the following commands: - For any `.NET/C#` application: ```bash showLineNumbers dotnet add package HttpAgent ``` - For `Web` applications (includes `HttpAgent` and provides `HttpContext` forwarding): ```bash showLineNumbers dotnet add package HttpAgent.AspNetCore ``` Before making `HTTP` remote requests, you need to register and configure the `HttpRemote` service in the `Startup.cs` or `Program.cs` file. ```cs showLineNumbers {2,5} // Register in Startup.cs: services.AddHttpRemote(); // In Program.cs, register as follows: // builder.Services.AddHttpRemote(); ``` > **Resolving the `AddHttpRemote` ambiguity error** If you encounter an ambiguity error for the `AddHttpRemote` method, you can resolve it by adding an empty delegate parameter, as shown below: ```cs showLineNumbers services.AddHttpRemote(builder => {}); ``` Then, inject the `IHttpRemoteService` service into your services, controllers, or any class that supports dependency injection. ```cs showLineNumbers {3,5} public class YourService { private readonly IHttpRemoteService _httpRemoteService; public YourService(IHttpRemoteService httpRemoteService) { _httpRemoteService = httpRemoteService; } } ``` If you are using `.NET 8` or later, you can simplify the code by injecting via [primary constructor](https://learn.microsoft.com/zh-cn/dotnet/csharp/whats-new/tutorials/primary-constructors): ```cs showLineNumbers {1} public class YourService(IHttpRemoteService httpRemoteService) { // Use the httpRemoteService variable } ``` Alternatively, you can also inject it on demand in a specific method: ```cs showLineNumbers {3} public class YourService { public Task GetResource([FromServices] IHttpRemoteService httpRemoteService) { // Your code logic here } } ``` > **Usage without a dependency injection environment** In `.NET Core` application development, it is recommended to build applications using dependency injection. Therefore, where conditions permit, it is recommended to prefer dependency injection for managing services. However, in certain specific scenarios, such as console applications (`Console`), `WinForms`, or `WPF` projects, `.NET` does not integrate a complete dependency injection container by default. In such cases, you can use the following two approaches to manually obtain the required services: - `Furion` framework If the project uses the `Furion` framework, you can inject the root container into `HttpRemoteClient` at startup. Afterwards, you can make `HTTP` requests through the `HttpRemoteClient.Service` static property: ```cs showLineNumbers var app = builder.Build().UseHttpRemoteClient(); // Inject the root container into HttpRemoteClient ``` ```cs showLineNumbers var result = await HttpRemoteClient.Service.GetAsStringAsync("https://furion.net/"); ``` You can also directly resolve the `HTTP` remote request service and send requests: ```cs showLineNumbers var httpRemoteService = App.GetRequiredService(); ``` - Other projects (`Console/WinForms/WPF`) For ordinary `Console/WinForms/WPF` projects, you can use the `Service` property provided by the `HttpRemoteClient` static class to make remote `HTTP` requests: ```cs showLineNumbers var result = await HttpRemoteClient.Service.GetAsStringAsync("https://furion.net/"); ``` **Usage recommendation: The above two approaches should be used as supplementary means to the dependency injection mechanism, not as a replacement. In environments that support dependency injection, please follow standard dependency injection practices to build and manage application services as much as possible.** > For detailed configuration of `HttpRemoteClient`, custom service registration, and how to integrate an external dependency injection container with the static class, see **8.16 Using in a non-dependency-injection environment (`Console/WinForms/WPF`)**. --- # 1.3 Using with Claude Code / Codex (llms.txt) > Source: https://http.furion.net/en/docs/getting-started/ai-assistants/ This website provides two AI-friendly files, `llms.txt` and `llms-full.txt`, so AI assistants (such as `Claude Code` and `Codex`) can read the entire documentation in one shot instead of crawling pages one by one. - `llms.txt`: a full index of the documentation (title + link + one-line summary); - `llms-full.txt`: the complete documentation content in a single file; - English edition: `https://http.furion.net/en/llms.txt` and `https://http.furion.net/en/llms-full.txt`. > **Why `llms-full.txt` is recommended** `llms-full.txt` contains all the content, so an AI can master the whole framework with a single read instead of crawling page by page. **1. Using with `Claude Code`** Option one: let `Claude` read it online: ```bash showLineNumbers # Type this directly in a Claude Code session: # First read https://http.furion.net/en/llms.txt, then answer any HttpAgent questions ``` Option two: download it locally and reference it from `CLAUDE.md` for offline use: ```bash showLineNumbers curl https://http.furion.net/en/llms-full.txt -o .claude/httpagent-docs.txt ``` Add the following to the project root `CLAUDE.md`: ```md showLineNumbers # HttpAgent usage - For any HttpAgent question, read .claude/httpagent-docs.txt before answering. ``` **2. Using with `Codex`** Download the docs and reference them from `AGENTS.md`: ```bash showLineNumbers curl https://http.furion.net/en/llms-full.txt -o docs/httpagent-llms-full.txt ``` Add the following to `AGENTS.md`: ```md showLineNumbers # HttpAgent - Read docs/httpagent-llms-full.txt before writing any HttpAgent-related code. - Official documentation index: https://http.furion.net/en/llms.txt ``` > **Tips** - Chinese environment: use `https://http.furion.net/llms.txt`; English environment: use `https://http.furion.net/en/llms.txt`; - Re-run the `curl` command to refresh your local copy after the docs are updated. --- # 1.4 Comparison with Other HTTP Client Libraries > Source: https://http.furion.net/en/docs/getting-started/comparison/ `HttpAgent`, [`Refit`](https://github.com/reactiveui/refit) and [`RestSharp`](https://github.com/restsharp/RestSharp) are all popular HTTP client solutions in the .NET ecosystem, each with a different focus: `Refit` champions "interface as API" declarative programming, `RestSharp` is the classic, battle-tested fluent client, and `HttpAgent` is the **full-featured** option — beyond everyday requests it ships `SSE`, `WebSocket`, long polling, stress testing and the `Profiler` traffic-analysis engine out of the box, with **zero third-party dependencies**. > **Bottom line first** All three are excellent open-source projects with their own strengths — there is no absolute winner. This comparison is based on their public documentation and typical usage and aims to be objective and fair: they all build on `HttpClient` under the hood and handle ordinary REST calls equally well. The real differences lie in **debugging experience** and **capability boundaries**. ### Typical usage side by side The same "fetch a list of users" in each library: **`HttpAgent`: builder or predicate style — your choice** ```cs showLineNumbers // Option 1: fluent builder var users = await httpRemoteService.SendAsAsync>( HttpRequestBuilder.Get("https://api.example.com/users")); // Option 2: request predicate (syntactic sugar) var users = await httpRemoteService.GetAsAsync>("https://api.example.com/users"); ``` **`Refit`: interface as API** ```cs showLineNumbers public interface IUsersApi { [Get("/users")] Task> GetUsersAsync(); } var api = RestService.For("https://api.example.com"); var users = await api.GetUsersAsync(); ``` **`RestSharp`: classic `RestClient` + `RestRequest`** ```cs showLineNumbers var client = new RestClient("https://api.example.com"); var request = new RestRequest("/users"); var users = await client.GetAsync>(request); ``` Notice that `Refit` requires an interface contract up front, while `RestSharp` and `HttpAgent` are write-as-you-go fluent styles — and `HttpAgent` supports both. ### Common REST capabilities | Capability | `HttpAgent` | `Refit` | `RestSharp` | | --- | --- | --- | --- | | Standard verbs (`GET`/`POST`/`PUT`/`DELETE`, …) | ✅ 9 verbs | ✅ interface attributes | ✅ `RestRequest` | | Fluent builder | ✅ `HttpRequestBuilder` | ❌ (interface-based, no builder) | ✅ `RestClient` + `RestRequest` | | Declarative interface (proxy) | ✅ declarative proxy | ✅ (core design) | ❌ | | Query params / headers / body | ✅ | ✅ | ✅ | | `JSON`/`XML` serialization | ✅ built-in content processors | ✅ `System.Text.Json` (replaceable) | ✅ pluggable serializers | | Multipart form upload | ✅ `MultipartFile.CreateFromPath` | ✅ `StreamPart` etc. | ✅ `.AddFile(...)` | | File upload / download (with progress) | ✅ built-in progress output | ✅ streaming responses | ✅ `DownloadDataAsync` etc. | | Authentication | ✅ `Bearer`/`Basic`/`Digest`/`Access Token` auto-management + `JWT` utilities | ✅ `[Authorize]` custom handlers | ✅ `OAuth1/OAuth2/JWT/NTLM` authenticators | | Retry policy | ✅ built-in retry + quota policy | ❌ (bring your own `Polly`) | ✅ official `Polly` integration | | Timeout | ✅ built-in | ✅ via `HttpClientHandler` | ✅ `client.Timeout` | | Redirect handling | ✅ built-in (configurable delegate) | ✅ client follows by default | ✅ configurable | | `HTTP` proxy | ✅ built-in | ✅ via `HttpMessageHandler` | ✅ via `HttpMessageHandler` | | Automatic decompression (`gzip`/`deflate`/`brotli`/`zstd`) | ✅ built-in | ✅ via `HttpMessageHandler` | ✅ via `HttpMessageHandler` | | `Cookie` management | ✅ built-in | ❌ (handle it yourself) | ✅ built-in | | Exception handling | ✅ unified exceptions + suppression | ✅ `ApiException` | ✅ built-in exceptions | | Cancellation (`CancellationToken`) | ✅ | ✅ | ✅ | ### Capabilities unique to `HttpAgent` (not built into the other two) | Capability | `HttpAgent` | `Refit` | `RestSharp` | | --- | --- | --- | --- | | `SSE` (`Server-Sent Events`, incl. `IAsyncEnumerable`) | ✅ built-in | ❌ | ❌ | | `WebSocket` duplex communication | ✅ built-in | ❌ | ❌ | | Polling (standard + long polling, configurable interval) | ✅ built-in | ❌ | ❌ | | `Profiler` traffic-analysis engine (capture & visualize requests/responses) | ✅ industrial-grade, built-in | ❌ | ❌ | | Stress / performance / simulation testing (configurable concurrency & iterations, reports) | ✅ built-in | ❌ | ❌ | | Request assertions (pre-send / post-response checks) | ✅ `Asserts(...)` | ❌ | ❌ | | Content processor / converter dual pipeline (pluggable) | ✅ built-in | ❌ | ❌ | | `MessagePack` serialization | ✅ built-in | ❌ | ❌ | | `WebService` / `SOAP` support (incl. `SOAPAction`) | ✅ built-in | ❌ | ❌ | | `OData` client (`$filter`/`$select`/`$expand`) | ✅ built-in | ❌ | ❌ | | Request proxying & forwarding (microservice integration) | ✅ built-in | ❌ | ❌ | | Request log auditing (custom `Logger`) | ✅ built-in | ❌ | ❌ | | Website "Workshop" visual code generator | ✅ (one-click builder/verb/`cURL`/declarative/`JSON` code) | ❌ | ❌ | | Website "HttpAgent Assistant" AI Q&A | ✅ (grounded in the official docs; select any text on the page to ask the AI; key stays local) | ❌ | ❌ | | `cURL` command import | ✅ built-in (visual generator on the website Workshop) | ❌ | ❌ | | `JSON` config import | ✅ built-in (`HttpRequestBuilder.FromJson()`) | ❌ | ❌ | | `ETag` cache / request quota | ✅ built-in | ❌ | ❌ | | `HttpContext` forwarding | ✅ built-in | ❌ | ❌ | | `MCP` message content | ✅ built-in | ❌ | ❌ | | `Mock` simulation testing | ✅ built-in | ❌ | ❌ | | Third-party dependencies | zero | `System.Text.Json` etc. | serializers etc. | ### Which one should you pick? - **A few REST endpoints and an interface-contract style** → `Refit` is a great fit; - **Legacy projects, ecosystem maturity and a huge body of community knowledge** → `RestSharp` is a safe bet; - **The full "request + debug + stress test + realtime" toolbox without assembling multiple libraries** → `HttpAgent` is the ideal choice: zero dependencies, built-in `Profiler`, and `SSE`/`WebSocket`/long polling/stress testing/declarative requests all ready to use — plus the website's [Workshop (visual code generator)](/workshop/) and the "HttpAgent Assistant" AI Q&A to lower the learning and debugging curve. > **Two companion tools on the website** Beyond the library itself, the website offers two companion tools that neither `Refit` nor `RestSharp` has: - [**Workshop**](/workshop/): a visual code generator — configure a request online and generate builder, verb, `cURL`, declarative-interface and `JSON` code with one click, perfect for getting started; - **HttpAgent Assistant**: the AI Q&A widget at the bottom-right of the site, grounded entirely in the official docs (with an optional full-docs mode). Select any text on any page to ask the AI about it on the spot; bring your own `DeepSeek` API key to ask questions in real time — the key never leaves your browser. > **An honest reminder** `Refit` and `RestSharp` keep evolving too — always check their official docs for the latest capabilities before deciding. And if your project already uses one of them deeply without pain points, there is no need to migrate for its own sake. Tools serve productivity: the right one is the one that fits. --- # 1.5 About the Author > Source: https://http.furion.net/en/docs/getting-started/about-author/ > **Self-Appreciation** However many people you have met, none of them was ever quite like me. ### Online Aliases - **百小僧** (Bai Xiaoseng) - MonkSoul - ~~新生帝~~ (Xinshengdi) ### Avatar ![furionlogo](/images/furionlogo.png) This avatar was designed by me on July 14, 2016. ### Personal Signatures > 2012.06.29 > Success comes from operations, failure from management, and mistakes from not learning. > > 2024.07.08, updated to: > Thinking only brings difficulties; doing brings answers. > > 2025.02.17, updated to: > **A decade in a dream, a dream in a decade. Now I know I am who I am.** ### Devotion Manifesto Selfless dedication is not a fantasy — sometimes, we can do it too. ### Why I Open Source Open source is like one's face: you can tell good from bad at a glance. Flaws may draw ridicule and criticism; merits win praise and respect. Don't worry — they are shaping a better you. ### Interests & Hobbies Always curious about new technologies, devoted to open source, and happy to share technical insights; fascinated by tattoo culture, keen on tech gadgets, playing handheld consoles in spare time, and occasionally relaxing in *CrossFire* and *Minecraft*. Often found on OSChina, CNBlogs, Zhihu, ITHome, GitHub and Gitee, also browsing Douyin, Bilibili, anime and American dramas. ### Personal Pages - Gitee: [https://gitee.com/monksoul](https://gitee.com/monksoul) - GitHub: [https://github.com/monksoul](https://github.com/monksoul) ### Technical Skills Since my first encounter with programming in 2008, I have spent more than a decade getting to know the surface of mainstream internet technologies, with `C#` and `JavaScript` as my most familiar languages. ### Favorite Tools `Visual Studio 2026`, `JetBrains Rider`, `Vim/NeoVim`, `Visual Studio Code`, `PostgreSQL`. ### Fields of Interest Always passionate about software engineering, architecture design, low-level principles, algorithms, embedded systems/MCUs, and network programming. --- # 2.1 Getting Website Content > Source: https://http.furion.net/en/docs/quick-start/get-content/ Getting website content is a common requirement, for example getting the homepage content of the `Furion` framework website (`https://furion.net`). The following shows several ways to achieve this using `httpRemoteService`. ```cs showLineNumbers // Get the string content directly var content = await httpRemoteService.GetAsStringAsync("https://furion.net"); ``` In addition to the above method, the following approaches are also supported: **1. Using the builder approach ✅** - Get the string-type content directly: ```cs showLineNumbers var content = await httpRemoteService.SendAsStringAsync(HttpRequestBuilder.Get("https://furion.net")); // var content = await httpRemoteService.SendAsStringAsync(HttpBuilder.Get("https://furion.net")); // HttpBuilder can be used instead of HttpRequestBuilder ``` - Specify the string type via generics: ```cs showLineNumbers var content = await httpRemoteService.SendAsAsync(HttpRequestBuilder.Get("https://furion.net")); ``` - Get the `HttpRemoteResult` type and extract the result from it: ```cs showLineNumbers {1} var result = await httpRemoteService.SendAsync(HttpRequestBuilder.Get("https://furion.net")); var content = result.Result; ``` - Get the `HttpResponseMessage` type and read its content: ```cs showLineNumbers {1} var httpResponseMessage = await httpRemoteService.SendAsync(HttpRequestBuilder.Get("https://furion.net")); var content = await httpResponseMessage.Content.ReadAsStringAsync(); ``` **2. Using the request verb approach** - Specify the string type via generics and get it directly: ```cs showLineNumbers {1,4,7} var content = await httpRemoteService.GetAsAsync("https://furion.net"); // Configure HttpRequestBuilder // var content = await httpRemoteService.GetAsAsync("https://furion.net", builder => builder.Profiler()); // ✅ Syntactic sugar: HttpRequestBuilder.Setup or HttpBuilder.Setup can be used instead of the builder => builder syntax // var content = await httpRemoteService.GetAsAsync("https://furion.net", HttpBuilder.Setup.Profiler()); ``` - Get the `HttpRemoteResult` type and extract the result from it: ```cs showLineNumbers {1} var result = await httpRemoteService.GetAsync("https://furion.net"); var content = result.Result; ``` - Get the `HttpResponseMessage` type and read its content: ```cs showLineNumbers {1} var httpResponseMessage = await httpRemoteService.GetAsync("https://furion.net"); var content = await httpResponseMessage.Content.ReadAsStringAsync(); ``` These approaches offer flexible options, so you can choose the method best suited to your specific needs to get website content. --- # 2.2 Sending Request Data > Source: https://http.furion.net/en/docs/quick-start/with-data/ When retrieving data from third-party `API`s, you usually need to carry request data, which can be `URL` address parameters or request content. The most common approaches are passing parameters through the `URL` address and sending `JSON`-formatted data. ```cs showLineNumbers {3-4} var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddModel", builder => builder .WithQueryParameters(new { query1 = 1, query2 = "furion" }) // Set URL query parameters .SetJsonContent(new { id = 1, name = "furion" })); // Set the request's JSON content ``` In addition to the above approach, the following methods are also supported: ```cs showLineNumbers {3-5} // Using the builder pattern var content = await httpRemoteService.SendAsAsync(HttpRequestBuilder.Post("https://localhost:7044/HttpRemote/AddModel") .WithQueryParameter("query1", 1) // Set query parameters (supports individual setting) .WithQueryParameter("query2", "furion") // Set query parameters (supports individual setting) .SetJsonContent("{\"id\":1,\"name\":\"furion\"}")); // Set request content (supports passing a JSON string directly) // For more detailed usage, see section 2.1 ``` In addition, you can use the `SetContent` method to set request content, which supports setting any type of request content. In fact, the `SetJsonContent` method is also implemented internally by calling `SetContent`. ```cs showLineNumbers {5,11,17} // Custom Content-Type var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddModel", builder => builder .WithQueryParameters(new { query1 = 1, query2 = "furion" }) // Set query parameters .SetContent(new { id = 1, name = "furion" }, "application/json")); // Set request content // Custom Content-Type supports configuring Charset var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddModel", builder => builder .WithQueryParameters(new { query1 = 1, query2 = "furion" }) // Set query parameters .SetContent(new { id = 1, name = "furion" }, "application/json;charset=utf-8")); // Set request content // Custom Content-Type supports configuring request encoding var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddModel", builder => builder .WithQueryParameters(new { query1 = 1, query2 = "furion" }) // Set query parameters .SetContent(new { id = 1, name = "furion" }, "application/json;charset=utf-8", Encoding.UTF8)); // Set request content ``` --- # 2.3 Form Form Submission (URL Encoding) > Source: https://http.furion.net/en/docs/quick-start/form-urlencoded/ In web applications, the most common way to save user-defined data is through `Form` submission. A `Form` can carry not only text data but also binary data, such as files. ```cs showLineNumbers {2} var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddForm?id=1", builder => builder.SetMultipartContent(multipart => multipart // Set multipart form content .AddJson(new { id = 1, name = "furion" }) // Set JSON data .AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file"))); // Set a file (supports stream, byte array, remote URL, and Base64 string forms) ``` **The `SetMultipartContent` method is specifically used to set the request content type to `multipart/form-data` form data**, and it provides a rich set of extension options, including: - `Boundary` or `SetBoundary(boundary)`: Sets the multipart form content boundary. - `AddJson(rawJson)`: Adds `JSON` content. - `AddFormItem(value, name)`: Adds a single form item content. - `AddHtml(htmlString, name)`: Adds `HTML` content. - `AddXml(xmlString, name)`: Adds `XML` content. - `AddText(text, name)`: Adds text content. - `AddObject(rawObject, name)`: Adds object content. - `AddFileFromRemote(url, name)`: Adds internet file content. - `AddFileFromBase64String(base64String, name, fileName)`: Adds `Base64` string file content. - `AddFileAsStream(path, name)`: Adds a local file as stream content. - `AddFileWithProgressAsStream(path, channel, name)`: Adds a local file as stream content (with file transfer progress). - `AddFileAsByteArray(path, name)`: Adds a local file as byte array content. - `AddFile(multipartFile, name)`: Adds `MultipartFile` file content. - `AddFile(fileInfo, name)`: Adds `FileInfo` file content. - `AddFile(IFormFile)`: Adds `IFormFile` file content. - `AddFiles(IFormFileCollection)`: Adds `IFormFileCollection` file content. - `AddFile(IBrowserFile)`: Adds `IBrowserFile` file content. - `AddFiles(IEnumerable)`: Adds multiple `IBrowserFile` file content. - `AddStream(stream, name)`: Adds binary stream content. - `AddByteArray(byteArray, name)`: Adds binary byte array content. - `Add(httpContent)`: Adds `HttpContent` content. These are only the commonly used methods; `SetMultipartContent` provides even more flexibility. In addition to the approaches above, the following methods are also supported: ```cs showLineNumbers {3-5} // Using the builder pattern var content = await httpRemoteService.SendAsAsync(HttpRequestBuilder.Post("https://localhost:7044/HttpRemote/AddForm?id=1") .SetMultipartContent(multipart => multipart // Set multipart form content .AddJson(new { id = 1, name = "furion" }) // Set JSON data .AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file"))); // Set a file (supports stream, byte array, remote URL, and Base64 string forms // For more detailed usage, see Section 2.1 ``` The following are some common examples of `Form` submission: ```cs showLineNumbers {4,5,7,9,11} var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddForm?id=1", builder => builder.SetMultipartContent(multipart => multipart // Set multipart form content .AddJson(new { id = 1, name = "furion" }) // Set JSON data .AddFormItem("age", "Age") // Supports setting a single value .AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file") // Set a single file (corresponds to the form's File field) // Supports internet file URLs .AddFileFromRemote("https://furion.net/img/furionlogo.png", "files") // Set multiple files (corresponds to the form's Files field) // Supports reading a local file as a byte array .AddFileAsByteArray(@"C:\Workspaces\httptest.jpg", "files")); // Set multiple files (corresponds to the form's Files field) // Add a MultipartFile file .AddFile(MultipartFile.CreateFromPath(@"C:\Workspaces\httptest.jpg"))); ``` > **Special Note** If you use the `SetContent` method to set the request content type to `multipart/form-data` and the content is not a `MultipartContent` instance, a `NotSupportedException` will be thrown. The exception message is as follows: ```cs showLineNumbers The method does not support setting the request content type to `multipart/form-data`. Please use the `SetMultipartContent` method instead. If you are using an HTTP declarative requests, define the parameter with the `Action` type or annotate the parameter with the `MultipartAttribute`. ``` **When you need to set the request content type to `multipart/form-data`, use the `SetMultipartContent` method correctly rather than `SetContent`.** - **`URL`-Encoded Form** In addition to the multipart `multipart/form-data` form request, another common request type is `application/x-www-form-urlencoded`, which sends data in `URL`-encoded form. This type of form is characterized by all special characters being `URL`-encoded, and it is typically used for simple form submission scenarios that do not require uploading binary data such as files. The following example shows how to build form data that conforms to the `application/x-www-form-urlencoded` submission type. ```cs showLineNumbers {3,8} var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddURLForm", builder => builder .SetFormUrlEncodedContent(new { id = 1, name = "furion" })); // Set application/x-www-form-urlencoded request content // Supports URL-encoded string format var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddURLForm", builder => builder .SetFormUrlEncodedContent("id=1&name=furion", useStringContent: true); ``` > **Notes on `URL`-Encoded Form Content** - **By default, the `URL`-encoded form is built using the [`FormUrlEncodedContent`](https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Net.Http/src/System/Net/Http/FormUrlEncodedContent.cs#L44) type, but this type does not support custom request content encoding and uses `Encoding.Latin1` instead of `UTF-8` by default.** This may cause exceptions when submitting to certain endpoints. To solve this problem, you can set the `useStringContent` parameter to `true` to build the form data using the `StringContent` approach, thereby allowing custom encoding to `UTF-8`. ```cs showLineNumbers {3} var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddURLForm", builder => builder .SetFormUrlEncodedContent(new { id = 1, name = "furion" }, useStringContent: true)); ``` - Some servers require an explicit charset declaration (`charset`); in this case, you can specify the encoding via the `contentEncoding` parameter, for example using `UTF-8`: ```cs showLineNumbers {3} var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddURLForm", builder => builder .SetFormUrlEncodedContent(new { id = 1, name = "furion" }, Encoding.UTF8)); ``` When sending the remote request, this setting generates the following `Content-Type` request header: `application/x-www-form-urlencoded; charset=UTF-8`. --- # 2.4 Downloading Network Resources > Source: https://http.furion.net/en/docs/quick-start/download/ One of the most common application scenarios for `HTTP` remote requests is downloading network resources and saving them to the local disk, including downloading web page content, images, archives, and installation software. The following example shows how to download the `ASP.NET Core` runtime: ```cs showLineNumbers {3} // Download the ASP.NET Core runtime from the specified URL and save it to the C:\Workspaces\ directory // If no file name is specified, the framework automatically resolves the file name from the download URL, for example: aspnetcore-runtime-8.0.10-win-x64.exe var fileTransferResult = await httpRemoteService.DownloadFileAsync("https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe" , @"C:\Workspaces\"); // To specify a file name, set it to C:\Workspaces\aspnetcore-runtime.exe ``` > **Notes on the Download File Save Path** - If no download file name is specified, the framework automatically resolves the file name from the download URL. - If a custom file name is provided, that name will be used to save the final downloaded file. - Additionally, if you only provide a destination folder (directory) for the downloaded file, make sure the folder (directory) path ends with a slash (`/`). After the file download completes, the framework returns a `FileTransferResult` object with the following properties: - `IsSuccess`: Whether the transfer completed successfully (`bool` type). **Note: Skipping because the file already exists is also considered a success.** - `RequestUri`: The file transfer address (`string` type). For downloads, this is the download address; for uploads, this is the upload address. - `FilePath`: The file's path (`string` type). - `FileSize`: The file's size (a `long` type in bytes). - `ElapsedMilliseconds`: The transfer duration (a `long` type in milliseconds). - `StatusCode`: The response status (`HttpStatusCode` type). If the local file already exists, an `InvalidOperationException` will be thrown: `System.InvalidOperationException: The destination path 'C:\Workspaces\aspnetcore-runtime-8.0.10-win-x64.exe' already exists.`. In this case, you can specify the behavior when the file exists via the `fileExistsBehavior` parameter: ```cs showLineNumbers {3} var fileTransferResult = await httpRemoteService.DownloadFileAsync("https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe" , @"C:\Workspaces\" , fileExistsBehavior: FileExistsBehavior.Overwrite); // Overwrite if the file exists ``` The `FileExistsBehavior` enum contains the following options: - `CreateNew` (default): If the file already exists, an exception is thrown; otherwise, a new file is created. - `Overwrite`: Overwrites the existing file. - `Skip`: Keeps the existing file and skips the download operation. While downloading a file, you can also obtain real-time download progress. The following example shows how to print the download progress: ```cs showLineNumbers {3-6} var fileTransferResult = await httpRemoteService.DownloadFileAsync("https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe" , @"C:\Workspaces\" , async progress => { Console.WriteLine(await progress.ToSummaryStringAsync()); // Output a brief progress string } , fileExistsBehavior: FileExistsBehavior.Overwrite); ``` Example console output of download progress (using `progress.ToSummaryString()`): ```bash showLineNumbers Transferred 0.26MB of 10.09MB (2.63% complete, Speed: 3.86MB/s, Time: 0.07s, ETA: 2.55s). File: aspnetcore-runtime-8.0.10-win-x64.exe, Path: C:\Workspaces\aspnetcore-runtime-8.0.10-win-x64.exe. Transferred 10.09MB of 10.09MB (100.00% complete, Speed: 9.99MB/s, Time: 1.01s, ETA: 0.00s). Done! File: aspnetcore-runtime-8.0.10-win-x64.exe, Path: C:\Workspaces\aspnetcore-runtime-8.0.10-win-x64.exe. ``` To display file download progress in real time in the console, the `UpdateConsoleProgressAsync()` method is recommended. An example follows: ```cs showLineNumbers {3,7} var fileTransferResult = await httpRemoteService.DownloadFileAsync("https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe" , @"C:\Workspaces\" , progress => progress.UpdateConsoleProgressAsync() // Update the file transfer progress bar in the console , fileExistsBehavior: FileExistsBehavior.Overwrite); // ✅ Or use the DownloadFileWithConsoleProgressAsync method (with console progress printing) var fileTransferResult = await httpRemoteService.DownloadFileWithConsoleProgressAsync("https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe" , @"C:\Workspaces\" , fileExistsBehavior: FileExistsBehavior.Overwrite); ``` After execution, the console displays the following progress information: ```bash showLineNumbers {2} File: aspnetcore-runtime-8.0.10-win-x64.exe, Path: C:\Workspaces\aspnetcore-runtime-8.0.10-win-x64.exe [############################## ] 61.35% (6.19MB/10.09MB) Speed: 5.81MB/s, Time: 1.07s, ETA: 0.67s. ``` If you use `progress.ToString()`, the console output will contain more detailed progress information: ```bash showLineNumbers Transfer Progress: File Name: aspnetcore-runtime-8.0.10-win-x64.exe File Path: C:\Workspaces\aspnetcore-runtime-8.0.10-win-x64.exe File Size: 10.09MB Transferred: 0.12MB Percentage Complete: 1.23% Transfer Rate: 2.20MB/s Time Elapsed (s): 0.06 Estimated Time Remaining (s): 4.52 Transfer Progress: File Name: aspnetcore-runtime-8.0.10-win-x64.exe File Path: C:\Workspaces\aspnetcore-runtime-8.0.10-win-x64.exe File Size: 10.09MB Transferred: 10.09MB Percentage Complete: 100.00% Transfer Rate: 9.77MB/s Time Elapsed (s): 1.03 Estimated Time Remaining (s): 0.00 ``` The type of the `progress` parameter is `FileTransferProgress`, which contains the following properties and methods: - **Properties**: - `FilePath`: The file's path (`string` type). - `FileName`: The file's name (`string` type). - `FileSize`: The file's size (a `long` type in bytes). - `Transferred`: The amount of data transferred (a `long` type in bytes). - `PercentageComplete`: The percentage of the transfer completed (`double` type). - `TransferRate`: The current transfer rate (a `double` type in bytes/second). - `TimeElapsed`: The duration from the start of the transfer to the present (`TimeSpan` type). - `EstimatedTimeRemaining`: The estimated remaining transfer time (`TimeSpan` type). - **Methods**: - `ToString()`: Outputs a detailed, indented progress string. - `ToStringAsync()`: Outputs a detailed, indented progress string. - `ToSummaryString()`: Outputs a brief progress string. - `ToSummaryStringAsync()`: Outputs a brief progress string. - `UpdateConsoleProgress()`: Updates (prints) the file transfer progress bar in the console. - `UpdateConsoleProgressAsync()`: Updates (prints) the file transfer progress bar in the console. ### Downloading Multiple Files in Parallel The framework natively supports downloading multiple files in parallel. With the `ParallelUtility.ForEachAsync` utility method, you can easily implement concurrent downloads and automatically enable **multi-line progress bar mode** — each file occupies two lines (the file header and the progress bar), and all progress bars refresh on the same screen in real time without interfering with each other: ```cs showLineNumbers {10,12} var urls = new[] { "https://img-s.msn.cn/tenant/amp/entityid/AA296jTM.img?w=640&h=1068&m=6", "https://img-s.msn.cn/tenant/amp/entityid/AA297bnQ.img?w=640&h=1240&m=6&x=236&y=233&s=64&d=64", "https://img-s.msn.cn/tenant/amp/entityid/AA296Rr4.img?w=640&h=821&m=6" }; const string saveDir = @"C:\Workspaces\"; // Download in parallel; the default maximum concurrency is 4 await ParallelUtility.ForEachAsync(urls, async (url, token) => { await _httpRemoteService.DownloadFileWithConsoleProgressAsync(url, saveDir, FileExistsBehavior.Overwrite, cancellationToken: token); }); ``` After execution, the console displays the download progress of all files simultaneously, with each file's progress bar refreshing independently: ```bash showLineNumbers File: AA296jTM.img, Path: C:\Workspaces\AA296jTM.img [########............] 40.12% (0.05MB/0.12MB) Speed: 1.20MB/s, Time: 42ms, ETA: 58ms. File: AA297bnQ.img, Path: C:\Workspaces\AA297bnQ.img [######..............] 30.05% (0.04MB/0.12MB) Speed: 0.95MB/s, Time: 38ms, ETA: 84ms. File: AA296Rr4.img, Path: C:\Workspaces\AA296Rr4.img [##########..........] 50.33% (0.06MB/0.12MB) Speed: 1.55MB/s, Time: 45ms, ETA: 39ms. ``` After all files finish downloading, the progress bars display the `Done!` status one by one: ```bash showLineNumbers File: AA296jTM.img, Path: C:\Workspaces\AA296jTM.img [####################] 100.00% (0.12MB/0.12MB) Speed: 2.61MB/s, Time: 167ms. Done! File: AA297bnQ.img, Path: C:\Workspaces\AA297bnQ.img [####################] 100.00% (0.12MB/0.12MB) Speed: 1.81MB/s, Time: 203ms. Done! File: AA296Rr4.img, Path: C:\Workspaces\AA296Rr4.img [####################] 100.00% (0.12MB/0.12MB) Speed: 1.61MB/s, Time: 204ms. Done! ``` You can also control the maximum concurrency via the `maxDegreeOfParallelism` parameter: ```cs showLineNumbers {5} await ParallelUtility.ForEachAsync(urls, async (url, token) => { await _httpRemoteService.DownloadFileWithConsoleProgressAsync(url, saveDir, FileExistsBehavior.Overwrite, cancellationToken: token); }, maxDegreeOfParallelism: 2); // Download at most 2 files at the same time ``` In addition to the approaches above, the following methods are also supported for downloading network resources: ```cs showLineNumbers {2} // Using the builder pattern var fileTransferResult = await httpRemoteService.SendAsync(HttpRequestBuilder.DownloadFile("https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe" , @"C:\Workspaces\" , fileExistsBehavior: FileExistsBehavior.Overwrite)); // For more detailed usage, see Section 2.1 ``` --- # 2.5 Uploading File Resources (OSS) > Source: https://http.furion.net/en/docs/quick-start/upload/ In internet applications, uploading files is a common requirement, covering scenarios such as setting an avatar, posting image-and-text updates, uploading albums to a cloud drive, and sharing a `Vlog` to a video community. The following demonstrates several ways to implement file uploads. **1. Uploading via the `Form` Form Method** Uploading files via the `Form` form method is consistent with the `Form` form submission method described in Section 2.3. ```cs showLineNumbers {2-3} await httpRemoteService.PostAsync("https://localhost:7044/HttpRemote/AddFile", builder => builder .SetMultipartContent(multipart => multipart .AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file"))); ``` If you need to upload multiple files, simply continue adding them in `multipart` (keeping the form name consistent, such as `files`): ```cs showLineNumbers {3-4} await httpRemoteService.PostAsync("https://localhost:7044/HttpRemote/AddFiles", builder => builder .SetMultipartContent(multipart => multipart .AddFileAsStream(@"C:\Workspaces\httptest.jpg", "files") .AddFileFromRemote("https://furion.net/img/furionlogo.png", "files"))); ``` In addition, it also supports using the builder pattern, as well as retrieving the return value of the file upload. For more details, refer to Section 2.1. ```cs showLineNumbers {2} // Use the builder pattern await httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://localhost:7044/HttpRemote/AddFile") .SetMultipartContent(multipart => multipart .AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file"))); // For more detailed usage, refer to Section 2.1 ``` **2. Uploading via a Non-`Form` Form Method (`OSS`)** When integrating with certain `OSS` (object storage services) or cloud drives, you often encounter cases where the traditional `Form` form upload method is not supported. In such cases, you need to upload the file directly as a file byte array or a `Stream`. The following is a concrete implementation example: ```cs showLineNumbers {1,4} var fileStream = File.OpenRead("file path"); // Or use: var fileBytes = File.ReadAllBytes("file path"); await httpRemoteService.PutAsync("https://localhost:7044/HttpRemote/AddFile", builder => builder .SetContent(fileStream)); // Or use .SetContent(fileBytes); ``` In some special scenarios, you may need to explicitly remove the `Content-Type` request header (i.e., set it to empty). In this case, you can do so by calling the `SetOmitContentType(true)` method, as shown below: ```cs showLineNumbers {3} await httpRemoteService.PutAsync("https://localhost:7044/HttpRemote/AddFile", builder => builder .SetContent(fileStream) // Or use .SetContent(fileBytes); .SetOmitContentType(true); // .AutoSetHostHeader()); // Some servers may enforce validation of the Host request header (optional) ``` > **Converting an `IFormFile` Instance to `Stream` and Uploading It** In web applications, we typically use the `IFormFile` type to receive files uploaded by users. If you need to upload such a file further to `OSS` (object storage services) or a cloud drive, you can convert it to `Stream` and complete the upload as follows: ```cs showLineNumbers {2} await httpRemoteService.PutAsync("https://localhost:7044/HttpRemote/AddFile", builder => builder .SetContent(formFile.OpenReadStream(), disposeResourcesOnRequestCompletion: true)); ``` **3. Using the `UploadFile` Extension Method (Form Method)** In applications such as video sharing, users typically need to view real-time progress when uploading files. For this purpose, you can use the `UploadFile` extension method, which supports retrieving real-time progress and allows restricting the file type and size. The following example shows how to print the upload progress: ```cs showLineNumbers {2-5} await httpRemoteService.UploadFileAsync("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file" , async progress => { Console.WriteLine(await progress.ToSummaryStringAsync()); // Output brief progress information }); ``` Console output example: ```bash showLineNumbers Transferred 0.01MB of 0.01MB (100.00% complete, Speed: 0.86MB/s, Time: 0.01s, ETA: 0.00s), File: httptest.jpg, Path: C:\Workspaces\httptest.jpg. ``` If you need to display the file upload progress in the console in real time, it is recommended to use the `UpdateConsoleProgressAsync()` method. The example is as follows: ```cs showLineNumbers {2,5} await httpRemoteService.UploadFileAsync("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file" , progress => progress.UpdateConsoleProgressAsync()); // Update the file transfer progress bar in the console // ✅ Or use the UploadFileWithConsoleProgressAsync method (with console progress printing) await httpRemoteService.UploadFileWithConsoleProgressAsync("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file"); ``` After execution, the console displays the following progress information: ```bash showLineNumbers {2} File: httptest.jpg, Path: C:\Workspaces\httptest.jpg. [##################################################] 61.35% (0.01MB/0.01MB) Speed: 0.86MB/s, Time: 0.01s, ETA: 0.00s. ``` If you need to restrict the file type and size, do as follows: ```cs showLineNumbers {1,6-7} await httpRemoteService.SendAsync(HttpRequestBuilder.UploadFile("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file" , async progress => { Console.WriteLine(await progress.ToSummaryStringAsync()); // Output brief progress information }) .SetAllowedFileExtensions(".jpg;.png") // Only allow jpg and png types .SetMaxFileSizeInBytes(5 * 1024 * 1024)); // Limit the file size to 5MB ``` If you need to attach additional form parameters when uploading a file, do as follows: ```cs showLineNumbers {6-9} await httpRemoteService.SendAsync(HttpRequestBuilder.UploadFile("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file" , async progress => { Console.WriteLine(await progress.ToSummaryStringAsync()); // Output brief progress information }) .WithMultipart(multipart => { multipart.AddText("Furion", "name"); }); ``` Through the above approaches, you can flexibly meet various file upload requirements. > **About Multiple File Uploads** The `UploadFile` extension method only supports uploading a single file and cannot handle uploading multiple files simultaneously. > **Disabling the Request Analysis Tool** When printing request content, the `Stream` object may be read repeatedly or become unreadable. This is because the stream is read into memory in advance, and its position pointer moves to the end. This prevents accurately obtaining the upload progress. Therefore, when using the framework's dedicated upload features, it is recommended to disable the request analysis tool to ensure accurate upload progress information can be obtained. --- # 2.6 HTTP Declarative Requests (Proxy Approach) > Source: https://http.furion.net/en/docs/quick-start/declarative/ The `HTTP` Declarative Requests mechanism dynamically builds implementation classes at runtime by implementing the `IHttpDeclarative` interface. This mechanism intelligently intercepts method calls that match specific rules and automatically generates the corresponding `HTTP` remote request code. This approach not only greatly reduces the burden on developers writing `HTTP` request code, but also makes the code structure clearer and easier to organize, maintain, and reuse. The following example briefly demonstrates how to define and use `HTTP` Declarative Requests: **1. Define the `IHttpService` Interface and Implement `IHttpDeclarative`** ```cs showLineNumbers {1,4-5,8-10,13-14,17-18,21-22} public interface IHttpService : IHttpDeclarative { // Get the website content [Get("https://furion.net")] Task GetWebSiteContent(); // Carry request data [Post("https://localhost:7044/HttpRemote/AddModel")] [QueryParam("query1", 1)] // Set query parameters Task PostData([QueryParam(AliasAs = "query2")] string param, [Body(MediaTypeNames.Application.Json)] object data); // Set query parameters and specify an alias and request content // Form form submission [Post("https://localhost:7044/HttpRemote/AddForm?id=1")] Task PostForm(Action multipart); // Form form submission [Post("https://localhost:7044/HttpRemote/AddForm?id=1")] Task PostForm2([Multipart(AsFormItem = false)] object obj, [Multipart("file", AsFileFrom = FileSourceType.Path)] string filePath); // URL-encoded form submission [Post("https://localhost:7044/HttpRemote/AddURLForm")] Task PostURLForm([Body(MediaTypeNames.Application.FormUrlEncoded)] object data); } ``` **2. Register the `IHttpService` Service** In the `Startup.cs` or `Program.cs` file, register and configure the `HttpRemote` service to support `HTTP` Declarative Requests: ```cs showLineNumbers {1,4,7} services.AddHttpRemote(builder => { // Register a single HTTP declarative request interface builder.AddHttpDeclarative(); // Scan assemblies to register HTTP declarative request interfaces in bulk (this registration method is recommended) // builder.AddHttpDeclarativesFromAssemblies([Assembly.GetEntryAssembly()]); // If you are using the Furion framework, you can pass App.Assemblies directly }); ``` **3. Inject the `IHttpService` Service** In classes that need to use `IHttpService`, obtain its instance via dependency injection: ```cs showLineNumbers {3,5} public class YourService { private readonly IHttpService _httpService; public YourService(IHttpService httpService) { _httpService = httpService; } } ``` If you are using `.NET 8` or later, you can simplify the code by injecting via [primary constructors](https://learn.microsoft.com/zh-cn/dotnet/csharp/whats-new/tutorials/primary-constructors): ```cs showLineNumbers {1} public class YourService(IHttpService httpService) { // Use the httpService variable } ``` Alternatively, you can also inject on demand within a specific method: ```cs showLineNumbers {3} public class YourService { public Task GetResource([FromServices] IHttpService httpService) { // Your code logic } } ``` **4. Call the `IHttpService` Methods** Use the injected `IHttpService` instance to call its methods to send `HTTP` requests and obtain responses: ```cs showLineNumbers {2,5,8-10,12,15} // Get the website content var content = await httpService.GetWebSiteContent(); // Carry request data var content = await httpService.PostData("furion", new { id = 1, name = "furion" }); // Form form submission var content = await httpService.PostForm(multipart => multipart .AddJson(new { id = 1, name = "furion" }) // Set regular fields .AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file")); var content = await httpService.PostForm2(new { id = 1, name = "furion" }, @"C:\Workspaces\httptest.jpg"); // URL-encoded form submission var content = await httpService.PostURLForm(new { id = 1, name = "furion" }); ``` By using `HTTP` Declarative Requests, you can significantly reduce the effort of writing `HTTP` request code and make the code more concise and easier to organize and maintain. This approach is especially recommended for large projects or multi-person collaboration projects. --- # 2.7 Request Analysis Tool > Source: https://http.furion.net/en/docs/quick-start/profiler/ Modern browsers typically include built-in developer tools that can capture and visually display all request and response data when users visit a website. Similarly, we also provide a set of analysis tools for the `HTTP` remote request module. ### How to Enable The following is an example of how to enable the request analysis tool: ```cs showLineNumbers {4,8,12-15} // Builder approach await httpRemoteService.SendAsync(HttpRequestBuilder.Get("https://furion.net") .WithHeader("X-Header", "custom") .Profiler()); // Enable the request analysis tool, or use Debugger() // HTTP request verb approach await httpRemoteService.GetAsync("https://furion.net" , builder => builder.Profiler()); // Enable the request analysis tool, or use Debugger() // You can also obtain the analysis data from the request analysis tool await httpRemoteService.GetAsync("https://furion.net" , builder => builder.Profiler(analyzer => { Console.WriteLine(analyzer.Data); })); ``` Once enabled, when executing an `HTTP` remote request, the console outputs the following detailed information: ```bash showLineNumbers Request Headers: User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0 X-Header: custom General: Request URL: https://furion.net/ Request Method: GET Status Code: 200 OK HTTP Version: 1.1 HTTP Content: Content Type: HttpClient Name: Request Duration (ms): 149.00 Response Headers: Server: nginx/1.22.1 Date: Thu, 14 Nov 2024 15:35:41 GMT Connection: keep-alive Vary: Accept-Encoding ETag: "67091697-f32f" Cache-Control: max-age=315360000 Accept-Ranges: bytes Content-Type: text/html Content-Length: 62255 Last-Modified: Fri, 11 Oct 2024 12:14:15 GMT Expires: Thu, 31 Dec 2037 23:55:55 GMT ``` > **Notes on `Blazor WebAssembly` Projects** In `Blazor WebAssembly` applications, the request analysis tool's content is displayed in the client-side (i.e., browser) developer tools console. Make sure to check this console during development to obtain the relevant analysis information. ### Global Enablement and Advanced Configuration In addition to enabling the profiler for a single request, you can also register it globally to enable it within `HttpClient`: ```cs showLineNumbers {3,7,10,13-14,17-18,21-22} // Enable for the default client services.AddHttpClient(string.Empty) .AddProfilerDelegatingHandler(); // You can also provide a conditional disable, for example disable in production environments services.AddHttpClient(string.Empty) .AddProfilerDelegatingHandler(disableIn: () => builder.Environment.EnvironmentName == "Production"); services.AddHttpClient(string.Empty) .AddProfilerDelegatingHandler(disableInProduction: true); // Enable for a specific client //services.AddHttpClient("weixin") // .AddProfilerDelegatingHandler(); // You can also enable it for all client configurations with one click services.ConfigureHttpClientDefaults(clientBuilder => clientBuilder.AddProfilerDelegatingHandler()); // Or use the IHttpRemoteBuilder extension method for one-click configuration services.AddHttpRemote() .ConfigureHttpClientDefaults(clientBuilder => clientBuilder.AddProfilerDelegatingHandler()); ``` ### Usage in Declarative Requests At the same time, `HTTP` Declarative Requests also support enabling the request profiler via the `[Profiler]` attribute: ```cs showLineNumbers {1,7,17} [Profiler] // Enable the request profiler for all methods within the interface public interface IHttpService : IHttpDeclarative { [Get("https://furion.net")] Task ProfilerMethod(); [Profiler(false)] // Disable the request profiler for this method [Get("https://furion.net")] Task NonProfilerMethod(); } public interface IHttpService : IHttpDeclarative { [Get("https://furion.net")] Task NonProfilerMethod(); [Profiler] // Enable the request profiler for this method [Get("https://furion.net")] Task ProfilerMethod(); } ``` By enabling the request profiler, developers can observe and debug `HTTP` requests more intuitively and conveniently, thereby improving development efficiency and debugging accuracy. ### Custom Log Output Target By default, the request profiler's information is output to the console via `Console.WriteLine`. However, in environments such as `WinForms`, `WPF`, or `MAUI`, console output may be invisible or unsupported. In this case, you can redirect the logs to other targets by configuring `FallbackLogger`, for example to debug output (`System.Diagnostics.Debug.WriteLine`): ```cs showLineNumbers {2,5} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { // Fallback log output delegate used when the logging service or console output is unavailable options.FallbackLogger = message => System.Diagnostics.Debug.WriteLine(message); }); ``` In this way, the profiling information will be displayed in the "Output" window (debug view) of `Visual Studio` or other locations supported by the debugger. > **Disable in Production** To ensure optimal performance and security in production environments, it is recommended to **disable** the request profiler in production. In addition, printing request content may cause the `Stream` object to be read repeatedly or become unreadable, because the stream is read into memory in advance and its `Position` consequently moves to the end. **Supplementary note:** By default, the request profiler only displays up to `5KB` of content data in the request or response content. --- # 2.8 Adding Authorization Credentials > Source: https://http.furion.net/en/docs/quick-start/authentication/ In the internet society, network security is becoming increasingly critical, especially when integrating with third-party interfaces, which usually requires authentication and authorization before access is granted. Currently, the common authorization methods used by internet application interfaces include `Bearer` authentication, `Basic` authentication, `Digest` authentication, and `OAuth` authentication. The following example shows how to add authorization to an `HTTP` remote request: ```cs showLineNumbers {3,7,11,15} // Add Bearer authentication await httpRemoteService.SendAsync(HttpRequestBuilder.Get("http://furion.net") .AddBearerAuthentication("your token")); // Add Basic authentication await httpRemoteService.SendAsync(HttpRequestBuilder.Get("http://furion.net") .AddBasicAuthentication("username", "password")); // Add Digest authentication await httpRemoteService.SendAsync(HttpRequestBuilder.Get("http://furion.net") .AddDigestAuthentication("username", "password")); // Add custom Schema authentication await httpRemoteService.SendAsync(HttpRequestBuilder.Get("http://furion.net") .AddAuthentication(new AuthenticationHeaderValue("X-Token", "your token"))); ``` If the authorization credentials are correct, the user can successfully access the network resources; otherwise, the service returns a `401` Unauthorized error. In addition to manually adding authorization credentials for a single request, you can also implement global registration of authorization credentials by creating a custom `AuthorizationDelegatingHandler` class that inherits from `DelegatingHandler`: ```cs showLineNumbers {1,5,13,16-17,20-21,24} public class AuthorizationDelegatingHandler : DelegatingHandler { protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) { // Refer to the SendAsync code return base.Send(request, cancellationToken); } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { // Add Bearer authentication request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "your token"); // Add Basic authentication var base64Credentials = Convert.ToBase64String(Encoding.UTF8.GetBytes("username" + ":" + "password")); request.Headers.Authorization = new AuthenticationHeaderValue("Basic", base64Credentials); // Add Digest authentication var digestCredentials = DigestCredentials.GetDigestCredentials($"https://furion.net/digest", "admin", "a123456789", HttpMethod.Get); request.Headers.Authorization = new AuthenticationHeaderValue("Digest", digestCredentials); // Add custom Schema authentication request.Headers.Authorization = new AuthenticationHeaderValue("X-Token", "your token"); return base.SendAsync(request, cancellationToken); } } ``` **Note:** In practical applications, you should choose one authentication method based on your requirements rather than using multiple authentication headers in a single request. The multiple authentication methods in the code above are only intended to demonstrate how to set different authentication headers. Next, register `AuthorizationDelegatingHandler` in the `Startup.cs` or `Program.cs` file: ```cs showLineNumbers {2,6,9-10} // Register AuthorizationDelegatingHandler as a service services.TryAddSingleton(); // Enable for the default client services.AddHttpClient(string.Empty) .AddHttpMessageHandler(); // Enable for a specific client //services.AddHttpClient("weixin") // .AddHttpMessageHandler() ``` In this way, whenever an `HTTP` request is sent, it will enter the `Send/SendAsync` method of the `AuthorizationDelegatingHandler` class, thereby automatically adding authorization credentials to the request. --- # 2.9 Setting Cookie (Simulated/Automatic Login) > Source: https://http.furion.net/en/docs/quick-start/cookies/ A `Cookie` is a piece of data sent by the server in an `HTTP` response. The client (optionally) stores the `Cookie` and returns it in subsequent requests. This allows the client and server to share state. When sending an `HTTP` remote request, there are two ways to set `Cookie`. - **Set `Cookie` globally via `HttpClient`** This approach allows sharing `Cookie` under the same-origin domain, and if the server returns new `Cookie`, these `Cookie` will be automatically carried in subsequent requests, which is very suitable for implementing features such as **automatic website login**. ```cs showLineNumbers {1-3,7-12} var cookieContainer = new CookieContainer(); // Optionally set the default Cookie cookieContainer.Add(new Uri("https://furion.net"), new Cookie("cookieName", "cookieValue")); // Default client configuration services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { CookieContainer = cookieContainer, UseCookies = true, // Automatically handle Cookies, will be carried automatically in subsequent requests AllowAutoRedirect = true }); ``` > **Security Notes on Automatic `Cookies` Handling** Automatic `Cookies` handling may lead to the leakage of sensitive information, especially when the application shares the same `HttpClient` instance across multiple different domains. If a response from one domain contains a `Cookie` and that `Cookie` is automatically added to a request for another domain, it may cause information leakage. At the same time, automatic `Cookies` handling increases the risk of `CSRF` attacks, because an attacker may leverage existing `Cookies` to perform operations without the user's consent. If your application requires `Cookie`, consider **disabling automatic `Cookie` handling** and call `ConfigurePrimaryHttpMessageHandler` to disable automatic `Cookie` handling: ```cs showLineNumbers {3-7} // Default client configuration services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { UseCookies = false // Disable automatic Cookies handling }); ``` - **Set `Cookie` for a single request** This approach only takes effect for the current request; if the server returns new `Cookie`, they will not be carried by subsequent requests. ```cs showLineNumbers {2-3} await httpRemoteService.SendAsync(HttpRequestBuilder.Get("http://furion.net") .WithCookie("cookieName", "cookieValue") // Set a single Cookie .WithCookies(new { name = "furion", author = "monksoul" })); // Set multiple Cookies ``` **Note:** In practical applications, you may need to choose the appropriate `Cookie` setting approach based on your specific requirements and ensure the security of `Cookie`, for example avoiding cross-site scripting attacks (`XSS`) and cross-site request forgery attacks (`CSRF`). At the same time, for sensitive information, it is recommended to use more secure authentication mechanisms such as `OAuth`, `JWT`, etc. --- # 2.10 Exception Handling (Exception Suppression) > Source: https://http.furion.net/en/docs/quick-start/exception-handling/ When sending an `HTTP` remote request, you may encounter the following exceptional situations: - The target host is unreachable - The request is canceled - The request times out - Other network exceptions By default, these exceptions interrupt program execution. To improve system robustness, the framework provides the following exception handling solutions: **1. Basic exception catching (`try/catch` pattern)** ```cs showLineNumbers {1,5,7-8} try { var httpResponseMessage = httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://furion.net/"); } catch(Exception ex) { var httpResponseMessage = ex.GetResponseMessage(); // You can obtain the HttpResponseMessage? object via the extension var requestDuration = ex.GetRequestDuration(); // You can obtain the request duration (milliseconds) via the extension // Exception handling logic (such as logging, fallback handling) } ``` Applicable scenarios: when precise control over exception handling logic is required (such as recording specific exception logs, performing compensation operations). **2. Exception suppression (silent mode)** Although developers usually use `try/catch` for exception handling, in certain scenarios we prefer that exceptions silently return `null` without interrupting the flow. For this purpose, the framework provides a flexible exception suppression feature. - **Suppress all request exceptions** ```cs showLineNumbers {2} var httpResponseMessage = httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://furion.net/") .SuppressExceptions()); // Suppress all exceptions ``` When an exception occurs in the request, the code does not interrupt but returns `null`, meaning the value of `httpResponseMessage` is `null`. In certain scenarios, we want to suppress the exception while still being able to capture the exception information (for example to record it in the log) without interrupting the normal execution of the program. In this case, you can achieve this via the `SetOnRequestFailed` callback: ```cs showLineNumbers {2-3} HttpRequestBuilder.Post("https://furion.net/") .SuppressExceptions() .SetOnRequestFailed((exception, responseMessage) => // Note: responseMessage may be empty { Console.WriteLine(exception.Message); }); ``` This method allows you to safely handle error information after the exception is suppressed, and is suitable for logging, monitoring, or other error response logic. - **Suppress only specific types of exceptions** The framework also supports suppressing only specific types of exceptions. For example, you can suppress only timeout exceptions and request cancellation exceptions: ```cs showLineNumbers {2} var httpResponseMessage = httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://furion.net/") .SuppressExceptions([typeof(TimeoutException), typeof(TaskCanceledException)])); // Suppress timeout and cancellation exceptions ``` - **Disable exception suppression configuration** If you need to restore the default behavior (i.e., interrupt the program when an exception occurs), you can explicitly disable exception suppression: ```cs showLineNumbers {2} var httpResponseMessage = httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://furion.net/") .SuppressExceptions(false); // Restore the default configuration ``` This configuration is equivalent to not calling `SuppressExceptions()`; when any exception occurs, program execution will be interrupted. > **Precautions** When enabling the exception suppression feature, please note the following: 1. **Override rule** When `SuppressExceptions()` or related configuration is called multiple times, **only the last call takes effect**. 2. **Priority of status code checking and exception suppression** Even if `EnsureSuccessStatusCode()` has been configured, suppressed exceptions still return `null` and will not trigger the status code checking logic. 3. **Priority of exception suppression** The exception suppression feature has a higher priority than status code checking. If both status code checking and exception suppression are enabled, exception suppression takes effect first. 4. **Request interceptors remain available** If the exception is captured via `SetOnRequestFailed(ex, res)` or other request handling mechanisms, the interceptor or callback method will still be called even if the exception is suppressed. 5. **Recommendations on selecting exception types** You should carefully select the exception types to suppress based on the specific business scenario, to avoid masking potential problems by over-suppressing exceptions. 6. **Automatically output suppression logs** When an exception is successfully suppressed, the framework automatically outputs a `Warning`-level log (for example `"An exception occurred but was suppressed by SuppressExceptionPipelineHandler."`), making it easier to troubleshoot problems. --- # 2.11 Stress and Simulation Testing (Performance Testing) > Source: https://http.furion.net/en/docs/quick-start/stress-test/ When developing application systems that are intended for the internet or that need to withstand concurrent access from many users, performance stress testing and automated API simulation testing become critical steps before deployment. Through the report metrics obtained from these two types of testing, we can optimize the code before the system goes live and ensure that it meets the minimum go-live requirements. Using the official website of the `Furion` framework as an example, perform stress testing: ```cs showLineNumbers {1-2} var stressTestHarnessResult = await httpRemoteService.StressTestHarnessAsync("https://furion.net/"); Console.WriteLine(stressTestHarnessResult.ToString()); // Print the stress test results ``` Test result overview: ```bash showLineNumbers Stress Test Harness Result: Total Requests: 100 // Total number of requests Total Time (s): 7.95 // Total time (seconds) Successful Requests: 100 // Number of successful requests Failed Requests: 0 // Number of failed requests QPS: 12.58 // Queries per second (QPS) Min RT (ms): 676.38 // Minimum response time (ms) Max RT (ms): 7,419.72 // Maximum response time (ms) Avg RT (ms): 3,314.94 // Average response time (ms) P10 RT (ms): 1,288.82 // P10 response time (ms) P25 RT (ms): 2,057.10 // P25 response time (ms) P50 RT (ms): 3,064.56 // P50 response time (ms) P75 RT (ms): 4,100.03 // P75 response time (ms) P90 RT (ms): 5,026.08 // P90 response time (ms) P95 RT (ms): 7,330.71 // P95 response time (ms) P99 RT (ms): 7,416.20 // P99 response time (ms) P99.99 RT (ms): 7,419.72 // P99.99 response time (ms) ``` The `stressTestHarnessResult` variable is of type `StressTestHarnessResult`, which contains the following properties and methods: - **Properties**: - `TotalRequests`: Total number of requests (`long` type). - `TotalTimeInSeconds`: Total time in seconds (`double` type). - `SuccessfulRequests`: Number of successful requests (`long` type). - `FailedRequests`: Number of failed requests (`long` type). - `QueriesPerSecond`: Queries per second (`QPS`) (`double` type). - `MinResponseTime`: Minimum response time (ms) (`double` type). - `MaxResponseTime`: Maximum response time (ms) (`double` type). - `AverageResponseTime`: Average response time (ms) (`double` type). - `Percentile10ResponseTime`: `P10` response time (ms) (`double` type). - `Percentile25ResponseTime`: `P25` response time (ms) (`double` type). - `Percentile50ResponseTime`: `P50` response time (ms) (`double` type). - `Percentile75ResponseTime`: `P75` response time (ms) (`double` type). - `Percentile90ResponseTime`: `P90` response time (ms) (`double` type). - `Percentile95ResponseTime`: `P95` response time (ms) (`double` type). - `Percentile99ResponseTime`: `P99` response time (ms) (`double` type). - `Percentile9999ResponseTime`: `P99.99` response time (ms) (`double` type). - **Methods**: - `ToString()`: Outputs a detailed report string with indentation. By default, the stress test executes `1` round, each containing `100` concurrent requests, with a maximum degree of concurrency of `100`. To obtain more accurate test results, you can adjust these parameters as needed: ```cs showLineNumbers {2-4,7,9} var stressTestHarnessResult = await httpRemoteService.SendAsync(HttpRequestBuilder.StressTestHarness("https://furion.net/") .SetNumberOfRequests(1000) // Set the number of concurrent requests .SetNumberOfRounds(5) // Set the number of stress test rounds .SetMaxDegreeOfParallelism(500)); // Set the maximum degree of parallelism // In most cases, you only need to set the number of concurrent requests var stressTestHarnessResult = await httpRemoteService.StressTestHarnessAsync("https://furion.net/", 500); var stressTestHarnessResult = await httpRemoteService.SendAsync(HttpRequestBuilder.StressTestHarness("https://furion.net/", 500)); ``` > **Quickly Generate Test Reports** When performing stress testing, a `GET` request is used by default and the full response content is downloaded (`HttpCompletionOption.ResponseContentRead`). If the full response content is not needed, you can use a `HEAD` request and set `completionOption` to `ResponseHeadersRead` to quickly generate stress test reports. > **Abuse Notice** **During stress testing, the `X-Stress-Test: Harness` request header is automatically added to prevent abuse from harming the target system.** At the same time, since the test results are affected by various factors such as hardware devices, operating systems, and code implementation, they are for reference only. In addition, **to obtain more accurate data, the request profiler is disabled by default**. --- # 2.12 Long Polling > Source: https://http.furion.net/en/docs/quick-start/long-polling/ Long polling (`Long Polling`) is a technique for pushing data from the server to the client. It simulates the effect of server push by keeping the `HTTP` connection open until new data is sent to the client, or until a timeout occurs. Long polling is an improvement over traditional polling (where the client periodically sends requests to the server to check for new data), which can reduce unnecessary requests and improve efficiency. How long polling works: 1. The client initiates a request to the server. 2. If there is no new data on the server, the server does not immediately respond to the request but instead holds the request. 3. Once new data is available on the server to send, or the preset timeout is reached, the server responds to the request and sends the data to the client. 4. After the client finishes processing the data, it initiates a new request to the server again, repeating the above process. ![long-polling](/images/long-polling.png) Application scenarios of long polling: - **Real-time notifications**: For example, in online chat applications, when a user receives a new message, the server can promptly push the message to the client through long polling. - **Online collaboration tools**: Such as applications where multiple people edit a document simultaneously; long polling can be used to synchronize users' editing operations in real time. - **Game updates**: In online games, long polling can be used to update game state in real time, such as player positions, scores, and other information. - **Stock market updates**: Financial applications use long polling to display stock price changes in real time. - **Configuration centers**: In a microservices architecture, configuration centers use long polling to ensure that each service can immediately receive the latest configuration changes. When a configuration changes, the configuration center can quickly push the update to all relevant service instances, ensuring configuration consistency and timeliness. The following example shows how to use a long polling request: ```cs showLineNumbers {1-2,9-11} await httpRemoteService.LongPollingAsync("https://localhost:7044/HttpRemote/LongPolling" , async (responseMessage, token) => { Console.WriteLine(await responseMessage.Content.ReadAsStringAsync(token)); await Task.CompletedTask; }, cancellationToken: cancellationToken); // Using the builder pattern await httpRemoteService.SendAsync(HttpRequestBuilder .LongPolling("https://localhost:7044/HttpRemote/LongPolling" , async (responseMessage, token) => { Console.WriteLine(await responseMessage.Content.ReadAsStringAsync(token)); await Task.CompletedTask; }), cancellationToken: cancellationToken); ``` Long polling also supports consuming data as `IAsyncEnumerable`, allowing you to use `await foreach` to iterate over each polling response: ```cs showLineNumbers {1,4,6,11,13,15} await foreach (var responseMessage in httpRemoteService.LongPollingAsAsyncEnumerable("https://localhost:7044/HttpRemote/LongPolling", cancellationToken: cancellationToken)) { // Note: each response needs to be manually disposed after use (or use using) using (responseMessage) { Console.WriteLine(await responseMessage.Content.ReadAsStringAsync(cancellationToken)); } } // Using the builder pattern await foreach (var responseMessage in httpRemoteService.SendAsAsyncEnumerable(HttpRequestBuilder.LongPolling("https://localhost:7044/HttpRemote/LongPolling"), cancellationToken)) { using (responseMessage) { Console.WriteLine(await responseMessage.Content.ReadAsStringAsync(cancellationToken)); } } ``` Although long polling addresses the need for real-time communication to a certain extent, it also has some disadvantages. For example, under high concurrency it may put considerable pressure on the server, and long-lived connections may affect server performance. As `Web` technologies evolve, more advanced technologies such as `Server-Sent Events` or `WebSocket` are gradually becoming the preferred solution for real-time bidirectional communication. However, in certain restricted environments, long polling is still a viable choice. --- # 2.13 Server-Sent Events Unidirectional Communication > Source: https://http.furion.net/en/docs/quick-start/sse/ With the rapid popularity of the artificial intelligence chatbot `ChatGPT`, the typewriter-effect conversation design simulated in its user interface has left a deep impression on people. This vivid and realistic interactive experience is actually implemented through a technology called "Server-Sent Events" (`Server-Sent Events`, `SSE`). `Server-Sent Events` is a communication technology that allows the server to proactively send real-time update data to the client (usually a browser). **Unlike the traditional client request–server response pattern, `SSE` implements unidirectional, asynchronous communication from the server to the client, eliminating the need for the client to continuously poll the server for the latest data.** This technology greatly reduces the burden on the server and improves the efficiency and real-time nature of data transmission. Application scenarios of `Server-Sent Events`: 1. **Real-time notifications**: It can be used to implement real-time message alerts or notification systems, such as new message alerts on social networks or email arrival notifications. 2. **Data stream updates**: For data that needs continuous updates, such as stock prices, weather information, or sports results, `SSE` can provide instant data updates. 3. **Progress reports**: When executing long-running tasks, such as file uploads or complex computations, `SSE` can be used to report the progress of the task to the client. 4. **Logs and monitoring**: In the development and operations fields, `SSE` can be used to display changes in log files in real time or monitor the health status of systems. The following example shows how to use `Server-Sent Events` to obtain data from the server: ```cs showLineNumbers {1,3,11,13} await httpRemoteService.ServerSentEventsAsync("https://localhost:7044/HttpRemote/Events" // Action to take when data is received , async (data, token) => { Console.WriteLine(data.Data); await Task.CompletedTask; }, cancellationToken: cancellationToken); // Using the builder pattern await httpRemoteService.SendAsync(HttpRequestBuilder .ServerSentEvents("https://localhost:7044/HttpRemote/Events" // Action to take when data is received , async (data, token) => { Console.WriteLine(data.Data); await Task.CompletedTask; }), cancellationToken: cancellationToken); ``` `Server-Sent Events` also supports consuming data as `IAsyncEnumerable`, allowing you to use `await foreach` to iterate over each polling response: ```cs showLineNumbers {1,3,7,9} await foreach (var data in httpRemoteService.ServerSentEventsAsAsyncEnumerable("https://localhost:7044/HttpRemote/Events", cancellationToken: cancellationToken)) { Console.WriteLine(data.Data); } // Using the builder pattern await foreach (var data in httpRemoteService.SendAsAsyncEnumerable(HttpRequestBuilder.ServerSentEvents("https://localhost:7044/HttpRemote/Events"), cancellationToken)) { Console.WriteLine(data.Data); } ``` The `data` parameter is of type `ServerSentEventsData`, which contains the following properties: - **Properties**: - `Event`: Event type (`string` type). - `Data`: Message (`string` type). - `RawLine`: Raw message line (`string` type). - `Id`: Event `ID` (`string` type). - `Retry`: Reconnection time (an `int` type in milliseconds). - `CustomFields`: Custom field data (`IReadOnlyCollection>` type). You can also listen for events when the connection succeeds and when sending fails: ```cs showLineNumbers {9,14,29,34} await httpRemoteService.ServerSentEventsAsync("https://localhost:7044/HttpRemote/Events" // Action to take when data is received , async (data, token) => { Console.WriteLine(data.Data); await Task.CompletedTask; }, builder => builder // Action to take when the connection opens .SetOnOpen(() => { Console.WriteLine("Connected."); }) // Action to take when the connection fails to open .SetOnError((ex) => { Console.WriteLine("Connection error: " + ex.Message); }), cancellationToken: cancellationToken); // Using the builder pattern await httpRemoteService.SendAsync(HttpRequestBuilder .ServerSentEvents("https://localhost:7044/HttpRemote/Events" // Action to take when data is received , async (data, token) => { Console.WriteLine(data.Data); await Task.CompletedTask; }) // Action to take when the connection opens .SetOnOpen(() => { Console.WriteLine("Connected."); }) // Action to take when the connection fails to open .SetOnError((ex) => { Console.WriteLine("Connection error: " + ex.Message); }), cancellationToken: cancellationToken); ``` `Server-Sent Events` is particularly suitable for scenarios where the server needs to send updates to the client but the client does not need to send requests to the server frequently. Whether it is used for real-time data updates, progress reports, or a simple notification system, `SSE` is a choice worth considering. > **Disable the Request Profiler** When sending `Server-Sent Events` (server-sent events), because it returns data in a `Stream`-based streaming manner, enabling the request profiler would cause each part of the streaming data to be loaded into memory and read in advance. This not only severely affects the real-time display of streaming data but may also cause excessive memory usage when a large amount of data is returned. Therefore, it is recommended to disable the request profiler when sending `Server-Sent Events` requests. > **Request Verb Notes** Standardized `Server-Sent Events (SSE)` only supports receiving server-pushed events via the `GET` method. However, the framework provides support for configuring `SSE` through any request verb (such as `POST` in the example): ```cs showLineNumbers {} HttpRequestBuilder .ServerSentEvents(HttpMethod.Post, new Uri("https://localhost:7044/HttpRemote/Events")); ``` --- # 2.14 WebSocket Duplex Communication > Source: https://http.furion.net/en/docs/quick-start/websocket/ `WebSocket` is a protocol that provides full-duplex communication over a single `TCP` connection. `WebSocket` simplifies data exchange between the client and the server, allowing the server to actively push data to the client. In the `WebSocket API`, the browser and the server only need to complete a single handshake, after which a persistent connection is created directly between them for bidirectional data transfer. Use cases for `WebSocket`: - **Real-time chat applications**: `WebSocket` enables real-time message delivery, making communication between users nearly latency-free. - **Online games**: For games that require fast responses, `WebSocket` provides low-latency data transfer. - **Stock market updates**: Update stock prices and other financial information in real time. - **Collaborative editing tools**: Allow multiple users to edit the same document simultaneously and see each other's changes in real time. - **Real-time map applications**: For example, real-time traffic condition updates in navigation applications. The following example shows how to use `WebSocketClient` to connect to a server: ```cs showLineNumbers {1,4,10,16,22,29,32,38,45,49} using var webSocketClient = new WebSocketClient("wss://ws.postman-echo.com/raw"); // Supports ws:// and wss:// // Connection established event webSocketClient.Connected += (sender, s) => { Console.WriteLine("Connected"); return Task.CompletedTask; }; // Connection closed event webSocketClient.Closed += (sender, args) => { Console.WriteLine("Connection closed"); return Task.CompletedTask; }; // Text message received webSocketClient.TextReceived += (sender, s) => { Console.WriteLine(s.Message); return Task.CompletedTask; }; // Binary message received webSocketClient.BinaryReceived += (sender, s) => { Console.WriteLine(s.Message); return Task.CompletedTask; }; // Connect to the server await webSocketClient.ConnectAsync(); // Start a task that sends messages in a loop _ = Task.Run(async () => { var i = 0; while (i < 5) { // Send a text message await webSocketClient.SendAsync("Hello, WebSocket!"); await Task.Delay(1000); i++; } // Close the connection await webSocketClient.CloseAsync(); }); // Wait for message reception and close events (blocking) await webSocketClient.WaitAsync(); ``` Differences between `WebSocket` and `Server-Sent Events (SSE)`: - **Communication direction**: `WebSocket` supports full-duplex bidirectional communication, while `SSE` only supports unidirectional data push from the server to the client. - **Protocol**: `WebSocket` uses the independent `WebSocket` protocol (`ws://` or `wss://`), while `SSE` is based on the `HTTP` protocol. - **Handshake process**: `WebSocket` requires a special `HTTP` upgrade header to switch protocols, while `SSE` requires no special handshake and establishes the connection directly through an `HTTP` request. - **Connection persistence**: A `WebSocket` connection remains open until explicitly closed, while an `SSE` connection may drop due to network issues, but the browser automatically reconnects. - **Data format**: `WebSocket` supports multiple data formats, including binary data, while `SSE` has a relatively fixed data format, typically simple text messages. - **Cross-origin support**: `WebSocket` checks the cross-origin policy when establishing a connection but is unrestricted afterward, while `SSE` relies on the `CORS` policy. Choosing between `WebSocket` and `SSE` mainly depends on the specific application requirements: - If bidirectional communication or handling large volumes of data streams is required, `WebSocket` is the better choice; - If only server-to-client push updates are needed and the data format requirements are not high, `SSE` may be lighter-weight and easier to implement. --- # 2.15 HttpContext Forwarding and Proxying > Source: https://http.furion.net/en/docs/quick-start/httpcontext-forward/ `HttpContext` forwarding refers to the process, within an `ASP.NET Core` application, of forwarding the contextual information of an `HTTP` request (including request headers, request content, query strings, response headers, response content, etc.) from one request to another internal request or service. This technique allows developers to redirect a request to another processing point without changing the client request, thereby implementing request proxying or routing functionality. Use cases for `HttpContext` forwarding: - **`API Gateway` pattern**: Serves as the entry point for all external requests, routing requests to the correct backend services. - **Load balancing and failover**: Forwards requests to other available service instances to ensure system stability and reliability. - **Request logging and auditing**: Records request information to a logging system or auditing service to facilitate monitoring and debugging. - **Security filtering and validation**: Checks the authentication information and permissions of requests during forwarding to ensure their legitimacy. - **A/B testing and blue-green deployment**: Routes part of the traffic to a new version of the service to gradually validate new features. - **Cross-origin request handling**: Handles cross-origin requests to ensure they can be executed successfully. Before using `HttpContext` for forwarding, make sure the following two steps have been completed: > **Standalone Library Notes** The `Furion` framework has this feature built in, so no additional `NuGet` package installation is required. If you are using the `HttpAgent` standalone library, install `HttpAgent.AspNetCore` instead of `HttpAgent`. 1. Register and enable the `IHttpContextAccessor` service. In the `Startup.cs` or `Program.cs` file, register and enable the `IHttpContextAccessor` service, and configure the forwarding target whitelist. ```cs showLineNumbers {1,4,7} services.AddHttpContextAccessor(); // Not required when using the Furion framework (already injected by default) // Configure HttpContext forwarding options globally services.Configure(options => { // The target host whitelist for forwarding; must be configured explicitly. If not configured or empty, any forwarding through the X-Forward-To header will be rejected options.AllowedHosts = ["*"]; // "*" allows all hosts and protocols (high risk; recommended only in trusted environments) }); ``` **`AllowedHosts` whitelist rules in detail:** - `"furion.net"` — Host name only; matches the default port (`80/443`) of any protocol (`http/https`). - `"furion.net:8080"` — Host + port; matches the specified port of any protocol. - `"furion.net:*"` — Host + port wildcard; matches any port under any protocol. - `"https://furion.net"` — Protocol + host; matches only the default port of the specified protocol. - `"http://furion.net:8080"` — Protocol + host + port; exact match. - `"https://furion.net:*"` — Protocol + host + port wildcard; matches only any port of the specified protocol. - `"[::1]"` — `IPv6` host (wrapped in square brackets); matches the default port of any protocol. - `"[::1]:8080"` — `IPv6` host + port; matches the specified port of any protocol. - `"[::1]:*"` — `IPv6` host + port wildcard; matches any port under any protocol. - `"http://[2001:db8::1]:8080"` — Protocol + IPv6 host + port; exact match. - `"*"` — Global wildcard; allows any host and protocol (completely bypasses all host validation). > **Security Risk Warning** - **Be sure to explicitly configure `AllowedHosts`**; leaving it empty or unconfigured will reject all `X-Forward-To` forwarding requests to prevent `SSRF` attacks. - Using the global wildcard `*` fully exposes the application to `SSRF` risk; enable it only when the request source is fully trusted (such as an internal management service) and the risks are understood. - Whenever possible, use the strictest rules (such as specifying the protocol and port), and combine them with a network firewall to restrict outbound traffic. - All host name and protocol matching is case-insensitive to prevent case-confusion bypasses. 2. Enable the request body buffering middleware to support repeated reading of the request content. ```cs showLineNumbers app.UseEnableBuffering(); ``` 3. **(Optional)** If a certificate error such as `The SSL connection could not be established, see inner exception.` occurs during forwarding, you can ignore `SSL` certificate validation by adding the following configuration: ```cs showLineNumbers {3,6-7,12,14,17-18} // Default client configuration services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { // Ignore SSL certificate validation ServerCertificateCustomValidationCallback = HttpRemoteUtility.IgnoreSslErrors, SslProtocols = HttpRemoteUtility.AllSslProtocols }); // If using SocketsHttpHandler, you can ignore SSL certificate validation with the following configuration services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler() { SslOptions = new SslClientAuthenticationOptions { // Ignore SSL certificate validation RemoteCertificateValidationCallback = HttpRemoteUtility.IgnoreSocketSslErrors, EnabledSslProtocols = HttpRemoteUtility.AllSslProtocols }, }); ``` The following is a simple example showing how to implement `HttpContext` forwarding in `ASP.NET Core`: ```cs showLineNumbers {3,10,18-19,27-28,35-35} [ApiController] [Route("[controller]/[action]")] public class GetStartController(IHttpRemoteService httpRemoteService, IHttpContextAccessor httpContextAccessor) : ControllerBase { // Forward proxy to a website [HttpGet] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] // Disable browser caching public Task ForwardToWebSite() { return httpContextAccessor.HttpContext.ForwardAsResultAsync("https://github.com"); } // Forward proxy to an image [HttpGet] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] // Disable browser caching public Task ForwardToImage() { return httpContextAccessor.HttpContext.ForwardAsResultAsync( "https://img-s-msn-com.akamaized.net/tenant/amp/entityid/AA1u7RJI.img?w=584&h=326&m=6"); } // Forward proxy to a download [HttpGet] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] // Disable browser caching public Task ForwardToDownload() { return httpContextAccessor.HttpContext.ForwardAsResultAsync( "https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe"); } // Forward proxy to a form [HttpPost] public Task ForwardToForm(int id, [FromForm] YourRemoteFormModel model) { return httpContextAccessor.HttpContext.ForwardAsAsync( "https://localhost:7044/HttpRemote/AddForm"); } } ``` > **The `X-Forward-To` Request Header** In addition to manually configuring the forwarding target address, the system also supports automatically setting the target address by parsing the `X-Forward-To` request header. **Note**: When using this header, the target host must be in the `AllowedHosts` whitelist; otherwise, the forwarding will be rejected. > **Possible Causes of `GET` Request Forwarding Failure** In certain special application scenarios, such as forwarding a `GET` request to a specific file or image, forwarding may fail. This may be caused by `TLS/SSL` certificate issues. In this case, make sure the target application used for forwarding deploys its website over the `HTTPS` protocol. Through `HttpContext` forwarding, flexible request routing and processing mechanisms can be implemented in `ASP.NET Core` applications in combination with `Middleware` middleware technology, making it suitable for various application scenarios such as `API Gateway`, load balancing, request logging, security validation, and more. --- # 2.16 WebService Interface Requests (SOAP) > Source: https://http.furion.net/en/docs/quick-start/webservice/ `WebService` is an application based on `SOA` (Service-Oriented Architecture) that is language- and platform-independent. It implements cross-language interoperation through `XML` descriptions and uses the `HTTP` protocol to interact between network applications on the `Internet`. The framework supports requests to `WebService` interfaces; the following is example code: ### **`SOAP 1.1`** ```cs showLineNumbers {1-3,15} var result = await httpRemoteService.PostAsStringAsync("http://your-host-address/Share/DatabaseManager.asmx", builder => builder.SetSOAPAction("http://tempuri.org/GetDatabaseList") // Can optionally auto-append double quotes: addQuotes: true .SetXmlContent(""" """, Encoding.UTF8)); ``` ### **`SOAP 1.2`** ```cs showLineNumbers {1-2,14} var result = await httpRemoteService.PostAsStringAsync("http://your-host-address/Share/DatabaseManager.asmx", builder => builder.SetXmlContent(""" """, Encoding.UTF8, "application/soap+xml")); ``` > **Ensure the server side supports `SOAP 1.2`.** If the server only supports `SOAP 1.1`, adjust the request to comply with the `SOAP 1.1` specification. Specifically, replace `xmlns:soap12` and `soap12:` in the `XML` content with `xmlns:soap` and `soap:` respectively. > **Differences Between `SOAP 1.1` and `SOAP 1.2`** | Feature | SOAP 1.1 | SOAP 1.2 | | ------------------- | ------------------------------------------- | ----------------------------------------- | | **Namespace** | `http://schemas.xmlsoap.org/soap/envelope/` | `http://www.w3.org/2003/05/soap-envelope` | | **`Content-Type`** | `text/xml` | `application/soap+xml` | | **`SOAPAction` Header** | The `SOAPAction` request header must be set | Optional; the `action` parameter can be used | | **Error Handling** | Uses `SOAP Fault` | Uses `SOAP Fault`, but with a more standardized structure | | **Protocol Support** | Older, widely supported | Newer, supports more features (such as `MTOM`) | In some `XML` returned by `WebService` interfaces, the `soap:Body` node may be `Base64`-encoded and `GZip`-compressed. In this case, you can decode and decompress it with the following code: ```cs showLineNumbers {2,4,6,9,12-15,18} // Parse the XML using XDocument var xDocument = XDocument.Parse(result!); // SOAP 1.1 var bodyContent = xDocument.Descendants(XName.Get("Body", "http://schemas.xmlsoap.org/soap/envelope/")).FirstOrDefault()?.Value!; // SOAP 1.2 // var bodyContent = xDocument.Descendants(XName.Get("Body", "http://www.w3.org/2003/05/soap-envelope")).FirstOrDefault()?.Value!; // Base64 decode var data = Convert.FromBase64String(bodyContent); // GZip decompression using var input = new MemoryStream(data); await using var gzip = new GZipStream(input, CompressionMode.Decompress); using var output = new MemoryStream(); await gzip.CopyToAsync(output); // Get the actual content var body = Encoding.UTF8.GetString(output.ToArray()); ``` --- # 2.17 Sending from a cURL Command > Source: https://http.furion.net/en/docs/quick-start/from-curl/ > **`cURL` Online Testing** We recommend using [ReqBin](https://reqbin.com/curl) to test and learn `cURL` commands online. This website provides a large number of ready-to-run `cURL` examples, which is very convenient for debugging APIs. When integrating with or debugging third-party APIs, `cURL` commands are the most common way to describe requests. The framework has a built-in `cURL` command parsing engine that supports initiating HTTP requests directly from a native `cURL` command string in one step, covering common options (such as `-X`, `-H`, `-d`, `-F`, `-u`, `--data-urlencode`, `--max-time`, `--http2`, etc.), and can be freely extended with custom flags. Usage is very simple: pass the `cURL` command directly into `HttpRequestBuilder.FromCurl()`, then send it via `IHttpRemoteService`. ```cs showLineNumbers {2} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromCurl("curl https://furion.net")); ``` The following examples demonstrate specific usage across several scenarios. ### A Regular `GET` Request ```cs showLineNumbers {2} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromCurl("curl https://furion.net")); ``` ### With Query Parameters and a `JSON` Request Body ```cs showLineNumbers {3-8} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromCurl(""" curl -k -X POST 'https://localhost:7044/HttpRemote/AddModel?query1=10&query2=hello' \ -H 'Content-Type: application/json' \ -d '{ "id": 1, "name": "sample" }' """)); ``` ### Multipart Form (File Upload + Regular Fields) ```cs showLineNumbers {3-6} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromCurl(""" curl -k -X POST 'https://localhost:7044/HttpRemote/AddForm?id=100' \ -F 'Id=100' \ -F 'Name=furion' \ -F 'File=@C:\Workspaces\httptest.jpg' """)); ``` File uploads use the `@` prefix. **Paths support local absolute paths** (such as `C:\...`) or **network `URL`s** (such as `@https://example.com/avatar.png`). ### URL-Encoded Form (`application/x-www-form-urlencoded`) Use `-d` to send URL-encoded data: ```cs showLineNumbers {3-5} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromCurl(""" curl -k -X POST 'https://localhost:7044/HttpRemote/AddUrlForm' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'id=200&name=furion' """)); ``` Use `--data-urlencode` to automatically encode special characters such as spaces: ```cs showLineNumbers {3-5} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromCurl(""" curl -k -X POST 'https://localhost:7044/HttpRemote/AddUrlForm' \ --data-urlencode 'id=200' \ --data-urlencode 'name=fu rion' """)); ``` ### Uploading a Single File ```cs showLineNumbers {3-4} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromCurl(""" curl -k -X POST 'https://localhost:7044/HttpRemote/AddFile' \ -F 'file=@C:\Workspaces\httptest.jpg' """)); ``` ### Uploading Multiple Files ```cs showLineNumbers {3-5} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromCurl(""" curl -k -X POST 'https://localhost:7044/HttpRemote/AddFiles' \ -F 'files=@C:\Workspaces\httptest.jpg' \ -F 'files=@C:\Workspaces\httptest.jpg' """)); ``` ### Sending a Raw String (such as `"This is a raw string"`) ```cs showLineNumbers {3-5} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromCurl(""" curl -k -X POST 'https://localhost:7044/HttpRemote/RawString' \ -H 'Content-Type: application/json' \ -d '"This is a raw string"' """)); ``` ### A Request with Authentication Information ```cs showLineNumbers {3-6} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromCurl(""" curl -X POST https://jsonplaceholder.typicode.com/posts \ -H "Content-Type: application/json" \ -u testuser:testpass \ -d '{"title":"Test"}' """)); ``` ### Ignoring Output Options (such as `-o`) Some `cURL` options (such as `-o`, `-v`, `-s`) are output controls. They **do not affect request building** and are automatically ignored. For example: ```cs showLineNumbers {2} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromCurl("curl -o qr.png \"https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=Hello\"")); ``` This command sends the `GET` request and obtains the response content normally, but it does not save the result to a file (file saving must be handled by you). > **Notes** - **The `cURL` command must start with `curl`**, otherwise it throws `InvalidOperationException("The cURL command must start with 'curl'.")`. - The command cannot contain only `curl` (without any arguments); otherwise an exception is likewise thrown. - The framework supports automatic parsing of common options; unrecognized options are skipped without raising an error. - The file upload path must actually exist; otherwise a runtime exception may be raised. ### Extending Custom `cURL` Options The framework's `cURL` parser uses a **pluggable extractor architecture**, where each `cURL` option is handled by an independent `IHttpCurlExtractor` implementation. You can support private `cURL` flags (such as `--my-flag`) by adding custom extractors without modifying the framework source code. #### Implementing a Custom Extractor The most convenient way to create a custom extractor is to inherit from the `HttpCurlExtractorBase` base class, which already encapsulates cursor advancement and argument consumption logic: ```cs showLineNumbers {4,9,14,22,25,28} /// /// Custom --my-flag extractor /// internal sealed class CurlMyFlagExtractor : HttpCurlExtractorBase { /// /// The set of flags to match (case-insensitive) /// protected override string[] Flags => ["--my-flag"]; /// /// Whether an argument is required. Defaults to true; set to false if the flag takes no argument. /// protected override bool RequiresArgument => true; /// /// The specific operation to perform when the flag is matched /// /// Request builder /// The currently matched flag (already lowercased) /// The argument value carried, or null if there is no argument protected override void Extract(HttpRequestBuilder httpRequestBuilder, string flag, string? argument) { // Configure the builder based on the flag here if (!string.IsNullOrWhiteSpace(argument)) { // Example: put the argument value into the X-My-Flag request header httpRequestBuilder.WithHeader("X-My-Flag", argument); } } } ``` For more complex scenarios (such as needing to control priority or manage the cursor manually), you can implement the `IHttpCurlExtractor` interface directly; if ordering is needed, additionally implement the `IOrderedHttpCurlExtractor` interface (the smaller the `Order`, the higher the priority). #### Registering a Custom Extractor Custom extractors are injected through a configuration delegate when calling `FromCurl`: ```cs showLineNumbers {2-3} var builder = HttpRequestBuilder.FromCurl( "curl --my-flag hello-world http://example.com", options => options.AddExtractor(new CurlMyFlagExtractor()) ); ``` To remove a built-in extractor, use `options.RemoveExtractor()`. For example: ```cs showLineNumbers {3} var builder = HttpRequestBuilder.FromCurl( "curl http://example.com", options => options.RemoveExtractor() ); ``` #### Context Object Reference `HttpCurlParsingContext` provides rich cursor-control methods: | Member | Description | | :--------------------------- | :-------------------------------------------------- | | `CurrentToken` | Gets the `Token` currently pointed to | | `PeekNext()` | Peeks at the next `Token` (without moving the pointer) | | `Advance(count)` | Advances forward by the specified number of steps (default 1) | | `CurrentTokenMatches(flags)` | Checks whether the current `Token` matches the given set of flags (case-insensitive) | | `IsEndOfTokens` | Whether the end of the `Token` list has been reached | When implementing `IHttpCurlExtractor` directly, you must call `Advance` yourself to consume `Token`s; otherwise it leads to an infinite parsing loop. #### Reference Implementations All built-in extractors (such as `CurlMethodExtractor`, `CurlFormExtractor`, etc.) are built on the same interfaces and base classes. You can view their source code in the repository as a reference: [View built-in extractor source code](https://github.com/monksoul/HttpAgent/tree/master/src/HttpAgent/src/Parsers/cURL/Extractors) --- # 2.18 Sending from JSON > Source: https://http.furion.net/en/docs/quick-start/from-json/ The framework also supports initiating an `HTTP` request from a `JSON` configuration string in one step, completely replacing traditional chained calls. Simply organize the request parameters into `JSON` format, pass them to the `HttpRequestBuilder.FromJson()` method, and then send via `IHttpRemoteService`. ```cs showLineNumbers {3-6} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://furion.net", "method": "GET" } """)); ``` ### Complete `JSON` Syntax Reference The following table lists all available `JSON` fields (property names are case-insensitive, and trailing commas are supported): | Field (Primary Key) | Aliases | Type | Required | Description | | :------------ | :------------------------------------ | :-------- | :--- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `method` | – | `string` | No | The request method (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`, etc.). If not specified, it is automatically inferred based on whether a request body is included: `POST` when a body is present, otherwise `GET`. | | `url` | `requestUri` | `string` | Yes | The request address. Supports absolute `URI`s (such as `https://api.furion.net`) or relative paths (such as `/api/data`). Relative paths can be used together with `baseURL`. | | `baseURL` | `baseAddress`, `baseUrl` | `string` | No | The request base address. Must be an absolute `URI`. When `url` is a relative path, the two are combined into a complete address according to the rules. | | `headers` | – | `object` | No | A dictionary of request headers. Keys are header names and values are header values (strings). For example `{"Accept": "application/json", "X-API-Key": "xxx"}`. | | `params` | `queries`, `query`, `queryParameters` | `object` | No | `URL` query parameters. They are automatically appended after the `?` in the request address. For example `{"page": 1, "size": 10}` → `?page=1&size=10`. | | `cookies` | – | `object` | No | A `Cookies` dictionary. For example `{"session": "abc", "user": "john"}`. | | `timeout` | – | `number` | No | The timeout duration (in milliseconds). For example `5000` means `5` seconds. | | `client` | `clientName`, `httpClientName` | `string` | No | The name of a client registered in `IHttpClientFactory`. Used to select a specific `HttpClient` instance. | | `httpVersion` | `version` | `string` | No | The `HTTP` version. Supports `"1.0"`, `"1.1"`, `"2.0"`, `"3.0"`, etc. | | `auth` | `authentication`, `authorization` | `object` | No | Authentication configuration. Must include a `type` field (`"bearer"`, `"basic"`, or `"digest"`). **Bearer example**: `{"type": "bearer", "token": "xxx"}`, with an optional `"header"` custom header name (default `Authorization`). **Basic example**: `{"type": "basic", "username": "user", "password": "pass"}`. **Digest example**: `{"type": "digest", "username": "user", "password": "secret"}`. | | `data` | – | `any` | No | The request body content. Can be a `JSON` object, string, number, etc. The framework passes the `JsonNode` as raw content, and the `Content-Type` is ultimately inferred by the content processor. | | `contentType` | – | `string` | No | Used together with `data` to explicitly specify `Content-Type`. If not specified, the framework automatically infers it from the actual type of `data` (for example, a `JSON` object is inferred as `application/json`). | | `encoding` | – | `string` | No | Used together with `data` to specify the content encoding (such as `"utf-8"`). If not specified, the default encoding is used. | | `multipart` | – | `object` | No | Multipart form (`multipart/form-data`) content. Each property of the object represents a form item. Regular field: `"name": "John"` → a text field. File field: `"file": "@C:\\path\\to\\file.jpg"` or `"@https://example.com/file.png"` (a network file). Supports the `@file;type=mime/type` and `@file;filename=renamed.txt` syntaxes. Multiple file upload: `"files": ["@file1.jpg", "@file2.jpg"]` (array form, same field name). | | `profiler` | `debugger` | `boolean` | No | Whether to enable the request profiler. `true` enables it, `false` disables it. | **Note**: When `method` is not specified, the framework automatically infers it based on whether `data` or `multipart` is present: `POST` when present, otherwise `GET`. ### A Regular `GET` Request ```cs showLineNumbers {3-6} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://furion.net", "method": "GET" } """)); ``` ### With Query Parameters and a `JSON` Request Body ```cs showLineNumbers {6-16} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://localhost:7044/HttpRemote/AddModel", "method": "POST", "queries": { "query1": 10, "query2": "hello" }, "headers": { "Content-Type": "application/json" }, "data": { "id": 1, "name": "sample" } } """)); ``` ### Multipart Form (File Upload + Regular Fields) ```cs showLineNumbers {9-13} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://localhost:7044/HttpRemote/AddForm", "method": "POST", "queries": { "id": 100 }, "multipart": { "Id": 100, "Name": "furion", "File": "@C:\\Workspaces\\httptest.jpg" } } """)); ``` File field values start with `@` and support local absolute paths or network URLs (such as `"@https://example.com/avatar.png"`). Extended syntaxes such as `@path;type=image/png` and `@path;filename=photo.jpg` are also supported. ### `URL`-Encoded Form ```cs showLineNumbers {6-9} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://localhost:7044/HttpRemote/AddUrlForm", "method": "POST", "headers": { "Content-Type": "application/x-www-form-urlencoded" }, "data": "id=200&name=furion" } """)); ``` Or serialize automatically through an object (which requires explicitly specifying `contentType`): ```cs showLineNumbers {6-10} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://localhost:7044/HttpRemote/AddUrlForm", "method": "POST", "data": { "id": 200, "name": "fu rion" }, "contentType": "application/x-www-form-urlencoded" } """)); ``` ### Single File Upload ```cs showLineNumbers {6-8} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://localhost:7044/HttpRemote/AddFile", "method": "POST", "multipart": { "file": "@C:\\Workspaces\\httptest.jpg" } } """)); ``` ### Multiple File Upload ```cs showLineNumbers {6-8} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://localhost:7044/HttpRemote/AddFiles", "method": "POST", "multipart": { "files": ["@C:\\Workspaces\\file1.jpg", "@C:\\Workspaces\\file2.jpg"] } } """)); ``` ### Sending a Raw String ```cs showLineNumbers {9} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://localhost:7044/HttpRemote/RawString", "method": "POST", "headers": { "Content-Type": "application/json" }, "data": "\"This is a raw string\"" } """)); ``` ### Request with Authentication Information ```cs showLineNumbers {9-13} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://jsonplaceholder.typicode.com/posts", "method": "POST", "headers": { "Content-Type": "application/json" }, "auth": { "type": "basic", "username": "testuser", "password": "testpass" }, "data": { "title": "Test" } } """)); ``` ### Specifying Timeout and `HTTP` Version ```cs showLineNumbers {6-7} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://httpbin.org/delay/3", "method": "GET", "timeout": 5000, "httpVersion": "2.0" } """)); ``` ### Enabling the Request Profiler ```cs showLineNumbers {6} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://furion.net", "method": "GET", "profiler": true } """)); ``` > **Notes** - The provided `JSON` must be a valid `JSON` object (starting with `{` and ending with `}`). If an array, a string, or a null value is passed, an `ArgumentException("The provided JSON must be a valid JSON object.")` is thrown. - Property names are case-insensitive, and trailing commas are allowed. - When `method` is not explicitly specified, if `data` or `multipart` is present, it is automatically inferred as `POST`; otherwise it is `GET`. - File upload paths must be ensured to actually exist (for local files) or be network-reachable (for remote files). ### Extending a Custom `JSON` Extractor Similar to the `cURL` parser, `JSON` parsing also uses a **pluggable extractor architecture**. Each `JSON` field is handled by an `IHttpJsonExtractor` implementation. You can add custom extractors to support private fields (such as `"customField"`) without modifying the framework source code. #### Implementing a Custom Extractor The most convenient approach is to inherit from the `HttpJsonExtractorBase` abstract base class, which already encapsulates the property-name and alias matching logic: ```cs showLineNumbers {6,11,16,25-28} using HttpAgent; /// /// Custom json.customField extractor /// internal sealed class JsonCustomFieldExtractor : HttpJsonExtractorBase { /// /// Primary property name /// protected override string PropertyName => "customField"; /// /// Optional alias list /// protected override string[]? Aliases => ["custom", "custom_field"]; /// /// Concrete operation executed when a property is matched /// protected override void Extract(HttpRequestBuilder httpRequestBuilder, JsonNode node, HttpJsonParsingContext context) { // Set the builder according to the node value here if (node is JsonValue jsonValue && jsonValue.TryGetValue(out var value)) { httpRequestBuilder.WithHeader("X-Custom-Field", value); } } } ``` For more complex scenarios, you can directly implement the `IHttpJsonExtractor` interface and manually iterate over the root `JsonObject`. #### Registering a Custom Extractor Inject it through the configuration delegate when calling `FromJson`: ```cs showLineNumbers {5,8} var builder = HttpRequestBuilder.FromJson(""" { "url":"http://example.com", "method":"GET", "customField":"hello" } """, options => options.AddExtractor(new JsonCustomFieldExtractor()) ); ``` To remove a built-in extractor, use `options.RemoveExtractor()`. For example: ```cs showLineNumbers {6} var builder = HttpRequestBuilder.FromJson(""" { "url":"http://example.com" } """, options => options.RemoveExtractor() ); ``` #### Context Object Description `HttpJsonParsingContext` provides safe access methods for the root `JsonObject`: | Member | Description | | :----------------------------------- | :---------------------------------------------------- | | `RootObject` | Gets the root `JsonObject` | | `TryGetNode(propertyName, out node)` | Safely gets the `JsonNode` with the specified property name; returns `false` if it does not exist | | `GetNode(propertyName)` | Gets the `JsonNode` with the specified property name; returns `null` if it does not exist | | `ContainsProperty(propertyName)` | Checks whether the root object contains the specified property | #### Reference Implementation All extractors built into the framework (such as `JsonMethodExtractor`, `JsonMultipartExtractor`, and so on) are built on the same base class and interface. You can view their source code in the repository as a reference: [View built-in `JSON` extractor source code](https://github.com/monksoul/HttpAgent/tree/master/src/HttpAgent/src/Parsers/JSON/Extractors) --- # 2.19 OData API Requests > Source: https://http.furion.net/en/docs/quick-start/odata/ `OData` (`Open Data Protocol`) is a `REST`-based `Web` protocol that lets you query an `API` through `URL` parameters just like operating on a database: filtering, sorting, paging, selecting fields, and so on. For example: - `$filter=Country eq 'China'` filters - `$select=CustomerID,CompanyName` specifies the fields - `$top=10` takes only the first 10 records Many Microsoft services (`Dynamics 365`, `Microsoft Graph`) support `OData`. The framework supports requests to `OData` APIs; the following is sample code: ```cs showLineNumbers {2-7,10-11} var result = await httpRemoteService.GetAsStringAsync("https://your-host-address/odata/Customers", builder => builder.WithQueryParameters(new Dicitionary { {"$top", "2"}, {"$select", "CustomerID,CompanyName"}, {"$format", "json"} })); // Parse the JSON content (a dynamic object such as `Clay` is recommended) var node = JsonNode.Parse(result!); var customers = node.Deserialize>(); ``` --- # 2.20 HTTP Request and Response Assertions (Assert) > Source: https://http.furion.net/en/docs/quick-start/assertions/ During development and testing, you often need to validate the request content and response result — that is, "assertions". The system divides assertions into two categories: - **Request assertions**: executed after `HttpRequestMessage` is built and before it is sent, used to validate the request's `URI`, method, headers, body, and so on. - **Response assertions**: executed after `HttpResponseMessage` is received, used to validate the status code, response headers, response body, elapsed time, and so on. Both types of assertions are enabled through `UseAssertions()` and configured uniformly in `Asserts(configure)`. If an assertion fails, an `HttpAssertionException` is thrown. ```cs showLineNumbers {2-3,5-6,8-9} HttpRequestBuilder.Get("https://furion.net") .UseAssertions() .Asserts(ast => ast // Request assertion: checked immediately before sending; the request is not sent on failure .RequestMethod(HttpMethod.Get) .RequestUri("https://furion.net/") // Response assertion: checked after the response is received .ResponseStatusCode(200) .ResponseHeaderExists("encoding") ); ``` Here, the `ast` parameter is of type `HttpAssertionBuilder`, which provides the following common assertion methods (custom extensions are supported): ### Request Assertion Methods (Executed Before Sending) - **`RequestUri(expectedUri)`**: asserts that the request `URI` equals the specified string - Thrown on failure: `Expected request URI to be '{expectedUri}', but found '{actual}'.` - **`RequestMethod(expectedMethod)`**: asserts that the `HTTP` method equals the specified `HttpMethod` - Thrown on failure: `Expected request method to be {expectedMethod}, but found {actual}.` - **`RequestHeaderExists(name)`**: asserts that the specified request header exists (including content headers) - Thrown on failure: `Expected request header '{name}' to exist, but it was not found.` - **`RequestHeaderEquals(name, expectedValue)`**: asserts that the first value of the request header strictly equals the specified string (case-sensitive) - Thrown on failure: `Expected request header '{name}' to be '{expectedValue}', but found '{actual}'.` - **`RequestHeaderContains(name, expectedValue)`**: asserts that any value of the request header contains the specified substring (case-insensitive) - Thrown on failure: `Expected request header '{name}' to contain '{expectedValue}', but the header was not found.` or `Expected request header '{name}' to contain '{expectedValue}', but actual values were: [{...}].` - **`RequestContentContains(expectedSubstring)`**: asserts that the request content contains the specified substring (case-insensitive) - Thrown on failure: `Expected request content to contain '{expectedSubstring}', but it was not found.` - **`RequestContentEquals(expected)`**: asserts that the request content exactly equals the specified string - Thrown on failure: `Expected request content to be '{expected}', but found '{actual}'.` - **`RequestSatisfies(assertion)`**: custom request assertion (synchronous or asynchronous) that directly operates on `HttpRequestMessage` - The asynchronous overload accepts `Func`. ### Response Assertion Methods (Executed After Receiving a Response) - **`AddAssertion(assertion)`**: Adds a custom assertion delegate (treated as a response assertion by default), such as `ast.AddAssertion(async context => await ...)`. - **`ResponseStatusCode(statusCode)`**: Asserts that the response status code equals the specified value (an integer or `HttpStatusCode`) - Throws on failure: `Expected response status code to be {expected}, but found {actual}.` - **`ResponseStatusCodeIn(allowedStatusCodes)`**: Asserts that the status code is in the allowed list - Throws on failure: `Expected response status code to be one of [{string.Join(", ", allowedStatusCodes)}], but found {actual}.` - **`ResponseIsSuccessStatusCode()`**: Asserts that the request succeeded (status code is `2xx`) - Throws on failure: `Expected response to be successful (2xx status code), but found status code {(int)context.StatusCode}.` - **`ResponseContentContains(expectedSubstring)`**: Asserts that the response content contains the specified substring (case-insensitive) - Throws on failure: `Expected response content to contain '{expectedSubstring}', but it was not found.` - **`ResponseContentEquals(expected)`**: Asserts that the response content exactly equals the specified string - Throws on failure: `Expected response content to be '{expected}', but found '{content}'.` - **`ResponseContentMatches(pattern)`**: Asserts that the response content matches the specified regular expression - Throws on failure: `Expected response content to match regex '{pattern}', but it did not.` - **`ResponseContentNotEmpty()`**: Asserts that the response content is not empty - Throws on failure: `Expected response content not to be empty.` - **`ResponseHeaderExists(name)`**: Asserts that the specified response header exists (including content headers) - Throws on failure: `Expected response header '{name}' to exist, but it was not found.` - **`ResponseHeaderEquals(name, expectedValue)`**: Asserts that the first value of the response header strictly equals the specified string (case-sensitive) - Throws on failure: `Expected response header '{name}' to be '{expectedValue}', but found '{actualValue}'.` - **`ResponseHeaderContains(name, expectedValue)`**: Asserts that any value of the response header contains the specified substring (case-insensitive) - Throws on failure: `Expected response header '{name}' to contain '{expectedValue}', but the header was not found.` or `Expected response header '{name}' to contain '{expectedValue}', but actual values were: [{string.Join(", ", values)}].` - **`ResponseHeaderNotExists(name)`**: Asserts that the specified response header does not exist (including content headers) - Throws on failure: `Expected response header '{name}' not to exist, but it was found.` - **`ResponseDurationUnder(maxMilliseconds)`**: Asserts that the request duration is under the specified number of milliseconds - Throws on failure: `Expected response duration to be under {maxDuration.TotalMilliseconds:F2}ms, but it took {actualDuration.TotalMilliseconds:F2}ms.` - **`ResponseSatisfies(assertion)`**: A custom response assertion (synchronous or asynchronous) that directly operates on `HttpResponseMessage` - The asynchronous overload accepts `Func`. ### Custom Assertion Methods In addition to the built-in methods, you can add custom assertion logic to `HttpAssertionBuilder` via extension methods to reduce duplicated code and improve readability. For example, implement a `ResponseIsJson` method to verify whether the response content is of type `application/json`: ```cs showLineNumbers {1,3,5,11-16} public static class HttpAssertionBuilderExtensions { public static HttpAssertionBuilder ResponseIsJson(this HttpAssertionBuilder httpAssertionBuilder) { return httpAssertionBuilder.AddAssertion(async context => { var contentType = context.ResponseMessage?.Content?.Headers.ContentType?.MediaType; const string jsonMediaType = "application/json"; // Allows "application/json" or "application/json; charset=utf-8", etc. if (string.IsNullOrEmpty(contentType) || !contentType.StartsWith(jsonMediaType, StringComparison.OrdinalIgnoreCase)) { await HttpAssertionException.ThrowAsync( $"Expected response Content-Type to be '{jsonMediaType}' (or a subtype with parameters), but found '{contentType}'."); } }); } } ``` Example of using the custom method: ```cs showLineNumbers {2-3} HttpRequestBuilder.Get("https://furion.net") .UseAssertions() .Asserts(ast => ast.ResponseIsJson().ResponseStatusCode(200)); // Supports chained calls ``` With `C#` extension methods, you can flexibly extend the functionality of `HttpAssertionBuilder`, improving the maintainability and reusability of your code. --- # 2.21 JSON Response Deserialization Wrapper > Source: https://http.furion.net/en/docs/quick-start/json-wrapper/ When communicating with third-party `API`s over `HTTP` remote calls, a unified `JSON` response structure is usually returned, such as the `ApiResult` type, where the actual data is stored in the `Data` property: ```cs showLineNumbers {1,4} public class ApiResult { public bool Success { get; set; } public T? Data { get; set; } // Actual returned data } ``` When the `JSON` response deserialization wrapper feature is not enabled, each call needs to explicitly specify the `ApiResult` type: ```cs showLineNumbers {1} var content = await httpRemoteService.SendAsAsync>( HttpRequestBuilder.Get("https://furion.net")); ``` ### Enabling #### 1. Enable Once To simplify the calling flow, you can configure the `JSON` response deserialization wrapper so that it automatically extracts the contents of the `Data` property: ```cs showLineNumbers {2-3,5} // Configure the default HTTP client services.AddHttpClient(string.Empty) .ConfigureOptions(options => { options.JsonResponseWrapper = new JsonResponseWrapper(typeof(ApiResult<>), nameof(ApiResult<>.Data)); }); ``` Once configured, enable the feature by calling `UseJsonResponseWrapper()`. After that, you only need to specify the target data type without repeatedly declaring `ApiResult`: ```cs showLineNumbers {1-2} var content = await httpRemoteService.SendAsAsync( HttpRequestBuilder.Get("https://furion.net").UseJsonResponseWrapper()); ``` The framework will automatically create an `ApiResult` instance at runtime and return the value of its `Data` property. #### 2. Enable Globally (Takes Effect for All Requests by Default) You can also enable the `JSON` response deserialization wrapper feature globally by simply setting `UseJsonResponseWrapper` to `true`: ```cs showLineNumbers {2-3,6} // Configure the default HTTP client services.AddHttpClient(string.Empty) .ConfigureOptions(options => { options.JsonResponseWrapper = new JsonResponseWrapper(typeof(ApiResult<>), nameof(ApiResult<>.Data)); options.UseJsonResponseWrapper = true; }); ``` After global enablement, all requests use the wrapper feature by default: ```cs showLineNumbers {2} var content = await httpRemoteService.SendAsAsync( HttpRequestBuilder.Get("https://furion.net")); // No need to explicitly call UseJsonResponseWrapper() ``` #### 3. Disable Once (Override the Global Setting) If you need to disable the feature for a specific request, call the following method: ```cs showLineNumbers {1,2} var content = await httpRemoteService.SendAsAsync>( HttpRequestBuilder.Get("https://furion.net").UseJsonResponseWrapper()); ``` By default, not calling `UseJsonResponseWrapper()` means the feature is not enabled, in which case you must pass the complete response type, unless `UseJsonResponseWrapper = true` is configured globally. ### Custom Result Handling (`ResultHandler`) Sometimes, in addition to extracting `Data`, you also need to perform additional validation or transformation on the response. This can be achieved through the `ResultHandler` callback: ```cs showLineNumbers {7,12,16,19} // Configure the default HTTP client services.AddHttpClient(string.Empty) .ConfigureOptions(options => { options.JsonResponseWrapper = new JsonResponseWrapper(typeof(ApiResult<>), nameof(ApiResult<>.Data)) { ResultHandler = context => { if (context.Instance is { } instance) { // Can access the wrapper type instance to get any of its properties var success = context.GetPropertyValue(nameof(ApiResult<>.Success)); } // For example, ensure the request succeeded context.ResponseMessage.EnsureSuccessStatusCode(); // Return the final target result (i.e. the value of Data) return context.Result; } }; }); ``` Through `ResultHandler`, you can execute any custom logic (such as validation, transformation, or exception handling) before returning the final data, making request handling more flexible. The type of the `context` parameter is `JsonResponseWrapperContext`, which contains the following properties and methods: - **Properties**: - `Instance`: The concrete instance of the wrapper type (such as `ApiResult`, of type `object?`). - `Result`: The target result (i.e. the value of `Data`, of type `object?`). - `ResponseMessage`: The response message (of type `HttpResponseMessage`). - **Methods**: - `GetPropertyValue(propertyName)`: Gets the value of a specified property of the concrete wrapper type (i.e. `Instance`). --- # 2.22 Automatic Access Token Management > Source: https://http.furion.net/en/docs/quick-start/access-token/ When integrating with third-party services (such as WeChat Official Accounts, WeChat Work, etc.), you usually need to obtain an `Access Token` first and carry that `Access Token` in subsequent requests to call the API normally. An `Access Token` has a validity period (usually two hours), after which it expires and must be re-obtained and updated. To simplify this process, the framework has a built-in `Access Token` automatic management mechanism: when the `Access Token` does not exist or has expired, it automatically obtains a new `Access Token` and, according to the configuration, injects it into the request's `Header`, `Query`, `Cookie`, and other locations. It also supports automatically retrying when a request fails due to an invalid `Access Token` (such as returning `401`). ### The `HttpAccessToken` Model `HttpAccessToken` represents `Access Token` information and contains the following constructors, properties, and methods: - **Constructors**: - `new(value, expiresAt)`: Passes in the `Access Token` and its absolute expiration time (`UTC` time). - `new(jwtToken)`: Passes in a `JWT Token` string. - **Properties**: - `Value`: The `Access Token` value (of type `string`). - `ExpiresAt`: The absolute expiration time of the `Access Token` (of type `DateTimeOffset`). - `Scheme`: The `HTTP` authentication scheme (of type `string?`). - `RefreshToken`: The refresh token (of type `string?`), internally providing convenient access based on `Items["refresh_token"]`. - `Items`: A shared data dictionary (of type `IDictionary`) used to store custom data related to the `Access Token` (such as `refresh_token`, user identifiers, etc.). - **Static Properties**: - `None`: Indicates that there is no available `Access Token` (of type `HttpAccessToken?`). - **Methods**: - `IsExpired()` checks whether the `Access Token` has expired. - `SetExpiresAt(expiresAt)` sets the absolute expiration time of the `Access Token`. ### Enablement Steps #### 1. Implement the `IHttpAccessTokenProvider` Interface This interface is responsible for defining how to obtain and refresh the `Access Token`. All of its methods receive an `HttpAccessTokenContext` parameter, through which you can obtain custom data (such as username and password) passed in at request time via `context.Items`. Example: ```cs showLineNumbers {1,4,7-8,10} public sealed class WeiXinHttpAccessTokenProvider(IHttpRemoteService httpRemoteService) : IHttpAccessTokenProvider { /// public async Task GetAsync(HttpAccessTokenContext context, CancellationToken cancellationToken) { // Request the WeChat server to obtain the Access Token var weixinToken = await httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://weixin.qq.com/login") .WithoutTokenManagement(), cancellationToken); // Skip Token management to avoid recursive calls (declarative requests use [SuppressTokenManagement]) return new HttpAccessToken(weixinToken.Token, DateTimeOffset.Parse(weixinToken.ExpiresAt)); } } ``` > **Default Refresh Behavior** If `RefreshAsync` is not overridden, the framework will directly call `GetAsync` to complete the refresh. If you need to distinguish the initial retrieval from refreshing (such as using a different endpoint or refreshing based on `refresh_token`), you can override the `RefreshAsync` method. #### 2. Enable automatic `Access Token` management for a specific `HttpClient` client: ```cs showLineNumbers {2-3,5,9-10,12} // Configure the default client services.AddHttpClient(string.Empty) .ConfigureOptions((options, serviceProvider) => { options.AccessTokenProvider = ActivatorUtilities.CreateInstance(serviceProvider); }); // Configure a specific client services.AddHttpClient("weixin") .ConfigureOptions((options, serviceProvider) => { options.AccessTokenProvider = ActivatorUtilities.CreateInstance(serviceProvider); }); ``` After completing the configuration above, all requests issued by that client will automatically manage the `Access Token`. ### Configuring the Injection Location (`Header`, `Query`, `Cookie`, etc.) By default, the `Access Token` is sent in the form of an `Authorization` request header. Developers can specify the authentication scheme (such as `Bearer`) by setting the `HttpAccessToken.Scheme` property: ```cs showLineNumbers {11} public sealed class WeiXinHttpAccessTokenProvider(IHttpRemoteService httpRemoteService) : IHttpAccessTokenProvider { /// public async Task GetAsync(HttpAccessTokenContext context, CancellationToken cancellationToken) { var weixinToken = await httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://weixin.qq.com/login") .WithoutTokenManagement(), cancellationToken); // Skip Token management to avoid recursive calls (declarative requests use [SuppressTokenManagement]) return new HttpAccessToken(weixinToken.Token, DateTimeOffset.Parse(weixinToken.ExpiresAt)) { Scheme = "Bearer" // Specify the Bearer scheme }; } } ``` If you need finer-grained control over how the `Access Token` is carried (such as placing it in a `URL` parameter or a `Cookie`), you can implement the `IHttpAccessTokenConfigurator` interface. It is recommended to implement this interface directly on the `IHttpAccessTokenProvider` implementation class, which both reduces type definitions and makes centralized management easier: ```cs showLineNumbers {2,14,17-20} public sealed class WeiXinHttpAccessTokenProvider(IHttpRemoteService httpRemoteService) : IHttpAccessTokenProvider, IHttpAccessTokenConfigurator { /// public async Task GetAsync(HttpAccessTokenContext context, CancellationToken cancellationToken) { var weixinToken = await httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://weixin.qq.com/login") .WithoutTokenManagement(), cancellationToken); // Skip Token management to avoid recursive calls (declarative requests use [SuppressTokenManagement]) return new HttpAccessToken(weixinToken.Token, DateTimeOffset.Parse(weixinToken.ExpiresAt)); } /// public void Configure(HttpRequestBuilder httpRequestBuilder, HttpAccessToken httpAccessToken) { // Customize the Token injection method httpRequestBuilder.AddBearerAuthentication(httpAccessToken.Value); // Bearer authentication // httpRequestBuilder.WithQueryParameter("access_token", httpAccessToken.Value); // URL parameter // httpRequestBuilder.WithCookie("access_token", httpAccessToken.Value); // Cookie // Other approaches... } } ``` Of course, implementing `IHttpAccessTokenConfigurator` independently is also supported: ```cs showLineNumbers {1,4-8} public sealed class CustomHttpAccessTokenConfigurator : IHttpAccessTokenConfigurator { /// public void Configure(HttpRequestBuilder httpRequestBuilder, HttpAccessToken httpAccessToken) { // Put the Access Token into a custom request header (it is recommended to add replace: true) httpRequestBuilder.WithHeader("X-Custom-Token", httpAccessToken.Value, replace: true); } } ``` Then register the implementation in the service container: ```cs showLineNumbers services.TryAddSingleton(); ``` ### Custom `Access Token` Refresh Trigger Conditions By default, when an `HTTP 401 Unauthorized` response is received, the framework forcibly refreshes the `Access Token` and retries the request. If your `API` indicates that the `Access Token` is invalid through another status code (such as `403`) or through the response content, you can override the `ShouldRefreshAsync` method of the `IHttpAccessTokenProvider` interface: ```cs showLineNumbers {13,16-17,20-21} public sealed class WeiXinHttpAccessTokenProvider(IHttpRemoteService httpRemoteService) : IHttpAccessTokenProvider { /// public async Task GetAsync(HttpAccessTokenContext context, CancellationToken cancellationToken) { var weixinToken = await httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://weixin.qq.com/login") .WithoutTokenManagement(), cancellationToken); // Skip Token management to avoid recursive calls (declarative requests use [SuppressTokenManagement]) return new HttpAccessToken(weixinToken.Token, DateTimeOffset.Parse(weixinToken.ExpiresAt)); } /// public async Task ShouldRefreshAsync(HttpAccessTokenContext context, HttpResponseMessage httpResponseMessage, CancellationToken cancellationToken) { // Example 1: Refresh when the status code is 401 or 403 // return httpResponseMessage.StatusCode == HttpStatusCode.Unauthorized // || httpResponseMessage.StatusCode == HttpStatusCode.Forbidden; // Example 2: Parse the error code in the response content JSON var content = await httpResponseMessage.Content.ReadAsStringAsync(cancellationToken); return content?.Contains("\"errorCode\":\"TOKEN_EXPIRED\"") == true; } } ``` When the method returns `true`, the `Access Token` is forcibly refreshed and the request is retried. **Note that the retry is performed only once to avoid an infinite loop.** ### Passing Custom Data to `IHttpAccessTokenProvider` If you need to pass dynamic parameters (such as username, password, etc.) when obtaining the `Access Token`, you can use the `HttpRequestBuilder.WithAccessTokenData` method. This data is automatically copied into `HttpAccessTokenContext.Items` for use by methods such as `GetAsync`. ```cs showLineNumbers {4-5} var result = await httpRemoteService.SendAsync( HttpRequestBuilder.Get("https://api.furion.net/data") .SetHttpClientName("myapi") // Optional .WithAccessTokenData("username", "admin") .WithAccessTokenData("password", "123456")); ``` ### Manually Setting the `Access Token` (`SetAsync`) In addition to letting the framework automatically call `GetAsync` to obtain the `Access Token`, it also supports manually setting the `Access Token` after a successful login and then overriding `RefreshAsync` to implement refresh logic based on the `RefreshToken`. By injecting the `IHttpAccessTokenManager` interface, call the `SetAsync` method to store the Token in the framework cache: ```cs showLineNumbers {3} // After a successful login, manually set the Access Token var token = new HttpAccessToken(accessToken, expiresAt) { RefreshToken = refreshToken }; await httpAccessTokenManager.SetAsync("myapi", token); ``` After that, the framework reads the `Access Token` from the cache and automatically calls your overridden `RefreshAsync` to refresh it when it expires. At this point `GetAsync` can return `null` or throw an exception (it will not be called). A typical manual refresh implementation is as follows: ```cs showLineNumbers {1,4,6,10-13,19,22} public sealed class ManualTokenProvider : IHttpAccessTokenProvider, IHttpAccessTokenConfigurator { public Task GetAsync(HttpAccessTokenContext context, CancellationToken cancellationToken) => Task.FromResult(HttpAccessToken.None); public async Task RefreshAsync(HttpAccessTokenContext context, HttpAccessToken? currentToken, CancellationToken cancellationToken) { // Use the RefreshToken in the current Token to obtain a new Token var refreshToken = currentToken?.RefreshToken; var response = await httpRemoteService.SendAsync( HttpRequestBuilder.Post("https://auth.furion.net/refresh") .WithHeader("X-Refresh-Token", refreshToken, replace: true) .WithoutTokenManagement(), cancellationToken); // Skip Token management to avoid recursive calls (declarative requests use [SuppressTokenManagement]) return new HttpAccessToken(response.Headers.GetValues("X-Access-Token").First(), DateTimeOffset.UtcNow.AddHours(1)) { RefreshToken = response.Headers.GetValues("X-Refresh-Token").First() }; } public void Configure(HttpRequestBuilder httpRequestBuilder, HttpAccessToken httpAccessToken) { // Put the Access Token into a custom request header (it is recommended to add replace: true) httpRequestBuilder.WithHeader("Authorization", $"Bearer {httpAccessToken.Value}", replace: true); } } ``` --- ### Built-in `FurionAccessTokenProvider` (Furion framework-specific) If your server uses the `JWT` token mechanism of the `Furion` framework, you can directly use the built-in `FurionAccessTokenProvider`. This provider automatically handles the `access-token` and `x-access-token` response headers to implement seamless rolling refresh. **1. Register the provider** ```cs showLineNumbers {4} services.AddHttpClient("furion_api") .ConfigureOptions((options, serviceProvider) => { options.AccessTokenProvider = ActivatorUtilities.CreateInstance(serviceProvider); }); ``` If you prefer to control instantiation manually, you can also pass dependencies explicitly: ```cs showLineNumbers {4} services.AddHttpClient("furion_api") .ConfigureOptions((options, serviceProvider) => { options.AccessTokenProvider = new FurionAccessTokenProvider(serviceProvider.GetRequiredService()); }); ``` **2. Manually set the initial `Access Token` after a successful login** ```cs showLineNumbers {2} var token = new HttpAccessToken(initialAccessToken, expiresAt) { RefreshToken = initialRefreshToken }; await httpAccessTokenManager.SetAsync("furion_api", token); ``` Afterwards, on every request, `FurionAccessTokenProvider` automatically carries `Authorization: Bearer {token}` and appends `X-Authorization: Bearer {refresh_token}` when the `Access Token` expires; the new `Access Token` returned by the server automatically updates the cache via the `access-token` and `x-access-token` response headers, with no additional code required. > Note: `FurionAccessTokenProvider` does not trigger a refresh based on `HTTP 401`, because its refresh logic is entirely driven by response headers. **Be sure to call `SetAsync` to set the initial `Access Token` before first use.** ### Built-in `WeChatAccessTokenProvider` (WeChat Open Platform-specific) If your project needs to call WeChat server-side APIs such as WeChat Official Accounts / Mini Programs, you can directly use the built-in `WeChatAccessTokenProvider`. This provider automatically manages the acquisition, caching, and refresh of `access_token`, and supports automatic retry based on WeChat error codes. **1. Register the provider** Obtaining the WeChat `access_token` requires `appId` and `appSecret`, which are passed in using `ActivatorUtilities.CreateInstance`: ```cs showLineNumbers {4-5} services.AddHttpClient("wechat_api") .ConfigureOptions((options, serviceProvider) => { options.AccessTokenProvider = ActivatorUtilities.CreateInstance( serviceProvider, "YourAppId", "YourAppSecret"); }); ``` **2. Automatic management flow** [https://developers.weixin.qq.com/miniprogram/dev/server/API/mp-access-token/api_getaccesstoken.html](https://developers.weixin.qq.com/miniprogram/dev/server/API/mp-access-token/api_getaccesstoken.html) - **First request**: The provider automatically calls the WeChat `/cgi-bin/token` endpoint to obtain `access_token` and caches it in memory (by default it expires `5` seconds early, to avoid using an invalid `access_token` due to network latency). - **Automatic injection**: The `access_token` is appended to the request `URL` as the query parameter `?access_token=xxx`. - **Expiry refresh**: When the WeChat error codes `40001` (invalid credential), `40014` (invalid `access_token`), or `42001` (`access_token` expired) are received, the framework automatically obtains a new `access_token` and retries the request. - **No manual action required**: The entire lifecycle is managed automatically by the framework; there is no need to call `SetAsync` to manually set the initial `access_token`. **3. Error retry explanation** `WeChatAccessTokenProvider` overrides `ShouldRefreshAsync` and checks both the `HTTP` status code (`401/403`) and the `errcode` in the response `JSON`. Only error codes related to an invalid `access_token` trigger a refresh, avoiding meaningless retries caused by temporary network issues or a busy WeChat system (such as `-1`). --- ### Multi-node Cluster Deployment When the service is deployed in a multi-node cluster environment, the default in-memory cache causes each node to manage the `Access Token` independently. After one node obtains or refreshes the `Token`, the `Token` on other nodes becomes invalid, leading to repeated acquisition and refresh, and even triggering `API` rate limiting. To solve this problem, you can migrate the storage of the `Access Token` from memory to a distributed cache (such as `Redis`). Simply implement the `IHttpAccessTokenManager` interface and replace the default service: ```cs showLineNumbers {1} public class RedisAccessTokenManager : IHttpAccessTokenManager { // Implement the interface methods to store the Access Token in a distributed cache such as Redis } ``` Then replace the default implementation during service registration: ```cs showLineNumbers services.Replace(ServiceDescriptor.Singleton()); ``` After the replacement, all nodes share the same `Access Token`, completely avoiding `Access Token` conflicts and duplicate refresh problems between nodes. --- With the above configuration, the framework automatically handles the acquisition, refresh, and injection of the `Access Token`, so developers do not need to worry about details such as the `Access Token` expiration time or invalid-token retries (for example, automatic resending on `401`), significantly reducing the complexity of integrating with third-party APIs. --- # 2.23 API Call Quota Limits > Source: https://http.furion.net/en/docs/quick-start/quota/ When integrating with third-party `API`s (such as [WeChat](https://developers.weixin.qq.com/doc/service/guide/dev/api/limit.html), payment gateways, etc.), you usually need to comply with their daily/monthly call limits. To avoid business interruptions or bans caused by exceeding the limit, the framework provides a flexible **API call quota limit** feature that supports daily, weekly, monthly, and permanent total-count strategies, and also allows custom strategies. After the quota limit is enabled, each request checks the current count according to the configured strategy. If the limit is reached, the request is **interrupted directly** and an `InvalidOperationException` is thrown (no `HTTP` request is actually sent). Use `HttpRequestBuilder.SetQuotaKey(key)` to assign a quota key to each request and associate it with the quota configuration in `HttpClientOptions`. ### Configuration #### 1. Register the default quota strategies Register the default quota strategies in the `Startup.cs` or `Program.cs` file: ```cs showLineNumbers {3} services.AddHttpRemote(builder => { builder.AddDefaultQuotaStrategies(); // Register the four strategies: daily, weekly, monthly, lifetime }); ``` > **Default Quota Strategies** If you only use custom quota strategies, this step can be skipped, but you must manually register the custom quota strategies (see below). #### 2. Configuring Quota Limits for a Specific `HttpClient` Client When registering an `HttpClient`, use `ConfigureOptions` to set the `QuotaLimits` dictionary, associating quota keys with their corresponding limit strategies: ```cs showLineNumbers {2-3,5-11,15-16,18-24} // Configure the default client services.AddHttpClient(string.Empty) .ConfigureOptions(options => // Or use the overload: .ConfigureOptions((options, serviceProvider) => { options.QuotaLimits = new Dictionary { ["wechat/accesstoken"] = new HttpQuotaLimit("daily", 2000), // Daily limit ["wechat/menu_create"] = new HttpQuotaLimit("weekly", 1000), // Weekly limit ["wechat/upload_media"] = new HttpQuotaLimit("monthly", 50000), // Monthly limit ["wechat/lifetime_stat"] = new HttpQuotaLimit("lifetime", 10000) // Lifetime total }; }); // Configure a specific client services.AddHttpClient("weixin") .ConfigureOptions(options => // Or use the overload: .ConfigureOptions((options, serviceProvider) => { options.QuotaLimits = new Dictionary { ["wechat/accesstoken"] = new HttpQuotaLimit("daily", 2000), // Daily limit ["wechat/menu_create"] = new HttpQuotaLimit("weekly", 1000), // Weekly limit ["wechat/upload_media"] = new HttpQuotaLimit("monthly", 50000), // Monthly limit ["wechat/lifetime_stat"] = new HttpQuotaLimit("lifetime", 10000) // Lifetime total }; }); ``` > **Recommended: Manage Quotas via Configuration Files** When there are many quota entries, it is recommended to place the quota configuration in `appsettings.json` to avoid hardcoding. **`appsettings.json` example:** ```json showLineNumbers {4-6} { "HttpQuotas": { "weixin": { "wechat/accesstoken": { "MaxCount": 2000, "Strategy": "daily" }, "wechat/menu_create": { "MaxCount": 1000, "Strategy": "weekly" }, "wechat/upload_media": { "MaxCount": 50000, "Strategy": "monthly" } } } } ``` **Load and bind:** ```cs showLineNumbers {2,5-6,8} services.AddHttpClient("weixin") .ConfigureOptions((options, serviceProvider) => { // Read the HttpQuotas:weixin configuration node and convert it to a Dictionary var configuration = serviceProvider.GetRequiredService(); var quotas = configuration.GetSection("HttpQuotas:weixin").Get>(); options.QuotaLimits = quotas; }); ``` #### 3. Specifying a Quota Key for a Request When sending a request, use `SetQuotaKey` to associate it with the corresponding quota configuration, so that the request is constrained by the matching rule: ```cs showLineNumbers {3-4} var response = await httpRemoteService.SendAsync( HttpRequestBuilder.Get("https://api.weixin.qq.com/cgi-bin/token") .SetHttpClientName("weixin") .SetQuotaKey("wechat/accesstoken")); // This key is limited to 2000 times per day ``` > **Notes on the `QuotaKey` Configuration Key** - If no quota key is specified, or if the specified key does not exist in `QuotaLimits`, no quota check is performed and the request is sent normally. - The quota key can be any custom string; it is recommended to use a name related to the endpoint path for easy identification and management. ### Built-in Quota Strategies The framework provides four common built-in strategies, specified through the `Strategy` property (case-insensitive): | Strategy name | Description | Window reset rule (based on `UTC` time) | | ---------- | ---------------------------- | ------------------------------- | | `daily` | Daily limit | Resets at `00:00:00` each day | | `weekly` | Weekly limit | Resets at `00:00:00` each Monday | | `monthly` | Monthly limit | Resets at `00:00:00` on the first day of each month | | `lifetime` | Lifetime total (not reset by time) | Never resets; permanently rejected once the limit is reached | For example, configuring `Strategy = "daily"` and `MaxCount = 2000` means at most `2000` calls per day. Configuring `Strategy = "lifetime"` and `MaxCount = 10000` means the quota key can be called at most `10000` times over the entire application lifetime, without being reset over time. ### Custom Quota Strategies You can implement the `IHttpQuotaStrategy` interface to create a strategy with any reset rule (for example, hourly, custom time windows, sliding windows, etc.). **1. Define the strategy class** ```cs showLineNumbers {1,4,7,13-16,20-21,23} public sealed class HourlyQuotaStrategy : IHttpQuotaStrategy { /// public string Name => "hourly"; // Unique name of the strategy /// public bool TryAcquire(HttpQuotaCounter quotaCounter, int maxCount, out int current) { // Use the current UTC hour as the window identifier (format: yyyy-MM-dd HH) var hourKey = DateTime.UtcNow.ToString("yyyy-MM-dd HH"); // If the window identifier changes, a new hour has begun, so reset the counter if (quotaCounter.WindowKey != hourKey) { quotaCounter.Count = 0; quotaCounter.WindowKey = hourKey; } // Increment the counter quotaCounter.Count++; current = quotaCounter.Count; return current <= maxCount; } } ``` **2. Register the custom quota strategy** Register the custom quota strategy in the `Startup.cs` or `Program.cs` file: ```cs showLineNumbers {3} services.AddHttpRemote(builder => { builder.AddQuotaStrategy(); }); ``` After registration, you can use `Strategy = "hourly"` in `QuotaLimits`: ```cs showLineNumbers {3} options.QuotaLimits = new Dictionary { ["some/high_freq_api"] = new HttpQuotaLimit("hourly", 100) // At most 100 calls per hour }; ``` ### Multi-Node Cluster Deployment By default, the quota manager `HttpQuotaManager` maintains counters based on an in-memory cache, which is suitable for single-node or single-instance deployments. In a multi-node cluster environment, each node maintains its own independent counting state, causing the overall quota limit to become ineffective (for example, an endpoint with a global limit of `2000` calls/day could be called `2000` times on each node without mutual awareness). To accurately share quotas across all nodes, you can migrate the counter storage to a distributed cache (such as `Redis`). Simply implement the `IHttpQuotaManager` interface, basing the counting and window-checking logic on distributed atomic operations, and then replace the default service: ```cs showLineNumbers {1} public class RedisHttpQuotaManager : IHttpQuotaManager { // Implement the interface methods, based on Redis for atomic increment, window reset, and over-limit checks } ``` Replace the default implementation during service registration: ```cs showLineNumbers services.Replace(ServiceDescriptor.Singleton()); ``` After replacement, all nodes share the same quota counter, ensuring that the cluster-wide call count always stays within the configured limits. When implementing a custom manager, make sure the window reset and counter increment operations are atomic to avoid exceeding the limit under concurrency. --- With the mechanisms above, you can easily configure differentiated call limits for different endpoints, effectively preventing third-party `API` limits or cost overruns caused by excessive calls. --- # 2.24 Service Discovery (ServiceDiscovery) > Source: https://http.furion.net/en/docs/quick-start/service-discovery/ Service discovery is a mechanism that allows developers to reference external services using logical names rather than physical addresses (such as `IP` addresses and ports). For example, we can use `furion` instead of `https://furion.net`. **The benefit of this approach is that service addresses can be modified through configuration at runtime without changing program code, while also enabling automatic selection of service endpoints to achieve load balancing**. Service discovery is especially common in microservice architectures. To use service discovery in `HTTP` remote requests, follow the steps below to configure it: ### 1. Install the `Microsoft.Extensions.ServiceDiscovery` Package ```cs showLineNumbers dotnet add package Microsoft.Extensions.ServiceDiscovery ``` ### 2. Configure and Enable the `ServiceDiscovery` Service Register and configure the `ServiceDiscovery` service in the `Startup.cs` or `Program.cs` file: ```cs showLineNumbers {1,3,5} services.AddServiceDiscovery(); services.AddHttpRemote() .ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.AddServiceDiscovery(); }); ``` ### 3. Add Service Endpoints in the Configuration File Configure the service endpoints in the `appsettings.json` file. The following example configures two services, `furion` and `weixin`, each containing multiple endpoints. Each time a request is sent, the system automatically selects an endpoint. ```json showLineNumbers {2,3,6} { "Services": { "furion": { "https": ["localhost:5001", "furion.net"] }, "weixin": { "https": ["localhost:8080", "weixin.qq.com"] } } } ``` ### 4. Configure the `BaseAddress` of the `HttpClient` Client Next, configure the `BaseAddress` of the `HttpClient` client so that logical names are used instead of concrete physical addresses when making requests. ```cs showLineNumbers {2,4,8,10} // Configure the default client services.AddHttpClient(string.Empty, client => { client.BaseAddress = new Uri("https://furion"); }); // Configure a specific client, e.g. "weixin" services.AddHttpClient("weixin", client => { client.BaseAddress = new Uri("https://weixin"); }); ``` ### 5. Send the `HTTP` Remote Request Finally, use the configured `HttpClient` to send the remote request: ```cs showLineNumbers {2,5} // Send a request with the default client await httpRemoteService.GetAsStringAsync("docs"); // The request URL is: https://localhost:5001/docs or https://furion.net/docs // Send a request with the "weixin" client await httpRemoteService.GetAsStringAsync("userinfo", builder => builder.SetHttpClientName("weixin")); // The request URL is: https://localhost:8080/userinfo or https://weixin.qq.com/userinfo ``` With the steps above, you can easily implement service discovery functionality in a `.NET` application, simplifying service invocation and improving the flexibility and scalability of the system. To learn more about service discovery in `.NET`, see the [Microsoft official documentation](https://learn.microsoft.com/zh-cn/dotnet/core/extensions/service-discovery). --- # 2.25 HttpRemoteResult return type > Source: https://http.furion.net/en/docs/quick-start/http-remote-result/ `HttpRemoteResult` is a generic type specifically used for the response content in the `HTTP` remote request module. The generic parameter `TResult` represents the final data type to convert to; in addition to supporting common `HTTP` response types such as `string`, `byte[]`, `Stream`, `HttpResponseMessage`, `IAsyncEnumerable`, and `IActionResult`, it also supports custom types and the framework's built-in `VoidContent` type. This type encapsulates commonly used `HTTP` response information and request duration, among other capabilities. In the `HTTP` remote request module, all default generic request methods that do not contain the `As` keyword return a value of type `HttpRemoteResult`. The following are examples of obtaining a return value of type `HttpRemoteResult` in different ways: ```cs showLineNumbers {2,5} // Request verb style using var httpResult = await httpRemoteService.GetAsync("https://furion.net/"); // Builder style using var httpResult = await httpRemoteService.SendAsync(HttpRequestBuilder.Get("https://furion.net/")); ``` `HttpRemoteResult` contains the following properties and methods: - **Properties**: - `ResponseMessage`: the response message (`HttpResponseMessage` type). - `ContentType`: the content type (`string` type). - `CharSet`: the character set (`string` type). - `ContentEncoding`: the content encoding (`ICollection` type). - `ContentLength`: the content size (`long` type). - `Server`: the raw `Server` response header (`HttpHeaderValueCollection` type). - `RawSetCookies`: the raw `Set-Cookie` response header collection (`List` type). - `SetCookies`: the response `Cookie` collection (`IList` type). - `StatusCode`: the response status code (`HttpStatusCode` type). - `IsSuccessStatusCode`: whether the request succeeded (`bool` type). - `Result`: the target data (`TResult` generic type). - `RequestDuration`: the request duration in milliseconds (`long` type). - `Headers`: the response headers (`HttpResponseHeaders` type). - `ContentHeaders`: the response content headers (`HttpContentHeaders` type). - `Version`: the `HTTP` version (`Version` type). - `HttpClientName`: the configuration name of the `HttpClient` instance (`string?` type). - **Methods**: - `ToString()`: outputs an indented detailed request and response information string. > **Return Value Type Note** By default, when the return value type is not `string`, `byte[]`, `Stream`, `HttpResponseMessage`, `VoidContent`, `IAsyncEnumerable`, or `IActionResult`, other types are deserialized using `System.Text.Json`. If you need to change this behavior, you can learn how to implement the `IHttpContentConverter` content converter interface for customization in later chapters. In the latest version, the framework introduced support for [deconstruction](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/deconstruction) for the `HttpRemoteResult` type. The deconstruction expression simplifies the object parsing process, making it more convenient to obtain key property values. The following is the sample code: ```cs showLineNumbers // Deconstruction expression for extracting the required property values var (result, response) = await httpRemoteService.GetAsync("https://furion.net/"); // You can call ThrowIfNull()/OrDefault() to resolve null reference warnings var (result, response, isSuccess) = await httpRemoteService.GetAsync("https://furion.net/"); // You can call ThrowIfNull()/OrDefault() to resolve null reference warnings var (result, response, isSuccess, statusCode) = await httpRemoteService.GetAsync("https://furion.net/"); // You can call ThrowIfNull()/OrDefault() to resolve null reference warnings ``` In these examples, `result` is of type `TResult`, `response` is of type `HttpResponseMessage`, `isSuccess` is of type `bool`, and `statusCode` is of type `HttpStatusCode`. Using deconstruction expressions not only improves code readability but also makes the development process more efficient. This improvement allows developers to directly access the required data, reducing the steps of manually obtaining individual property values and making the code more concise and intuitive. --- In addition, the `HttpRemoteResult` type has a built-in `ToString()` method that can clearly print the detailed request header and response header information in an indented format, as shown below: ```cs showLineNumbers Console.WriteLine(httpResult.ToString()); // Or use Console.WriteLine(httpResult); ``` The terminal console output is as follows: ```bash showLineNumbers Request Headers: User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0 traceparent: 00-602c9070b85da9bd73fc1eac36fdb3cb-14dded89e0f5266b-00 General: Request URL: https://furion.net/ Request Method: GET Status Code: 200 OK HTTP Version: 1.1 HTTP Content: Content Type: HttpClient Name: Request Duration (ms): 133.00 Response Headers: Server: nginx/1.22.1 Date: Mon, 18 Nov 2024 21:26:06 GMT Connection: keep-alive Vary: Accept-Encoding ETag: "67091697-f32f" Cache-Control: max-age=315360000 Accept-Ranges: bytes Content-Type: text/html Content-Length: 62255 Last-Modified: Fri, 11 Oct 2024 12:14:15 GMT Expires: Thu, 31 Dec 2037 23:55:55 GMT ``` --- # 2.26 Official DeepSeek Integration > Source: https://http.furion.net/en/docs/quick-start/deepseek/ `DeepSeek` is a multifunctional artificial intelligence model developed by DeepSeek, capable of chat, writing, programming, data analysis, translation, and educational tutoring. Its powerful understanding ability and fast learning speed make it suitable for a variety of scenarios, with great potential for future development. Before integrating the `DeepSeek` AI model, you need to first register an account and create an `API key` on the [`DeepSeek` development platform](https://platform.deepseek.com/). After obtaining the `API key`, you can integrate the `DeepSeek` AI model into your project. The framework provides several ways to integrate the `DeepSeek` AI model: > **`DeepSeek` API Documentation** To learn more about the `DeepSeek` development documentation, please visit: [https://api-docs.deepseek.com/zh-cn/](https://api-docs.deepseek.com/zh-cn/) **1. Standard output (non-streaming)** Standard output (non-streaming) means sending the user prompt all at once and returning the final result. The result is presented all at once, which is suitable for scenarios that require complete output: ```cs showLineNumbers {5-15,18-19} [HttpGet] public async Task DeepSeek(CancellationToken cancellationToken) { var result = await httpRemoteService.PostAsStringAsync("https://api.deepseek.com/chat/completions", HttpRequestBuilder.Setup .AddBearerAuthentication("your-api-key") .SetJsonContent(""" { "model": "deepseek-v4-pro", "messages": [ {"role": "system", "content": "You are a professional C# domain expert."}, {"role": "user", "content": "What is the future of the Furion framework?"} ], "stream": false } """), cancellationToken); // Parse the JSON content (using a mutable object is recommended) var node = JsonNode.Parse(result!); var content = node?["choices"]?[0]?["message"]?["content"]?.GetValue(); return content ?? string.Empty; } ``` > **Tip** In the `JSON` data above, the `messages` array contains two objects, each of which has a `role` key set to `system` and `user` respectively, for example: ```json showLineNumbers {4-5} { "model": "deepseek-v4-pro", "messages": [ { "role": "system", "content": "You are a professional C# domain expert." }, { "role": "user", "content": "How is the future of the Furion framework?" } ], "stream": false } ``` - **`system` role**: used to define the initial identity or skills of the large model. For example, you can set it to "all-round talent", "IT expert", "medical expert", or "historian". This role helps the model understand its task context. - **`user` role**: represents the user's input, i.e., the question or prompt posed by the user. **2. Streaming output (`Server-Sent Events`)** Streaming output can simulate the effect of a typewriter and is ideal for scenarios that require progressively displaying results. The main difference from the standard output mode is that you need to set the `stream` parameter to `true` and use `Server-Sent Events` to implement unidirectional communication. The framework has built-in support for `Server-Sent Events` and can be used directly: ```cs showLineNumbers {5-15,17,20,27-28,32} [HttpGet] public async Task DeepSeek_Stream(CancellationToken cancellationToken) { var builder = HttpRequestBuilder.ServerSentEvents("https://api.deepseek.com/chat/completions") .AddBearerAuthentication("your-api-key") .SetJsonContent(""" { "model": "deepseek-v4-pro", "messages": [ {"role": "system", "content": "You are a professional C# domain expert."}, {"role": "user", "content": "Who is the author of the Furion framework?"} ], "stream": true } """); await foreach (var data in httpRemoteService.SendAsAsyncEnumerable(builder, cancellationToken)) { // Output complete if (data.IsDone) { Console.WriteLine("++++++++++++ End ++++++++++++"); break; } // Parse the JSON content (using a mutable object is recommended) var node = JsonNode.Parse(data.Data); var content = node?["choices"]?[0]?["delta"]?["content"]?.GetValue(); if (!string.IsNullOrEmpty(content)) { Console.WriteLine(content); } } return "OK"; } ``` **3. Streaming output (`Server-Sent Events`) via the browser `URL` address (`Web`)** You can also achieve the streaming output effect by accessing a `URL` address in the browser: ```cs showLineNumbers {7,10-20,22,25,28-29,32,35} [HttpGet] public async Task DeepSeekChat([FromServices] IHttpContextAccessor httpContextAccessor, [FromQuery] string message, CancellationToken cancellationToken) { var httpContext = httpContextAccessor.HttpContext!; // Configure the standard Server-Sent Events (SSE) streaming response format httpContext.Response.EnableServerSentEvents(); var builder = HttpRequestBuilder.ServerSentEvents("https://api.deepseek.com/chat/completions") .AddBearerAuthentication("your-api-key") .SetJsonContent($$""" { "model": "deepseek-v4-pro", "messages": [ {"role": "system", "content": "You are a professional C# domain expert."}, {"role": "user", "content": "{{message}}"} ], "stream": true } """); await foreach (var data in httpRemoteService.SendAsAsyncEnumerable(builder, cancellationToken)) { // DeepSeek output completion flag if (data.IsDone) return; // Parse the JSON content (using a mutable object is recommended) var node = JsonNode.Parse(data.Data); var content = node?["choices"]?[0]?["delta"]?["content"]?.GetValue(); // Write a message to the client and flush the response stream immediately await httpContext.Response.WriteAndFlushAsync(content, cancellationToken); } await httpContext.Response.CompleteAsync(); } ``` Open a browser and visit the following address to experience the streaming output effect: `https://localhost:7044/GetStart/DeepSeekChat?message=How is the Furion framework`. As shown in the image below: ![sse-ai](/images/sse-ai.svg) --- # 2.27 Crawling Web Page Content (Crawler) > Source: https://http.furion.net/en/docs/quick-start/web-crawler/ This section shows three ways to crawl web pages: direct fetching plus an `HTML` parsing library, and headless browser crawling for `JavaScript`-rendered pages. With the `HTTP` remote request module, you can easily crawl any web page content on the internet (commonly known as a "crawler"). Combined with an `HTML` parsing library (such as [AngleSharp](https://github.com/AngleSharp/AngleSharp) or [HtmlAgilityPack](https://github.com/zzzprojects/html-agility-pack)), you can further extract the required data; for pages that rely on `JavaScript` rendering, you can also use a headless browser (such as Playwright) to crawl the fully rendered content. The following uses crawling the blog titles on the cnblogs homepage as an example to demonstrate how to use the two parsing libraries and the headless browser respectively. ### Using `AngleSharp` **1. Install the NuGet package** ```bash showLineNumbers Install-Package AngleSharp ``` **2. Write the crawling code** ```cs showLineNumbers {2-3,9,12,15} // Get the cnblogs homepage HTML string var cnblogs = await httpRemoteService.SendAsStringAsync(HttpRequestBuilder.Get("https://www.cnblogs.com/") .SetUserAgent(UserAgents.GetRandom())); // Random browser User-Agent // Create a context for parsing web pages using the default configuration var context = BrowsingContext.New(Configuration.Default); // Parse the fetched HTML string as the content of a virtual request into an operable document object var document = await context.OpenAsync(req => req.Content(cnblogs)); // Use CSS selectors to query all matching elements in the document: post-item-title class elements under the post_list id var elements = document.QuerySelectorAll("#post_list .post-item-title"); // Extract the text content of each element (i.e., the blog title) var titles = elements.Select(u => u.TextContent).ToList(); ``` ### Using `HtmlAgilityPack` **1. Install the NuGet package** ```bash showLineNumbers Install-Package HtmlAgilityPack ``` **2. Write the crawling code** ```cs showLineNumbers {2-3,9,12,15} // Get the cnblogs homepage HTML string var cnblogs = await httpRemoteService.SendAsStringAsync(HttpRequestBuilder.Get("https://www.cnblogs.com/") .SetUserAgent(UserAgents.GetRandom())); // Random browser User-Agent // Create an HtmlDocument instance for parsing HTML var document = new HtmlDocument(); // Load the fetched HTML string document.LoadHtml(cnblogs); // Use XPath to get a node collection: select all child elements whose class contains post-item-title within the element whose id is post_list var nodes = document.DocumentNode.SelectNodes("//*[@id='post_list']//*[contains(@class,'post-item-title')]"); // Extract the inner text of each node (i.e., the blog title) var titles = nodes.Select(node => node.InnerText).ToList(); ``` ### Using a Headless Browser (`Playwright`) Some pages rely on `JavaScript` dynamic rendering (such as single-page applications `SPA` or asynchronously loaded lists), so fetching the raw `HTML` directly does not yield the final content. In this case, you can use a **headless browser** to execute the page scripts in a real browser engine and then extract the rendered data. [Playwright for .NET](https://playwright.dev/dotnet/) (the `Microsoft.Playwright` package) is currently the most mainstream headless browser library in the `C#` ecosystem (with over 60 million cumulative `NuGet` downloads), officially maintained by Microsoft, runs headless by default, and supports the `Chromium`, `Firefox`, and `WebKit` engines. If you only need `Chromium`, [PuppeteerSharp](https://github.com/hardkoded/puppeteer-sharp) is also a good choice. **1. Install the NuGet package** ```bash showLineNumbers Install-Package Microsoft.Playwright ``` **2. Download the browser engine** Download the browser engine before the first use (taking `Chromium` as an example): ```bash showLineNumbers dotnet tool install --global Microsoft.Playwright.CLI playwright install chromium ``` **3. Write the crawling code** ```cs showLineNumbers {2-3,10,13,16} // Launch Playwright (Chromium runs in headless mode by default) using var playwright = await Playwright.CreateAsync(); await using var browser = await playwright.Chromium.LaunchAsync(); // Create a page and set the viewport size (to simulate a real browser environment) var page = await browser.NewPageAsync(); await page.SetViewportSizeAsync(1920, 1080); // Visit the page and wait for the network to become idle to ensure JavaScript has finished executing await page.GotoAsync("https://www.cnblogs.com/", new() { WaitUntil = WaitUntilState.NetworkIdle }); // Wait for the target element to appear (SPA pages may render asynchronously) await page.WaitForSelectorAsync("#post_list .post-item-title"); // Extract the text of all matching elements (i.e., the blog titles); page.ContentAsync() returns the fully rendered HTML var titles = await page.Locator("#post_list .post-item-title").AllTextContentsAsync(); ``` A headless browser actually executes page scripts and can crawl the final `JavaScript`-rendered content — the strongest of the three approaches; see the notes below for its overhead and usage guidance. ### Notes - **Legal compliance**: be sure to comply with the target website's `robots.txt` protocol and relevant laws and regulations, control the request frequency reasonably, and avoid putting pressure on the server. - **Anti-crawler strategies**: you can appropriately configure strategies such as `User-Agent`, delayed waiting, and proxy `IP` to improve crawling stability. - **Parsing methods**: `AngleSharp` supports `CSS` selectors, and its syntax is closer to frontend development; `HtmlAgilityPack` is based on `XPath`. Both have their own strengths and can be chosen as needed. - **Headless browser**: the browser engine must be downloaded before the first use (hundreds of `MB`), and it consumes more resources than direct fetching, so use it only when the page relies on `JavaScript` rendering; for regular static pages, prefer the direct fetching approaches in the previous two sections, which are lighter and more efficient. By combining `HTTP` remote requests, parsing libraries, and a headless browser, you can quickly meet various web page data collection needs. --- # 2.28 httpbin.org Online Testing Service > Source: https://http.furion.net/en/docs/quick-start/httpbin/ [`httpbin.org`](https://httpbin.org/) is a free, open-source online `HTTP` request and response testing service, created by `Kenneth Reitz`, the author of the well-known `Python` community project `requests`, and now maintained by the `Postman` team. It can echo any request information sent by the client and simulate various `HTTP` scenarios, making it an ideal tool for developing `HTTP` clients and debugging request logic. ### Key Features - Echoes details such as the request method, path, headers, body, parameters, and source `IP`. - Simulates various `HTTP` status codes (such as `404` and `500`). - Tests scenarios such as redirection, delayed responses, file uploads, `Cookie`, and `GZip`. - Provides **`Basic Auth`**, **`Bearer Auth`**, and **`Digest Auth`** endpoints for validating authentication implementations. ### Usage Recommendations - Suitable for integration testing or manual verification during development, without the need to build your own server. - This is a public resource, so **never send real sensitive data**. - Affected by the external network environment, rate limiting or unavailability may occasionally occur; it is recommended that core testing still rely primarily on local `Mock`. - If an offline environment is required, you can deploy it yourself from the [GitHub repository](https://github.com/postmanlabs/httpbin). > **Alternative** If you need a more modern online testing service, you can also try [`httpbun.com`](https://httpbun.com). It is compatible with `httpbin`'s interface. --- # 3.1 HttpRequestBuilder Request Builder > Source: https://http.furion.net/en/docs/request-builder/httprequestbuilder-request-builder/ `HttpRequestBuilder` is a builder tool specifically designed to construct the `HttpRequestMessage` object required when sending requests through `HttpClient`. It can be said that `HttpRequestBuilder` is the core component of the entire `HTTP` remote request module, responsible for preparing all necessary request data before a request is sent. As shown in the following diagram: ![httpagent](/images/httpagent.jpg) [**View the high-definition architecture diagram**](https://github.com/monksoul/HttpAgent/blob/master/drawio/HttpAgent.drawio) --- # 3.2 Creating a Builder Instance > Source: https://http.furion.net/en/docs/request-builder/creating-a-builder-instance/ The constructor of the `HttpRequestBuilder` type is designed to be private, so it cannot be instantiated directly using the `new` keyword. However, it provides multiple static methods to conveniently create instances of `HttpRequestBuilder`. > **Tip** Find `HttpRequestBuilder` too long? Use `HttpBuilder` for a cleaner look! **1. Using request verb static methods (recommended)** `HttpRequestBuilder` provides a variety of static methods based on `HTTP` request methods (such as `GET`, `POST`, etc.) for quickly creating instances. These methods support overloads to accommodate different parameter requirements. ```cs showLineNumbers var httpRequestBuilder = HttpRequestBuilder.Get("https://furion.net/"); // GET request, supports multiple overloads var httpRequestBuilder = HttpRequestBuilder.Put("https://furion.net/"); // PUT request, supports multiple overloads var httpRequestBuilder = HttpRequestBuilder.Post("https://furion.net/"); // POST request, supports multiple overloads var httpRequestBuilder = HttpRequestBuilder.Delete("https://furion.net/"); // DELETE request, supports multiple overloads var httpRequestBuilder = HttpRequestBuilder.Options("https://furion.net/"); // OPTIONS request, supports multiple overloads var httpRequestBuilder = HttpRequestBuilder.Trace("https://furion.net/"); // TRACE request, supports multiple overloads var httpRequestBuilder = HttpRequestBuilder.Patch("https://furion.net/"); // PATCH request, supports multiple overloads var httpRequestBuilder = HttpRequestBuilder.Query("https://furion.net/"); // QUERY request, supports multiple overloads ``` **2. Using the `Create` static method** The `Create` method allows you to create a `HttpRequestBuilder` instance in a more flexible way, supporting direct specification of the request method and `URL`, or the use of a custom `HttpMethod`. ```cs showLineNumbers var httpRequestBuilder = HttpRequestBuilder.Create("GET", "https://furion.net/"); var httpRequestBuilder = HttpRequestBuilder.Create(HttpMethod.Get, "https://furion.net/"); // Custom request verb, such as a CONNECT request var httpRequestBuilder = HttpRequestBuilder.Create("Connect", "https://furion.net/"); ``` **3. Using the `Setup` static property** `Setup` returns a blank `HttpRequestBuilder` instance, specifically used to configure `HttpRequestBuilder` and pass it as an `Action` delegate to verb shortcut methods (such as `GetAsync`, `PostAsync`, etc.). **It automatically converts chained configuration into a delegate through implicit conversion, eliminating the `builder => builder` wrapping.** ```cs showLineNumbers {2,5} // Traditional approach: builder => builder await httpRemoteService.GetAsync("https://furion.net/", builder => builder.UseETag().Profiler()); // ✅ Using Setup instead await httpRemoteService.GetAsync("https://furion.net/", HttpRequestBuilder.Setup.UseETag().Profiler()); ``` > **Applicable Scenarios** `Setup` is primarily used for the `configure` parameter of verb shortcut methods (such as `GetAsync`, `PostAsync`, `PutAsync`, etc.) of the `IHttpRemoteService` interface. It **cannot** be used directly as a complete request builder. **4. Using the `FromCurl` static method** By passing in a native `cURL` command string, `FromCurl` can generate a fully configured `HttpRequestBuilder` instance in one step, saving the tedious steps of manually calling various configuration methods. ```cs showLineNumbers {2,5-6} // Parse the simplest cURL command, automatically recognizing the GET method var httpRequestBuilder = HttpRequestBuilder.FromCurl("curl http://example.com"); // Parse a command with a request body (-d), automatically inferred as POST var httpRequestBuilder = HttpRequestBuilder.FromCurl( "curl -X POST https://api.furion.net/data -H \"Content-Type: application/json\" -d '{\"name\":\"John\"}'"); ``` - **`curlCommand`**: the complete `cURL` command string, which must start with `curl`. - **`configure`**: optional delegate used to register custom flag extractors. > **Notes** - The command must start with `curl`, otherwise an `InvalidOperationException` will be thrown. - If the command contains only `curl` with no arguments, an exception will also be thrown. - The parser automatically infers a `POST` request based on data options such as `-d` and `-F`; otherwise it defaults to `GET`. > **Tip** For all `cURL` options supported by `FromCurl`, usage examples, and how to extend custom extractors, refer to the **2.17 Sending from a cURL Command** section. **5. Using the `FromJson` static method** By passing in a `JSON` configuration string, `FromJson` can generate a fully configured `HttpRequestBuilder` instance in one step, saving the tedious steps of manual chained calls. ```cs showLineNumbers {2,5-6} // Parse the simplest JSON configuration, automatically recognizing the GET method var httpRequestBuilder = HttpRequestBuilder.FromJson(""" { "url":"http://example.com", "method":"GET" } """); // Parse a JSON configuration with a request body, automatically inferred as POST var httpRequestBuilder = HttpRequestBuilder.FromJson(""" { "url": "https://api.furion.net/data", "method": "POST", "headers": { "Content-Type": "application/json" }, "data": { "name": "John" } } """); ``` - **`json`**: the complete `JSON` configuration string, which **must be a `JSON` object** (starting with `{` and ending with `}`); property names are case-insensitive and trailing commas are allowed. - **`configure`**: optional delegate used to register custom `JSON` field extractors. > **Notes** - The configuration must be a valid `JSON` object; if an array, string, or null value is passed, an `ArgumentException("The provided JSON must be a valid JSON object.")` will be thrown. - If `method` is not explicitly specified, the parser automatically infers based on whether the `data` or `multipart` field exists: `POST` if there is a request body, otherwise `GET`. > **Tip** For all `JSON` fields supported by `FromJson`, usage examples, and how to extend custom extractors, refer to the **2.18 Sending from `JSON`** section. --- # 3.3 Setting the Request Address > Source: https://http.furion.net/en/docs/request-builder/setting-the-request-address/ In the static methods provided by the `HttpRequestBuilder` type, you can configure the request address. The following shows how to use the static methods of the `HttpRequestBuilder` type to define different request addresses: ```cs showLineNumbers {2,5,8,11,14} // Using a full URL address HttpRequestBuilder.Get("https://furion.net/"); // Using a relative address (without a leading slash) HttpRequestBuilder.Get("api/get/user"); // Using a relative address (with a leading slash) HttpRequestBuilder.Get("/api/get/user"); // Request address is an empty string HttpRequestBuilder.Get(""); // string.Empty can also be used instead // Request address is null HttpRequestBuilder.Get(null); ``` - When the provided request address is a full `URL`, it is used directly as the final request address. - If the request address is a relative address (whether or not it includes a leading slash `/`), the framework attempts to combine it with the `BaseAddress` configured on the `HttpClient` to generate the final request address (`RFC 3986`). For example: ```cs showLineNumbers {3} services.AddHttpClient(string.Empty, client => { client.BaseAddress = new Uri("https://furion.net/"); }); ``` In the configuration above, if the request address is `"api/get/user"` or `"/api/get/user"`, the final request address will be `"https://furion.net/api/get/user"`. - If the request address is an empty string or `null`, the `BaseAddress` configured on the `HttpClient` is used directly as the final request address. This means that if `BaseAddress` is `"https://furion.net/"`, the final request address will also be `"https://furion.net/"`. > **Tip** It is worth noting that all methods supporting request address configuration accept string addresses and are also compatible with `Uri`-type address settings. --- # 3.4 Method Naming Conventions > Source: https://http.furion.net/en/docs/request-builder/method-naming-conventions/ When designing the methods of the `HttpRequestBuilder` object, we follow a clear set of naming conventions to ensure that each method's functionality and behavior are intuitive and easy to understand. **Specifically, all methods that can only perform an operation start with `Set` or `Use`, while all methods that support repeated invocation and additive operations start with `With` or `Add`.** - **Methods starting with `Set` or `Use`**: these methods are used to set a property or parameter; if called repeatedly, the later call overrides the previous setting. For example, for the `SetTraceIdentifier(traceId)` method, when called multiple times, only the `traceId` from the last call takes effect. - **Methods starting with `With` or `Add`**: these methods are used to add or modify certain content and support repeated invocation. When called repeatedly, they do not override previous settings but accumulate instead. For example, the `WithHeader(key, value)` method, when called multiple times, preserves all previous header information and adds new header information. This naming convention makes the methods of the `HttpRequestBuilder` object clearer and easier to understand, helping developers quickly grasp the functionality and behavior of each method. --- # 3.5 Setting the Trace Identifier > Source: https://http.furion.net/en/docs/request-builder/setting-the-trace-identifier/ Specifies a unique identifier for the request to facilitate tracking and debugging. This identifier is set in the `X-Trace-ID` request header. ```cs showLineNumbers {2} HttpRequestBuilder.Get("https://furion.net/") .SetTraceIdentifier("your-id"); ``` --- # 3.6 Setting the Content Type > Source: https://http.furion.net/en/docs/request-builder/setting-the-content-type/ Specifies the content type of the request. ```cs showLineNumbers {2,5} HttpRequestBuilder.Get("https://furion.net/") .SetContentType("text/plain"); HttpRequestBuilder.Get("https://furion.net/") .SetContentType("text/plain; charset=utf-8"); // supports specifying a character set ``` --- # 3.7 Setting the Content Encoding > Source: https://http.furion.net/en/docs/request-builder/setting-the-content-encoding/ Sets the content encoding of the request. ```cs showLineNumbers {2,5} HttpRequestBuilder.Get("https://furion.net/") .SetContentEncoding(Encoding.UTF8); HttpRequestBuilder.Get("https://furion.net/") .SetContentEncoding("utf-8"); // supports encoding strings ``` > **Content Encoding Usage Notes** • When content encoding is set, the system automatically appends `;charset=encoding` after `Content-Type`. For example: - Original `Content-Type`: `application/json`. - After setting the encoding to `utf-8`, it finally becomes: `application/json;charset=utf-8`. • Notes: 1. Some third-party servers may not support `Content-Type` with a `charset` parameter, which can cause the request to fail. 2. **If there is no special requirement, it is recommended to keep the default and not set content encoding.** --- # 3.8 Setting JSON Content > Source: https://http.furion.net/en/docs/request-builder/setting-json-content/ Sets the request's content type to `application/json` and sends `JSON` data. ```cs showLineNumbers {2,5,8,11,14,17} HttpRequestBuilder.Post("https://furion.net/") .SetJsonContent(new { id = 1, name = "Furion" }); // Supports anonymous objects or typed objects HttpRequestBuilder.Post("https://furion.net/") .SetJsonContent("{\"id\":1,\"name\":\"furion\"}"); // Sends a JSON string directly HttpRequestBuilder.Post("https://furion.net/") .SetJsonContent("{\"id\":1,\"name\":\"furion\"}", Encoding.UTF8); // Sets the encoding HttpRequestBuilder.Post("https://furion.net/") .SetJsonContent("{\"id\":1,\"name\":\"furion\"}", Encoding.UTF8, "application/json-patch+json"); // Custom content-type HttpRequestBuilder.Post("https://furion.net/") .SetJsonContent(new { id = 1, name = "Furion" }, jsonSerializerOptions: new JsonSerializerOptions()); // Supports passing a JsonSerializerOptions object HttpRequestBuilder.Post("https://furion.net/") .SetJsonContentWithoutValidation("{\"id\":1,\"name\":\"furion\"}"); // Sends a JSON string directly (without validating the JSON format) ``` > **Recommended: use 【[raw string literals](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/tokens/raw-string)】 to set `JSON`** We recommend using [raw string literals](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/tokens/raw-string) to set `JSON` data. In `C# 11`, this feature was introduced, allowing strings wrapped in three double quotes (`"""`) to contain multi-line text, while escape characters within the string (such as `\n`, `\t`, etc.) are treated as ordinary characters without needing to be escaped. For example: ```cs showLineNumbers {2-7} HttpRequestBuilder.Post("https://furion.net/") .SetJsonContent(""" { "id": 1, "name": "Furion" } """); ``` If you need to insert a variable into the raw string, simply add `$$` before the first `"""` and use the `{{variableName}}` template as a placeholder. For example: ```cs showLineNumbers {1,4,7} var val = "Furion"; HttpRequestBuilder.Post("https://furion.net/") .SetJsonContent($$""" { "id": 1, "name": "{{val}}" } """); ``` Using raw string literals to set `JSON` data simplifies the code and avoids the tedious handling of escape characters. > **Custom `JSON` property names** When an object is serialized to `JSON`, properties use camelCase naming (`CamelCase`) by default. If you need to customize the serialized property name, add the `[JsonPropertyName("custom name")]` attribute to the property. > **Notes on `JSON` strings** If the `JSON` string passed in has an invalid format, a `JsonException` is thrown. If format validation is not required, use the `SetJsonContentWithoutValidation` method. --- # 3.9 Setting HTML Content > Source: https://http.furion.net/en/docs/request-builder/setting-html-content/ Sets the request's content type to `text/html` and sends `HTML` data. ```cs showLineNumbers {2,5,8} HttpRequestBuilder.Post("https://furion.net/") .SetHtmlContent(""); HttpRequestBuilder.Post("https://furion.net/") .SetHtmlContent("", Encoding.UTF8); // Sets the encoding HttpRequestBuilder.Post("https://furion.net/") .SetHtmlContent("", Encoding.UTF8, "application/html"); // Custom content-type ``` > **Recommended: use 【[raw string literals](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/tokens/raw-string)】 to set `HTML`** Refer to 【3.8 Setting `JSON` Content】. --- # 3.10 Setting XML Content > Source: https://http.furion.net/en/docs/request-builder/setting-xml-content/ Sets the request's content type to `text/xml` and sends `XML` data. ```cs showLineNumbers {2,5,8} HttpRequestBuilder.Post("https://furion.net/") .SetXmlContent(""); HttpRequestBuilder.Post("https://furion.net/") .SetXmlContent("", Encoding.UTF8); // Sets the encoding HttpRequestBuilder.Post("https://furion.net/") .SetXmlContent("", Encoding.UTF8, "application/soap+xml"); // Custom content-type ``` > **Recommended: use 【[raw string literals](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/tokens/raw-string)】 to set `XML`** Refer to 【3.8 Setting `JSON` Content】. --- # 3.11 Setting Text Content > Source: https://http.furion.net/en/docs/request-builder/setting-text-content/ Sets the request's content type to `text/plain` and sends plain text data. ```cs showLineNumbers {2,5,8} HttpRequestBuilder.Post("https://furion.net/") .SetTextContent("Furion"); HttpRequestBuilder.Post("https://furion.net/") .SetTextContent("Furion", Encoding.UTF8); // Sets the encoding HttpRequestBuilder.Post("https://furion.net/") .SetTextContent("Furion", Encoding.UTF8, "text/plain"); // Custom content-type ``` > **Recommended: use 【[raw string literals](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/tokens/raw-string)】 to set text** Refer to 【3.8 Setting `JSON` Content】. --- # 3.12 Setting Raw raw String Content > Source: https://http.furion.net/en/docs/request-builder/setting-raw-raw-string-content/ In modern `API` testing tools such as `Postman`, users can send requests in the `raw` data format. In `ASP.NET Core` server-side applications, this typically manifests as receiving a string parameter marked with the `[FromBody]` attribute (for example, `str`): ```cs showLineNumbers {3} [HttpPost] // [Consumes("application/json")] public string AddBodyString([FromBody] string str) // Note: by default ASP.NET Core does not support binding of the text/plain content type { return str; } ``` To set raw string content, use the following methods: ```cs showLineNumbers {2,5,8} HttpRequestBuilder.Post("https://furion.net/") .SetRawStringContent("Furion"); // Default content type text/plain HttpRequestBuilder.Post("https://furion.net/") .SetRawStringContent("Furion", "application/json"); // Content type is required HttpRequestBuilder.Post("https://furion.net/") .SetContent("\"Furion\"", "application/json"); // Equivalent to calling SetRawStringContent ``` > **Raw string format** Please note that when calling the `SetRawStringContent(text)` method, the passed string content is automatically wrapped in double quotes before being sent. For example, if the input string is `Furion`, the actual content sent will be `"Furion"`. --- # 3.13 Setting URL-Encoded Form Content > Source: https://http.furion.net/en/docs/request-builder/setting-url-encoded-form-content/ Sets the request's content type to `application/x-www-form-urlencoded` and sends form data. ```cs showLineNumbers {2,5,9,12} HttpRequestBuilder.Post("https://furion.net/") .SetFormUrlEncodedContent(new { id = 1, name = "Furion" }); HttpRequestBuilder.Post("https://furion.net/") .SetFormUrlEncodedContent(new { id = 1, name = "Furion" }, useStringContent: true); // Uses StringContent to resolve the FormUrlEncodedContent encoding issue // Supports URL-encoded string format HttpRequestBuilder.Post("https://furion.net/") .SetFormUrlEncodedContent("id=1&name=furion", useStringContent: true); HttpRequestBuilder.Post("https://furion.net/") .SetFormUrlEncodedContent(new { id = 1, name = "Furion" }, urlEncode: false); // Can be configured to skip URL encoding ``` > **Notes on `URL`-encoded form content** - **By default, `URL`-encoded forms are built via the [`FormUrlEncodedContent`](https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Net.Http/src/System/Net/Http/FormUrlEncodedContent.cs#L44) type, but this type does not support custom request content encoding; it uses `Encoding.Latin1` instead of `UTF-8` by default.** This may cause exceptions when submitting to certain endpoints. To resolve this issue, set the `useStringContent` parameter to `true` to build form data using `StringContent`, which allows customizing the encoding to `UTF-8`. ```cs showLineNumbers {3} var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddURLForm", builder => builder .SetFormUrlEncodedContent(new { id = 1, name = "furion" }, useStringContent: true)); ``` - Some servers require an explicit character set (`charset`) declaration; in that case, specify the encoding via the `contentEncoding` parameter, for example using `UTF-8`: ```cs showLineNumbers {3} var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddURLForm", builder => builder .SetFormUrlEncodedContent(new { id = 1, name = "furion" }, Encoding.UTF8)); ``` With this setting, the following `Content-Type` request header is generated when sending the remote request: `application/x-www-form-urlencoded; charset=UTF-8`. --- # 3.14 Setting File Content > Source: https://http.furion.net/en/docs/request-builder/setting-file-content/ Sets file content from a local path or internet address, automatically detecting the file name and `Content-Type`. ```cs showLineNumbers {3,7,11,16-19} // Sets a local file HttpRequestBuilder.Post("https://furion.net/") .SetFileContent(@"C:\Users\Furion\test.png"); // Sets a local file and specifies the file name and content type HttpRequestBuilder.Post("https://furion.net/") .SetFileContent(@"C:\Users\Furion\test.png", "avatar.png", "image/png"); // Sets an internet file HttpRequestBuilder.Post("https://furion.net/") .SetFileContent("https://furion.net/files/test.png"); // Sets an internet file and specifies custom request configuration HttpRequestBuilder.Post("https://furion.net/") .SetFileContent("https://furion.net/files/test.png", configure: request => { request.Headers.TryAddWithoutValidation("custom-key", "custom-value"); }); ``` > **Notes on file content** - When the passed `filePath` is an internet address (`http/https`), the framework automatically issues a `GET` request to download the file stream. - If `fileName` is not specified, the framework automatically resolves the file name from the path or `Uri` address. - If `contentType` is not specified, the framework automatically infers the `Content-Type` based on the file extension. - This method automatically adds a `Content-Disposition` request header internally, in the format `attachment; filename="fileName"`. --- # 3.15 Setting Binary Stream Content > Source: https://http.furion.net/en/docs/request-builder/setting-binary-stream-content/ Directly sets a `Stream` as the request content, suitable for file streams, memory streams, network streams, and other scenarios. ```cs showLineNumbers {3,7,10,12,16} // Sets a file stream HttpRequestBuilder.Post("https://furion.net/") .SetStreamContent(File.OpenRead(@"C:\Users\Furion\test.png")); // Sets a file stream and specifies the file name and content type HttpRequestBuilder.Post("https://furion.net/") .SetStreamContent(File.OpenRead(@"C:\Users\Furion\test.png"), "avatar.png", "image/png"); // Sets a memory stream var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes("Hello Furion")); HttpRequestBuilder.Post("https://furion.net/") .SetStreamContent(memoryStream, contentType: "text/plain"); // Sets a stream and specifies the encoding HttpRequestBuilder.Post("https://furion.net/") .SetStreamContent(memoryStream, contentEncoding: Encoding.UTF8); ``` > **Notes on stream content** - The `disposeResourcesOnRequestCompletion` parameter defaults to `true`, meaning the stream resources are automatically released after the request completes. If you need to manage the stream's lifetime manually, set this parameter to `false`. - When the `fileName` parameter is provided, the framework automatically adds a `Content-Disposition` request header in the format `attachment; filename="fileName"`. - If `contentType` is not specified, the framework infers it automatically from the file name extension; if it cannot be inferred, `application/octet-stream` is used based on the stream type. --- # 3.16 Setting Request Content (Body) > Source: https://http.furion.net/en/docs/request-builder/setting-request-content-body/ Supports setting any type of request content. ```cs showLineNumbers {2,5,8,11,14,17,20,23,26,29} HttpRequestBuilder.Post("https://furion.net/") .SetContent(null); HttpRequestBuilder.Post("https://furion.net/") .SetContent("furion", "text/plain"); HttpRequestBuilder.Post("https://furion.net/") .SetContent(new { id = 1, name = "Furion"}); // Automatically infer Content-Type HttpRequestBuilder.Post("https://furion.net/") .SetContent(new { id = 1, name = "Furion"}, "application/json"); HttpRequestBuilder.Post("https://furion.net/") .SetContent(new MemoryStream(), "application/octet-stream"); HttpRequestBuilder.Post("https://furion.net/") .SetContent(new byte[]{}, "application/octet-stream"); HttpRequestBuilder.Post("https://furion.net/") .SetContent(new StringContent(...), "text/plain; charset=utf-8"); HttpRequestBuilder.Post("https://furion.net/") .SetContent(new ReadOnlyMemory(...), "application/octet-stream"); HttpRequestBuilder.Post("https://furion.net/") .SetContent(new MultipartContent(), "multipart/form-data"); HttpRequestBuilder.Post("https://furion.net/") .SetContent(new MemoryStream(), "multipart/form-data", disposeResourcesOnRequestCompletion: true); // Configure automatic resource release after request completion ``` > **Default Behavior When `Content-Type` Is Not Provided** When `Content-Type` is not specified, the framework determines `Content-Type` according to the following priority: 1. **Request content header**: If `Content-Type` is set via `WithHeader` or similar, it takes precedence. 2. **Automatic content type inference**: If no content header is set, the framework automatically infers based on the concrete type of `RawContent` according to the following rules: - **`JsonContent`**: `application/json` - **`JsonNode` or `JsonElement`** - If it represents a `JSON` object or array, `application/json` - Otherwise (e.g. a `JSON` scalar value), fall back to `text/plain` - **`FormUrlEncodedContent`**: `application/x-www-form-urlencoded` - **`StringContent`**: `text/plain` - **`MultipartFormDataContent`**: `multipart/form-data` - **`MultipartContent`** (not a `FormData` subclass): `multipart/mixed` - **`ByteArrayContent`, `StreamContent`, `ReadOnlyMemoryContent`**: `application/octet-stream` - **`byte[]`, `Stream`, `ReadOnlyMemory`**: `application/octet-stream` - **Other custom `HttpContent` subclasses** (no header set and none of the concrete types above matched): `application/octet-stream` - **`MultipartFile`**: `application/octet-stream` - **`FileInfo`**: inferred from the file extension via `FileTypeMapper`; if it cannot be recognized, defaults to `application/octet-stream` - **Other complex objects** (not a primitive type, enum, or collection): `application/json` (assumed to be serialized as `JSON`) 3. **Global default fallback value**: If none of the above rules match, the value configured in `HttpClientOptions.DefaultContentType` is used. This value defaults to `text/plain`; to change this fallback value, configure it with the following code: ```cs showLineNumbers {2,5} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { // Set the default request content type (final fallback value) options.DefaultContentType = "application/json"; }); ``` > **Tip** The `SetJsonContent`, `SetHtmlContent`, `SetXmlContent`, `SetTextContent`, `SetRawStringContent`, `SetFormUrlEncodedContent`, `SetFileContent`, `SetStreamContent`, and `SetMcpContent` methods all call the `SetContent` method internally. --- # 3.17 Setting MCP/2.0 Message Content > Source: https://http.furion.net/en/docs/request-builder/content-mcp/ `SetMcpContent` is an extension method specifically designed for the **`MCP` (`Model Context Protocol`) 2.0** protocol, used to quickly build requests conforming to the `JSON‑RPC 2.0` format and automatically attach the required `MCP` request headers. It supports sending two message types: **requests** (which require a response) and **notifications** (which require no response). ```cs showLineNumbers {2,6,10-13,17} HttpRequestBuilder.Post("https://mcp.example.com/mcp") .SetMcpContent("MyClient", "tools/list"); // Carries parameters HttpRequestBuilder.Post("https://mcp.example.com/mcp") .SetMcpContent("MyClient", "tools/list", new { name = "get_weather" }); // Custom ID HttpRequestBuilder.Post("https://mcp.example.com/mcp") .SetMcpContent("MyClient", new McpMessageData("tools/list") { Id = "custom-001" }); // Used together with Server Sent Events streaming responses HttpRequestBuilder.ServerSentEvents("https://mcp.example.com/mcp") .SetMcpContent("MyClient", "tools/call", new { name = "get_weather" }); ``` > **Notes on `MCP/2.0` content** - This method automatically completes the `MCP` request encapsulation: it fills in the required request headers (`Mcp-Name`, `MCP-Protocol-Version`, `Mcp-Method`, `Accept`) and serializes the request body according to the `JSON‑RPC 2.0` specification. - When `Id` is not specified, an incrementing integer is generated automatically; to send a notification, set `Id` to `null`. - If `GET/HEAD` is used with request content, the framework automatically switches the method to `POST`. > **Handling `Server Sent Events` streaming responses** When the server returns a `text/event-stream` streaming response, you can parse `ServerSentEventsData` into `McpMessageData` via the `ToMcpMessage()` extension method, and then use `GetResult()` or `GetData()` to obtain the returned content. --- # 3.18 Setting Multipart Form Content > Source: https://http.furion.net/en/docs/request-builder/multipart/ Sets the request's content type to `multipart/form-data` and sends multipart form content. ```cs showLineNumbers {2-5,12} HttpRequestBuilder.Post("https://furion.net/") .SetMultipartContent(multipart => { multipart.AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file"); // ... }); // Supports keeping the multipart content's default Content-Type (not kept by default) HttpRequestBuilder.Post("https://furion.net/") .SetMultipartContent(multipart => { multipart.AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file"); // ... }, false); ``` Besides building content all at once inside `SetMultipartContent`, you can also append content multiple times externally via the `WithMultipart` method for more flexible chaining. ```cs showLineNumbers {2,5-6} HttpRequestBuilder.Post("https://furion.net/") .SetMultipartContent(multipart => { multipart.AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file"); }) .WithMultipart(multipart => multipart.AddJson("{}", "name")) .WithMultipart(multipart => multipart.AddText("Hello", "name")); // Supports configuring multiple ``` > **Notes** - The `WithMultipart` method is only effective when `SetMultipartContent` was previously called to set multipart content; otherwise the operation is skipped. - `WithMultipart` supports multiple calls; content appended each time is merged into the same multipart form. - The second parameter of `SetMultipartContent`, `omitContentType`, defaults to `true`, meaning the default `Content-Type` header (usually auto-generated by `HttpContent`) is **removed**. To keep that header, explicitly pass `false`. > **Important** Using the `SetMultipartContent` method overrides the other content-setting methods (`SetJsonContent`, `SetHtmlContent`, `SetXmlContent`, `SetTextContent`, `SetRawStringContent`, `SetFormUrlEncodedContent`, and `SetContent`). ```cs showLineNumbers {2-5} HttpRequestBuilder.Post("https://furion.net/") .SetMultipartContent(multipart => { multipart.AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file"); }) .SetContent(new { id = 1, name = "Furion" }, "application/json"); // Will be overridden ``` In addition, after calling the `SetMultipartContent` method, the `MultipartFormDataBuilder` property of the `HttpRequestBuilder` instance is initialized (i.e. no longer `null`), at which point complex business logic can be handled through that property. --- # 3.19 Setting Request Headers > Source: https://http.furion.net/en/docs/request-builder/setting-request-headers/ Adds or modifies request headers. ```cs showLineNumbers {2-6} HttpRequestBuilder.Get("https://furion.net/") .WithHeader("X-Header", "X-Value") // Add a single header .WithHeader("date", DateTime.Now, format: "yyyyMMdd") // Supports format formatting .WithHeaders(new Dictionary { }) // Add multiple headers .WithHeaders(new { id = 1, name = "Furion" }) // Add multiple headers, supporting multiple key-value types .WithHeaders("Content-Type: application/json"); // Supports configuration using a colon (:) ``` If duplicate request headers exist, they are merged, and multiple values are separated by a comma followed by a space (`, `). By setting the `replace: true` parameter, you can override previously set request headers. > **Using the `HeaderNames` Static Class to Set `HTTP` Headers** When configuring standard headers for an `HTTP` request or response, such as `Authorization`, **manually writing header names can cause issues due to typos**. To avoid such errors and take full advantage of the IntelliSense features of the integrated development environment (`IDE`), the `HeaderNames` static class is recommended. For example, using `HeaderNames.Authorization` and `HeaderNames.UserAgent` ensures header name accuracy and improves code readability and maintainability. > **Configuration Parameter Support** Request headers support configuration parameters for reading configuration information and performing replacement operations. Configuration parameters use the `[[key]]` syntax. --- # 3.20 Setting Request Headers to Remove > Source: https://http.furion.net/en/docs/request-builder/setting-request-headers-to-remove/ Removes the specified request headers. ```cs showLineNumbers {2} HttpRequestBuilder.Get("https://furion.net/") .RemoveHeaders("User-Agent", "Host", "Accept"); // Remove multiple headers ``` Before sending the `HTTP` request, the set of request headers to remove specified in the configuration is removed. In other words, the `RemoveHeaders` method executes after all `WithHeader[s]` method calls. --- # 3.21 Setting the Fragment Identifier > Source: https://http.furion.net/en/docs/request-builder/fragment/ Adds a fragment identifier to the `URL`. ```cs showLineNumbers {2,5} HttpRequestBuilder.Get("https://furion.net/") .SetFragment("About"); // Generates URL: https://furion.net/#About HttpRequestBuilder.Get("https://furion.net/") .SetFragment("#About"); // Supports starting with the # symbol ``` --- # 3.22 Setting the Timeout > Source: https://http.furion.net/en/docs/request-builder/setting-the-timeout/ Sets the timeout duration for a single request. ```cs showLineNumbers {2,5,8,11,14,20,26,29} HttpRequestBuilder.Get("https://furion.net/") .SetTimeout(TimeSpan.FromSeconds(2)); HttpRequestBuilder.Get("https://furion.net/") .SetTimeout(2000); // Unit is milliseconds HttpRequestBuilder.Get("https://furion.net/") .SetTimeout(-1); // Never times out; or use .WithoutTimeout(), or set System.Threading.Timeout.InfiniteTimeSpan HttpRequestBuilder.Get("https://furion.net/") .SetTimeout(0); // Immediately cancels the request HttpRequestBuilder.Get("https://furion.net/") .SetTimeout(TimeSpan.FromSeconds(2), () => // Supports a timeout callback { // Action to execute when a timeout occurs }); HttpRequestBuilder.Get("https://furion.net/") .SetTimeout(2000, () => // Supports a timeout callback { // Action to execute when a timeout occurs }); HttpRequestBuilder.Get("https://furion.net/") .SetTimeout(new HttpTimeoutOptions()); // Custom timeout options HttpRequestBuilder.Get("https://furion.net/") .SetTimeout(options => options.SetTimeout(2000)); // Custom timeout options ``` > **Timeout and Retry Mechanism** `SetTimeout` sets the timeout for a **single request**. If `SetRetry` is also configured, this timeout is **applied independently to each retry**, rather than being the total elapsed time of all retries. To limit the **maximum total elapsed time**, including retries and wait times, use an external `CancellationTokenSource`: ```cs showLineNumbers {2,5-6,9} // Limit the total elapsed time of the entire process (including retries) to no more than 2 seconds using var timeoutCancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(2)); var builder = HttpRequestBuilder.Get("https://furion.net/") .SetTimeout(500) // Single request timeout of 500 ms .SetRetry(5, TimeSpan.FromSeconds(1)); // Allow 5 retries with a 1-second interval between each // Pass a global Token to terminate the entire process immediately at 2 seconds var response = await httpRemoteService.SendAsync(builder, timeoutCancellationToken.Token); ``` > **Notes on `HttpClient` Timeout** When setting the timeout in `HttpClient`, make sure the single-request timeout does not exceed the timeout configured on `HttpClient`. For example, if the `HttpClient` timeout is set to `10` minutes while the single-request timeout is set to `15` minutes, a timeout exception will still be triggered when the single request exceeds `10` minutes. The sample code is as follows: ```cs showLineNumbers {1,3} services.AddHttpClient(string.Empty, client => { client.Timeout = TimeSpan.FromMinutes(10); // The default timeout is 100 seconds and must be set explicitly // client.Timeout = System.Threading.Timeout.InfiniteTimeSpan; // Never times out }); ``` Therefore, **if a single request needs a longer timeout, make sure the `HttpClient` timeout is set correspondingly longer.** --- # 3.23 Configuring Retry Policies > Source: https://http.furion.net/en/docs/request-builder/configuring-retry-policies/ Configures the retry policy for a single request. By default, if a retry policy is configured, the retry mechanism is triggered automatically when an unsuppressed exception occurs during the request. ```cs showLineNumbers {2,5,8,11,14,17,20,23,27,31,34} HttpRequestBuilder.Get("https://furion.net/") .SetRetry(3); // Maximum number of retries; 0 means no retry HttpRequestBuilder.Get("https://furion.net/") .SetRetry(3, ctx => Console.WriteLine($"This is attempt {ctx.Attempt}.")); // Configures the callback delegate invoked before each retry HttpRequestBuilder.Get("https://furion.net/") .SetRetry(3, TimeSpan.FromSeconds(1)); // Configures the retry interval HttpRequestBuilder.Get("https://furion.net/") .SetRetry(3, TimeSpan.FromSeconds(1), ctx => Console.WriteLine($"This is attempt {ctx.Attempt}.")); // Configures the callback delegate invoked before each retry HttpRequestBuilder.Get("https://furion.net/") .SetRetry(3, 1000); // Configures the retry interval (milliseconds) HttpRequestBuilder.Get("https://furion.net/") .SetRetry(3, 1000, ctx => Console.WriteLine($"This is attempt {ctx.Attempt}.")); // Configures the callback delegate invoked before each retry HttpRequestBuilder.Get("https://furion.net/") .SetRetry(new HttpRetryOptions()); // Custom retry options HttpRequestBuilder.Get("https://furion.net/") .SetRetry(options => options.SetMaxRetries(3)); // Custom retry options HttpRequestBuilder.Get("https://furion.net/") .SetRetry(options => options.SetMaxRetries(3) .AddRetryStatusCodes(401)); // Retries on specific HTTP status codes HttpRequestBuilder.Get("https://furion.net/") .SetRetry(options => options.SetMaxRetries(3) .AddRetryExceptions(typeof(InvalidOperationException))); // Retries on specific exception types HttpRequestBuilder.Get("https://furion.net/") .SetRetryIndefinitely(); // Sets infinite retry until success ``` > **`HttpClient` timeout and its impact on retries** The total time consumed by retries is constrained by the `HttpClient` timeout. For example, if the timeout is set to `3` seconds, the maximum number of retries is `4`, and the interval is `1` second, subsequent retries will be canceled if they still have not succeeded within `3` seconds. Therefore, to ensure retries are not interrupted, configure the timeout appropriately; otherwise, the request may be terminated prematurely or block indefinitely. `HttpRetryOptions` contains the following properties and methods: - **Properties**: - `MaxRetries`: Maximum number of retries (`int` type). Defaults to `0`, meaning no retry. If `RetryIntervals` is set, this value is automatically overridden with the array length. - `RetryInterval`: Base retry interval (`TimeSpan` type). Defaults to `1` second. Takes effect only when `RetryIntervals` is not set. - `UseExponentialBackoff`: Whether to use exponential backoff for retries (`bool` type). Defaults to `false`. When set to `true`, each retry interval = `RetryInterval * 2^(retry-1)`. Takes effect only when `RetryIntervals` is not set. - `RetryIntervals`: Custom retry interval array (`IList?` type). If this property is set, the number of retries equals the array length, and `MaxRetries` and `UseExponentialBackoff` are ignored. Each retry uses the interval at the corresponding index in the array, in order. - `RetryStatusCodes`: Collection of `HTTP` status codes to retry (`HashSet?` type). If empty, only failures caused by exceptions are retried. - `RetryExceptionTypes`: Collection of exception types to retry (`HashSet?` type). If empty, all `Exception`s are retried (subject to `MaxRetries`). - `OnRetry`: Callback delegate invoked before each retry (`Action?` type). Can be used for logging, sending notifications, and so on. **If this callback is not set, the framework automatically outputs a default retry warning log.** Its parameter is `HttpRetryContext`, which contains the following properties: - `Attempt`: Current retry count (`int` type). Starts from `1`. - `Exception`: The exception that triggered the retry (`Exception?` type). - `StatusCode`: The `HTTP` status code that triggered the retry (`HttpStatusCode?` type). - `MaxRetries`: Maximum number of retries (`int` type). `-1` means unlimited (displayed as `∞` in logs). - `IsExceptionRetry`: Whether the retry was triggered by an exception (`bool` type). - `IsStatusCodeRetry`: Whether the retry was triggered by a status code (`bool` type). - `RetryIndefinitely`: Whether to retry indefinitely until success (`bool` type). Defaults to `false`. When set to `true`, `MaxRetries` and the length of `RetryIntervals` are ignored, and it keeps retrying until success or until a non-retryable exception occurs. - **Methods**: - `SetMaxRetries(maxRetries)`: Sets the maximum number of retries. - `SetRetryInterval(interval)` and `SetRetryInterval(milliseconds)`: Sets the base retry interval. - `SetUseExponentialBackoff(use)`: Sets whether to use exponential backoff. - `SetRetryIntervals(intervals)` and `SetRetryIntervals(milliseconds)`: Sets the custom retry interval array. - `AddRetryStatusCode(statusCode)`: Adds an `HTTP` status code to retry. - `AddRetryStatusCodes(statusCodes)`: Adds multiple `HTTP` status codes to retry. - `AddRetryException()` and `AddRetryException(exceptionType)`: Adds an exception type to retry. - `AddRetryExceptions(exceptionTypes)`: Adds multiple exception types to retry. - `SetOnRetry(onRetry)`: Sets the callback delegate invoked before each retry. - `SetRetryIndefinitely(retryIndefinitely)`: Sets whether to retry indefinitely. --- # 3.24 Setting Path Segments > Source: https://http.furion.net/en/docs/request-builder/setting-path-segments/ Adds `URL` path segments. ```cs showLineNumbers {2-7} HttpRequestBuilder.Get("https://furion.net/") .WithPathSegment("user") // Adds a single path segment .WithPathSegments(["detail", "edit"]) // Adds multiple path segments ``` The resulting final `URL` is: `https://furion.net/user/detail/edit`. If duplicate path segments exist, they will appear repeatedly in subsequent appends (for example: `/docs/docs/users/docs/`). --- # 3.25 Setting Path Segments to Remove > Source: https://http.furion.net/en/docs/request-builder/setting-path-segments-to-remove/ Removes the specified path segments. ```cs showLineNumbers {2} HttpRequestBuilder.Get("https://furion.net/docs/login/users") .RemovePathSegments("docs", "user"); // Removes multiple path segments ``` The resulting final `URL` is: `https://furion.net/login`. Before the `HTTP` request is sent, the configured set of path segments to remove will be removed. In other words, the `RemovePathSegments` method executes after all `WithPathSegment[s]` method calls. --- # 3.26 Setting Query Parameters (URL Parameters) > Source: https://http.furion.net/en/docs/request-builder/setting-query-parameters-url-parameters/ Adds or modifies `URL` query parameters. ```cs showLineNumbers {2-7,9-12} HttpRequestBuilder.Get("https://furion.net/") .WithQueryParameter("id", 1) // Adds a single parameter .WithQueryParameter("date", DateTime.Now, format: "yyyyMMdd") // Supports format formatting .WithQueryParameter("name", new[] { "furion", "monksoul" }) // Adds multiple values, generating: name=furion&name=monksoul .WithQueryParameter("name", (object?)null) // Sets a null value .WithQueryParameter("r", () => DateTimeOffset.UtcNow.ToUnixTimeSeconds()) // Sets a dynamically computed parameter (used for cache busting) .WithQueryParameter("r", context => DateTimeOffset.UtcNow.ToUnixTimeSeconds()) // Sets a dynamically computed parameter (used for cache busting) .WithQueryParameters(new Dictionary { }) // Adds multiple parameters .WithQueryParameters(new { id = 1, name = "Furion" }) // Adds multiple parameters, generating: id=1&name=Furion .WithQueryParameters(new { id = 1, name = "Furion" }, "user") // Adds parameters with a prefix, generating: user.id=1&user.name=Furion .WithQueryParameters(new Dictionary { { "str1", null }, {"str2", "test" } }, ignoreNullValues: true); // Ignores null values ``` If duplicate query parameter keys exist, they are merged into multiple key-value pairs (for example `key1=value1&key1=value2`). By setting the `replace: true` parameter, you can override previous query parameters and the original `URL` parameters. **By default, query parameters with a `null` value are added to the `URL`; to ignore these parameters, set `ignoreNullValues: true`.** ### `URL` Parameter Formatter When setting query parameters on an `HTTP` request, the framework passes the parameter keys and values to `IUrlParameterFormatter` for formatting. The default implementation, `UrlParameterFormatter`, generates a `key=value` pair for each value. However, certain types (such as `DateTime`) may require special handling, or you may want to change the output form of the entire key-value pair (for example, outputting multiple values in an array format such as `key[0]=val1&key[1]=val2`), which can be achieved through a custom formatter. The following example shows how to override the `Format` method to format values of type `DateTime` as `yyyyMMdd`, while other types use the default handling: ```csharp showLineNumbers {1,4,6-15} public class CustomUrlParameterFormatter : UrlParameterFormatter { /// public override IEnumerable>? Format(UrlFormattingContext context, string key, IEnumerable values) { foreach (var value in values) { if (value is DateTime dateTime) { yield return new(key, dateTime.ToString("yyyyMMdd")); // Formats continue; } yield return new(key, FormatValue(context, value)); } } } ``` After completing the custom formatter, you can register it as the default `URL` parameter formatter when configuring `HttpRemoteOptions`: ```csharp showLineNumbers {2,4} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { options.UrlParameterFormatter = new CustomUrlParameterFormatter(); }); ``` In this way, when building `URL` query parameters, if a `DateTime` value is encountered, the framework automatically formats it as a `yyyyMMdd` string, ensuring the output matches expectations. ### `URL` Parameter Sorting Although sorting `URL` query parameters is a relatively rare requirement, some systems with higher security requirements often need to verify the order of parameters. The framework provides sorting support for this purpose, sorting the final key-value pair collection: ```cs showLineNumbers {3} HttpRequestBuilder.Get("https://furion.net/") .WithQueryParameters(new { name = "furion", id = 1}) .SetQueryParametersSorter(pairs => pairs.OrderBy(kv => kv.Key)); ``` Use the `.SetQueryParametersSorter()` method to configure the query parameter sorting rule. This method receives a sequence of `KeyValuePair` and returns a new sorted sequence. When it is `null`, no sorting is applied (the original insertion order is preserved). --- # 3.27 Setting Query Parameters to Remove > Source: https://http.furion.net/en/docs/request-builder/setting-query-parameters-to-remove/ Removes the specified query parameters. ```cs showLineNumbers {2} HttpRequestBuilder.Get("https://furion.net/") .RemoveQueryParameters("id", "name", "age"); // Removes multiple parameters ``` Before the `HTTP` request is sent, the configured set of query parameters to remove will be removed. In other words, the `RemoveQueryParameters` method executes after all `WithQueryParameter[s]` method calls. --- # 3.28 Setting Path Parameters (Template/Configuration Parameters) > Source: https://http.furion.net/en/docs/request-builder/path-params/ Replaces object template strings in the `URL` path. ```cs showLineNumbers {2-6,8-9,11-12} HttpRequestBuilder.Get("https://furion.net?id={id}&name={name}") .WithPathParameter("id", 1) // Adds a single parameter; {id} will be replaced with 1 .WithPathParameter("name", new[] { "furion", "monksoul" }) // Adds a single parameter; {name} will be replaced with furion,monksoul .WithPathParameters(new Dictionary { }) // Adds multiple path parameters .WithPathParameters(new { id = 1, name = "Furion" }) // Adds multiple path parameters; {id} and {name} are replaced with 1 and Furion respectively .WithPathParameters(new { id = 1, name = "Furion" }, "user") // Adds parameters with a prefix; {user.id} and {user.name} are replaced with 1 and Furion respectively HttpRequestBuilder.Get("https://furion.net/{id}/{name?}") // A trailing "?" means it is replaced with an empty string when the key does not exist .WithPathParameter(new { id = 1 }); HttpRequestBuilder.Get("https://furion.net/{**path}") // A leading "**" means the path separator "/" is not escaped .WithPathParameter(new { path = "files/images/photo.jpg" }); ``` If a path parameter key is duplicated, the later key-value setting overrides the previous one. **Template Path Syntax** In addition to directly using `{key}`, template paths support accessing an object's properties and nested properties via `.`, and accessing elements in a collection via `[index]`. Furthermore, when a property of an object type is not found as a same-named property, the framework automatically tries to treat it as a dictionary and retrieves the value using the path identifier as the key (equivalent to `dict["key"]`). - `{key}`: Directly replaces the corresponding value. - `{key.property}`: Accesses the `property` property of the `key` object, or when `key` is a dictionary, accesses the value with the key `"property"`. - `{key.property.nested}`: Multi-level property/key access. - `{list[0]}`: Accesses the element at index `0` in the `list` collection (arrays, `List`, etc.). - `{user.names[1]}`: First accesses the `names` property of the `user` object, then takes the element at index `1`. - `{dic.key}`: When `dic` is a dictionary (including `Dictionary` and `Hashtable`, etc.), `dic.key` is resolved as `dic["key"]`. - `{obj.dictProp.someKey[0].another}`: Mixes dot and index notation to drill down level by level. > **Nested `JSON` value access in dictionary values** When a dictionary (`IDictionary`) is used as the data source, if the value of a key is itself a valid `JSON` string (such as an object or array), the framework automatically parses that `JSON` and continues accessing the internal data via `.` and `[index]`. For example: if the dictionary contains `["user"] = "{\"name\":\"Monk\",\"tags\":[\"A\",\"B\"]}"`, then `{user.name}` is replaced with `Monk`, and `{user.tags[0]}` is replaced with `A`. This way, you only need to serialize a complex object into `JSON` and store it in the dictionary, and then use the unified placeholder syntax to perform deep value access, greatly simplifying the template concatenation logic. All of the above paths support appending `?` at the end to indicate that the value is replaced with an empty string when it does not exist, and adding a `**` prefix to indicate that the path separator `/` is not escaped. --- **Configuration Parameters** In addition to setting path parameters via the `{key}` template syntax, the framework also provides configuration parameters for reading configuration information and performing replacement. Configuration parameters use the `[[key]]` syntax, for example: ```cs showLineNumbers HttpRequestBuilder.Get("https://furion.net?id=[[id]]&name=[[name]]"); ``` **Enabling Configuration Parameter Support** To enable configuration parameter support in the `HttpRemote` service, configure it as follows: ```cs showLineNumbers {2,5} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { // Sets the provider source used to replace the configuration template parameters in the URL options.Configuration = builder.Configuration; // When using the Furion framework, you can directly set App.Configuration }); ``` **Using Configuration Parameters** Configuration parameters are read from your configuration file and replaced into the `URL`. For example, your configuration file might look like this: ```json showLineNumbers title="appsettings.json" { "id": 1, "name": "Furion" } ``` Configuration parameter keys support multiple format syntaxes for more flexible access to values in the configuration file: - `[[key]]`: Directly accesses the value corresponding to `key`. - `[[key:sub]]`: Accesses the value of the `sub` sub-item under `key`. - `[[key:sub:nest]]`: Accesses the value of the `nest` sub-item within the `sub` sub-item under `key`. - Fallback value lookup: - `[[notfound | bak]]`: If `notfound` does not exist, looks up `bak`. - `[[notfound | bak | other]]`: If neither `notfound` nor `bak` exists, looks up `other`. - `[[notfound | bak:sub | other:sub:nest]]`: Supports deeper fallback lookups. - Default values: - `[[notfound || default]]`: If `notfound` does not exist, uses `default` as the value. - `[[notfound | bak | other || default value]]`: Combines fallback lookup and default values to ensure a value is always available. --- # 3.29 Setting Cookie > Source: https://http.furion.net/en/docs/request-builder/setting-cookie/ Adds or modifies `Cookie`. ```cs showLineNumbers {2-7} HttpRequestBuilder.Get("https://furion.net/") .WithCookie("id", 1) // Sets a single Cookie, generating: id=1 .WithCookie("date", DateTime.Now, format: "yyyyMMdd") // Supports format formatting .WithCookie("name", new[] { "furion", "monksoul" }) // Adds multiple values, generating: name=furion,monksoul .WithCookie("DeviceId=; ASP.NET_SessionId=dr1kcfupurtqpk42dzhwvsvq; CookieLastUName=sh") // Supports a Cookie header value string .WithCookies(new Dictionary { }) // Sets multiple Cookies .WithCookies(new { id = 1, name = "Furion" }); // Sets multiple Cookies, generating: id=1; name=Furion ``` If a `Cookie` key is duplicated, the later key-value setting overrides the previous one. > **Configuration Parameter Support** `Cookie` values support configuration parameters for reading configuration information and performing replacement. Configuration parameters use the `[[key]]` syntax. --- # 3.30 Setting Cookies to Remove > Source: https://http.furion.net/en/docs/request-builder/setting-cookies-to-remove/ Removes the specified `Cookie`. ```cs showLineNumbers {2} HttpRequestBuilder.Get("https://furion.net/") .RemoveCookies("id", "name", "age"); // Removes multiple Cookies ``` Before the `HTTP` request is sent, the configured set of `Cookie` keys to remove will be removed. In other words, the `RemoveCookies` method executes after all `WithCookie[s]` method calls. --- # 3.31 Setting the HttpClient instance name (multiple base addresses) > Source: https://http.furion.net/en/docs/request-builder/setting-the-httpclient-instance-name-multiple-base-addresses/ By default, the system uses `IHttpClientFactory` to create `HttpClient` instances and sets the default client name to an empty string (`string.Empty`). You can specify the client name used when creating the `HttpClient` instance. ```cs showLineNumbers {2,5} HttpRequestBuilder.Get("https://furion.net/") .SetHttpClientName(string.Empty); // Use the default client (usually no need to set it explicitly) HttpRequestBuilder.Get("https://furion.net/") .SetHttpClientName("weixin"); // Specify the client named "weixin" ``` You can also provide configuration for a named `HttpClient` client in the `Startup.cs` or `Program.cs` file: ```cs showLineNumbers {2,5} // Configure the default client (whose name is an empty string) services.AddHttpClient(string.Empty, client => { }); // Configure the client named "weixin" services.AddHttpClient("weixin", client => { }); ``` --- # 3.32 Setting the maximum buffer size for response content > Source: https://http.furion.net/en/docs/request-builder/setting-the-maximum-buffer-size-for-response-content/ Configures the maximum number of bytes to buffer for the response content of a single request. ```cs showLineNumbers {2} HttpRequestBuilder.Get("https://furion.net/") .SetMaxResponseContentBufferSize(10 * 1024); // Set to 10KB ``` If the `Content-Length` of the response content exceeds the configured `MaxResponseContentBufferSize` limit (for example, a limit of `10240` bytes), an `HttpRequestException` is thrown. The message of that exception is: `Cannot write more bytes to the buffer than the configured maximum buffer size: '10240'.`. > **Notes on the maximum buffer size of `HttpClient` response content** When setting the maximum number of bytes to buffer for response content on `HttpClient`, make sure the maximum buffer size for the response content of a single request does not exceed the maximum buffer size configured for the `HttpClient` response content. For example, if the maximum buffer size of the `HttpClient` response content is set to `5120` bytes while the maximum buffer size of a single request is set to `10240` bytes, then the single request will still trigger an `HttpRequestException` when it exceeds `5120` bytes. The sample code is as follows: ```cs showLineNumbers {1,3} services.AddHttpClient(string.Empty, client => { client.MaxResponseContentBufferSize = 5 * 1024; }); ``` Therefore, **if a single request needs a larger maximum buffer size for its response content, make sure the maximum buffer size of the `HttpClient` response content is set correspondingly larger.** --- # 3.33 Setting the HttpClient instance provider > Source: https://http.furion.net/en/docs/request-builder/httpclient-provider/ By default, the system creates and automatically manages the lifetime of `HttpClient` instances through `IHttpClientFactory`. If you need to manage the `HttpClient` lifetime manually, you can configure a separate `HttpClient` instance for a single request. ```cs showLineNumbers {2} HttpRequestBuilder.Get("https://furion.net/") .SetHttpClientProvider(() => (new HttpClient(), client => client.Dispose())); ``` `SetHttpClientProvider` method description: - The parameter type is the `Func<(HttpClient, Action?)>` delegate. - This delegate returns a tuple, where the first element is the `HttpClient` instance used to send the request. - The second element (optional) is a delegate used to dispose the `HttpClient` instance after the request completes. Before the request is sent, the system calls this delegate (if it exists) and obtains the `HttpClient` instance to make the request; after the request completes, if a disposal delegate was provided, it is called to dispose the `HttpClient` instance. --- # 3.34 Adding request content processors > Source: https://http.furion.net/en/docs/request-builder/adding-request-content-processors/ The `IHttpContentProcessor` interface defines how to build an `HttpContent` instance based on the content type or raw type of the request. ```cs showLineNumbers {2} HttpRequestBuilder.Post("https://furion.net/") .AddHttpContentProcessors(() => [ new StringContentProcessor() ]); // Multiple processors can be added ``` --- # 3.35 Adding response content converters > Source: https://http.furion.net/en/docs/request-builder/adding-response-content-converters/ The `IHttpContentConverter` interface specifies how to convert the response content `HttpResponseMessage` into an instance of the target type. ```cs showLineNumbers {2} HttpRequestBuilder.Post("https://furion.net/") .AddHttpContentConverters(() => [ new StringContentConverter() ]); // Multiple converters can be added ``` --- # 3.36 Enabling HttpClient Pooling Management > Source: https://http.furion.net/en/docs/request-builder/enabling-httpclient-pooling-management/ By default, a new `HttpClient` instance is created each time an `HTTP` request is sent. However, in scenarios that require frequent requests, this approach may cause performance bottlenecks and excessive memory usage, especially during stress testing. To optimize performance, we can enable `HttpClient` pooling management so that `HttpClient` instances are reused throughout requests. ```cs showLineNumbers {2,5-8,11} var httpRequestBuilder = HttpRequestBuilder.Get("https://furion.net/") .UseHttpClientPool(); // Enable HttpClient pooling management // Send requests in a loop for (var i = 0; i < 10; i++) { await httpRemoteService.SendAsync(httpRequestBuilder); } // Release resources to avoid memory leaks httpRequestBuilder.ReleaseResources(); ``` > **Note** After enabling `HttpClient` pooling management, the `HttpClient` instance will not be released automatically. Therefore, after all requests complete, you must manually call the `httpRequestBuilder.ReleaseResources()` method to release resources and prevent memory overflow. --- # 3.37 Adding Resources to Release When the Request Ends > Source: https://http.furion.net/en/docs/request-builder/adding-resources-to-release-when-the-request-ends/ Memory safety is an issue every developer must take seriously. When sending an `HTTP` request, you sometimes need to introduce unmanaged resources — for example, when sending a file, you need to read the file from disk and send it as a stream. In such cases, if handled improperly, you may encounter the problem of the stream resource not being released. To solve this problem, we can add resources that are automatically processed for release after the request ends: ```cs showLineNumbers {2,5-7,12} // Open the file and read the file stream (without using) var fileStream = File.OpenRead(@"C:\Workspaces\httptest.jpg"); var httpRequestBuilder = HttpRequestBuilder.Post("https://furion.net/") .SetContent(fileStream); // Set the request content .AddDisposable(fileStream) // Add a resource to release when the request ends .AddDisposables(fileStream1, fileStream2); // Supports adding resources in bulk // Send the request var responseMessage = await httpRemoteService.SendAsync(httpRequestBuilder); // At this point, fileStream, fileStream1, and fileStream2 have been released automatically. ✅ ``` The `AddDisposable` method accepts any object that implements the `IDisposable` interface as an argument, and it can be called repeatedly; each call adds a new `IDisposable` object to the collection. --- # 3.38 Managing and Releasing Resources > Source: https://http.furion.net/en/docs/request-builder/managing-and-releasing-resources/ Refer to sections `3.36` and `3.37` to learn how to release resources at the end of a request to avoid memory leaks. ```cs showLineNumbers {2} // Release resources to avoid memory leaks httpRequestBuilder.ReleaseResources(); ``` The following is the underlying implementation of the `ReleaseResources` method, which is responsible for managing and releasing all resources associated with the `HTTP` request: ```cs showLineNumbers {1,11,14} public void ReleaseResources() { // Null check if (HttpClientPooling is not null) { HttpClientPooling.Release?.Invoke(HttpClientPooling.Instance); HttpClientPooling = null; } // Release the collection of disposable objects ReleaseDisposables(); } internal void ReleaseDisposables() { // Null check if (Disposables.IsNullOrEmpty()) { return; } // Iterate and release each item foreach (var disposable in Disposables) { disposable.Dispose(); } // Clear the collection Disposables.Clear(); } ``` --- # 3.39 Setting the operation before adding request content > Source: https://http.furion.net/en/docs/request-builder/setting-the-operation-before-adding-request-content/ Before assigning the `HttpContent` instance to the `Content` property of the `HttpRequestMessage` object, you can perform some additional pre-processing operations. ```cs showLineNumbers {2,5-10} HttpRequestBuilder.Post("https://furion.net/") .SetOnPreSetContent(httpContent => { // Example: set the Content-Disposition request header for the request content httpContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") { Name = multipartFormDataItem.Name, FileName = multipartFormDataItem.FileName, Size = multipartFormDataItem.FileSize }; }); ``` **Note**: The `SetOnPreSetContent` method supports multiple calls, and the results of each call are accumulated and combined. --- # 3.40 Setting the operation before sending the request > Source: https://http.furion.net/en/docs/request-builder/setting-the-operation-before-sending-the-request/ Before sending the `HTTP` remote request, you can perform some pre-processing operations. ```cs showLineNumbers {2,5} HttpRequestBuilder.Post("https://furion.net/") .SetOnPreSendRequest(requestMessage => { // Example: add a request header named "header1" requestMessage.Headers.TryAddWithoutValidation("header1", "value1"); }); ``` **Note**: The `SetOnPreSendRequest` method supports multiple calls, and the results of each call are accumulated and combined. --- # 3.41 Setting the operation after receiving the response > Source: https://http.furion.net/en/docs/request-builder/setting-the-operation-after-receiving-the-response/ After receiving the `HTTP` response, you can perform some post-processing operations. ```cs showLineNumbers {2,5} HttpRequestBuilder.Post("https://furion.net/") .SetOnPostReceiveResponse((responseMessage, cancellationToken) => { // Example: print the response status code Console.WriteLine(responseMessage.StatusCode); return Task.CompletedTask; }); ``` **Note**: The `SetOnPostReceiveResponse` method supports multiple calls, and the results of each call are accumulated and combined. --- # 3.42 Setting the handling when sending the request fails > Source: https://http.furion.net/en/docs/request-builder/setting-the-handling-when-sending-the-request-fails/ When an exception occurs while sending an `HTTP` request, you can perform some error-handling operations. ```cs showLineNumbers {2,5} HttpRequestBuilder.Post("https://furion.net/") .SetOnRequestFailed((exception, responseMessage) => // Note: responseMessage may be null { // Example: print the exception message Console.WriteLine(exception.Message); }); ``` **Note**: `SetOnRequestFailed` can be used together with the exception suppression method `SuppressExceptions()` to capture and handle request failure information without interrupting the program flow. > **Recommended: use the `WithStatusCodeHandler` approach** It is recommended to use the `WithStatusCodeHandler` method for setting response status code handlers described in section 3.51. When you need to handle specific status codes (such as server error status codes, i.e. those greater than `500`), you can do so as follows: ```cs showLineNumbers {3} HttpRequestBuilder.Get("https://furion.net/") // Indicates that a callback handler is configured for status codes greater than or equal to 500 .WithStatusCodeHandler(">=500", async (responseMessage, cancellationToken) => { Console.WriteLine("Calling the status code handler"); }) ``` --- # 3.43 Ensuring the request succeeds > Source: https://http.furion.net/en/docs/request-builder/ensuring-the-request-succeeds/ When this feature is enabled, an exception is thrown automatically when the `HTTP` response status code is not in the `200-299` range (that is, when the `IsSuccessStatusCode` property is `false`). ```cs showLineNumbers {2,5} HttpRequestBuilder.Post("https://furion.net/") .EnsureSuccessStatusCode(); HttpRequestBuilder.Post("https://furion.net/") .EnsureSuccessStatusCode(false); // Disable validation ``` --- # 3.44 Setting Basic authentication > Source: https://http.furion.net/en/docs/request-builder/setting-basic-authentication/ Adds an `Authorization` header to the request whose value consists of the `Basic` keyword followed by the `Base64` encoding of the `username:password` string. ```cs showLineNumbers{2,5} HttpRequestBuilder.Post("https://furion.net/") .AddBasicAuthentication("username", "password"); HttpRequestBuilder.Post("https://furion.net/") .AddBasicAuthentication("username", null); // Supports setting the password to null ``` > **`Authorization` header value format** The value of the `Authorization` header follows the `Schema value` format, that is, `Basic` followed by a space, and then the `Base64`-encoded `username:password` string. --- # 3.45 Setting Bearer authentication (JWT) > Source: https://http.furion.net/en/docs/request-builder/setting-bearer-authentication-jwt/ Adds an `Authorization` header to the request in the format of the `Bearer` keyword followed by the `Token` string. ```cs showLineNumbers{2,5} HttpRequestBuilder.Post("https://furion.net/") .AddBearerAuthentication("your-token"); HttpRequestBuilder.Post("https://furion.net/") .AddBearerAuthentication("X-Authorization", "your-token"); // Supports a custom header key ``` > **`Authorization` header value format** The value of the `Authorization` header follows the `Schema value` format, that is, `Bearer` followed by a space, and then the `Token` string. --- # 3.46 Setting Digest authentication > Source: https://http.furion.net/en/docs/request-builder/setting-digest-authentication/ Adds an `Authorization` header to the request in the format of the `Digest` keyword followed by the digest string generated from the username and password. ```cs showLineNumbers{2} HttpRequestBuilder.Post("https://furion.net/") .AddDigestAuthentication("username", "password"); ``` > **`Authorization` header value format** The value of the `Authorization` header follows the `Schema value` format, that is, `Digest ` followed by a space, and then the digest string generated from the username and password. --- # 3.47 Setting custom authentication > Source: https://http.furion.net/en/docs/request-builder/setting-custom-authentication/ Adds a custom `Authorization` header to the request, following the `Schema value` format. ```cs showLineNumbers{2,5} HttpRequestBuilder.Post("https://furion.net/") .AddAuthentication(new AuthenticationHeaderValue("your-schema", "your-secret")); HttpRequestBuilder.Post("https://furion.net/") .AddAuthentication("your-schema", "your-secret"); // Overload version that simplifies the new AuthenticationHeaderValue operation ``` > **`Authorization` header value format** The value of the `Authorization` header follows the `Schema value` format, that is, `your-schema` followed by a space, and then the corresponding credential string (`your-secret`). --- # 3.48 Disabling HTTP caching > Source: https://http.furion.net/en/docs/request-builder/cache/ When sending an `HTTP GET` request, the server may cache the result of that request to improve performance. To cancel this caching behavior, you can add the following operation: ```cs showLineNumbers {2,5} HttpRequestBuilder.Get("https://furion.net/") .DisableCache(); HttpRequestBuilder.Get("https://furion.net/") .DisableCache(false); // Enable caching (default) ``` After adding this operation, the `HTTP` request will automatically include the following request headers before being sent to ensure cache control: ```bash showLineNumbers Cache-Control: must-revalidate, no-cache, no-store Pragma: no-cache If-None-Match: "" ``` --- # 3.49 Setting the request event handler > Source: https://http.furion.net/en/docs/request-builder/message-handler/ The `IHttpRequestEventHandler` interface allows you to define pre-processing operations for `HTTP` requests. By implementing this interface, you can create a custom request event handler, such as the `CustomRequestEventHandler` class: ```cs showLineNumbers {1} public class CustomRequestEventHandler : IHttpRequestEventHandler { // Operation before sending the HTTP request public void OnPreSendRequest(HttpRequestMessage httpRequestMessage) {} // Operation after receiving the HTTP response public Task OnPostReceiveResponseAsync(HttpResponseMessage httpResponseMessage, CancellationToken cancellationToken) {} // Operation when an exception occurs while sending the HTTP request public void OnRequestFailed(Exception exception, HttpResponseMessage? httpResponseMessage = null) {} } ``` To enable this handler in your application, register the `CustomRequestEventHandler` service in the `Startup.cs` or `Program.cs` file: ```cs showLineNumbers services.TryAddSingleton(); ``` Next, you can specify this handler when building the `HTTP` request: ```cs showLineNumbers {2,5} HttpRequestBuilder.Get("https://furion.net/") .SetEventHandler(); HttpRequestBuilder.Get("https://furion.net/") .SetEventHandler(typeof(CustomRequestEventHandler)); // Set using the type approach ``` > **Reuse tip** You can create a custom implementation type of the `IHttpRequestEventHandler` interface and reuse that implementation across multiple `HttpRequestBuilder` instances. ### Global Event Handler In addition to configuring each request individually, you can also set a global event handler for a specific `HttpClient` instance via `HttpClientOptions`. This handler takes effect for all requests issued by that client. ```cs showLineNumbers {5} // Configure the default client services.AddHttpClient(string.Empty) .ConfigureOptions(options => // or use the overload: .ConfigureOptions((options, serviceProvider) => { options.HttpRequestEventHandler = new CustomRequestEventHandler(); }); // Configure a specific client services.AddHttpClient("weixin") .ConfigureOptions(options => // or use the overload: .ConfigureOptions((options, serviceProvider) => { options.HttpRequestEventHandler = new CustomRequestEventHandler(); }); ``` > **Execution Order** The execution of event handlers follows the priority below (global first, then specific, then inline): 1. **Global event handler** (`HttpClientOptions.HttpRequestEventHandler`) 2. **Specific event handler** (an `IHttpRequestEventHandler` implementation registered via the `SetEventHandler` method) 3. **Builder inline callbacks** (callbacks set via the `SetOnPreSendRequest`, `SetOnPostReceiveResponse`, and `SetOnRequestFailed` methods) That is, the execution order is: **global → attribute → inline callback**. --- # 3.50 Simulating a Browser Environment (Crawler Detection) > Source: https://http.furion.net/en/docs/request-builder/browser/ When developing a crawler, the target website may serve different page versions — such as `PC` and mobile — based on the user agent (`User-Agent`) or other factors. In addition, some websites implement anti-crawler mechanisms that can identify and block crawler access. To address these issues, we can configure request headers to simulate a real browser environment when making requests. ```cs showLineNumbers {2,5} HttpRequestBuilder.Get("https://www.baidu.com/") .SimulateBrowser(); // Simulate a PC browser environment HttpRequestBuilder.Get("https://www.baidu.com/") .SimulateBrowser(isMobile: true); // Simulate a mobile browser environment ``` After adding this operation, the `HTTP` request will automatically include the following request headers before being sent, ensuring that the server can accurately identify and process the request: ```bash showLineNumbers {2,5} # PC browser user agent Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0 # Mobile browser user agent Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Mobile Safari/537.36 Edg/142.0.0.0 ``` --- # 3.51 Adding Response Status Code Handlers > Source: https://http.furion.net/en/docs/request-builder/statuscode/ When sending an `HTTP` request and receiving a response, we often need to perform specific operations based on different response status codes. To meet this need, `HttpRequestBuilder` provides the `WithStatusCodeHandler` method, which allows us to configure callback handling logic for specific status codes. The following is an example of how to use the `WithStatusCodeHandler` method: ```cs showLineNumbers {3,8,13,18,23,28,33,38} HttpRequestBuilder.Get("https://furion.net/") // Configure a callback handler for status code 200 .WithStatusCodeHandler(200, async (responseMessage, cancellationToken) => { Console.WriteLine("Status code handler invoked"); }) // Configure a callback handler for status code 200 .WithStatusCodeHandler(HttpStatusCode.OK, async (responseMessage, cancellationToken) => { Console.WriteLine("Status code handler invoked"); }) // Configure a callback handler for status codes in the range 200 ~ 299 (inclusive) .WithStatusCodeHandler("200-299", async (responseMessage, cancellationToken) => // Equivalent to "200~299" { Console.WriteLine("Status code handler invoked"); }) // Supports comparison operators such as: >=200, <=300, <100, =100, >100 .WithStatusCodeHandler(">=200", async (responseMessage, cancellationToken) => { Console.WriteLine("Status code handler invoked"); }) // Configure a unified callback handler for status codes 200, 204, and 500 .WithStatusCodeHandler([200, 204, 500], async (responseMessage, cancellationToken) => { Console.WriteLine("Status code handler invoked"); }) // Configure a unified callback handler for all status codes .WithAnyStatusCodeHandler(async (responseMessage, cancellationToken) => { Console.WriteLine("Status code handler invoked"); }) // Configure a unified callback handler for status codes 200~299 (successful requests) .WithSuccessStatusCodeHandler(async (responseMessage, cancellationToken) => { Console.WriteLine("Status code handler invoked"); }) // Supports multiple status code representations, including the HttpStatusCode enum, string status codes, status code ranges, comparison operators, and wildcards .WithStatusCodeHandler([200, "204", HttpStatusCode.InternalServerError, "200-299", ">=200", "*"], async (responseMessage, cancellationToken) => { Console.WriteLine("Status code handler invoked"); }); ``` > **Status Code Parameter Types** The status code parameter of the `WithStatusCodeHandler` method supports multiple types: - A positive integer type, e.g. `200`. - A string type, e.g. `"200"`. - The `HttpStatusCode` enum type, e.g. `HttpStatusCode.OK`. - A string range type, e.g. `"200-500"` or `"200~500"`, representing all status codes within that range. - Types containing comparison operators, such as: `">=200"` (greater than or equal to), `"<=300"` (less than or equal to), `"<100"` (less than), `"=100"` (equal to), and `">100"` (greater than) for specific status codes. - The special string `"*"`, which matches all status codes. - A collection of the above types, allowing combined use to match multiple status codes. This way, you can flexibly set the status code parameter according to your actual needs. With the `WithStatusCodeHandler` method, we can flexibly perform different operations based on the response status code, thereby enhancing the ability to handle `HTTP` requests and responses. --- # 3.52 Enabling the Request Analysis Tool > Source: https://http.furion.net/en/docs/request-builder/profiler/ Modern browsers usually have built-in developer tools that can capture and visually display all request and response data when a user visits a website. Similarly, we provide a set of analysis tools for the `HTTP` remote request module. ```cs showLineNumbers {2,5,9-12,18} HttpRequestBuilder.Get("https://furion.net") .Profiler(); // or use Debugger() HttpRequestBuilder.Get("https://furion.net") .Profiler(false); // Disable the request analysis tool, or call any function: DisableProfiler(), DisableDebugger(), Debugger(false) // Get the request analysis tool's data HttpRequestBuilder.Get("https://furion.net") .Profiler(analyzer => { Console.WriteLine(analyzer.Data); }); HttpRequestBuilder.Get("https://furion.net") .Profiler(analyzer => { Console.WriteLine(analyzer.Data); }, false); // Disable the request analysis tool ``` After enabling it, when an `HTTP` remote request is executed, the console will output the following detailed information: ```bash showLineNumbers Request Headers: User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0 X-Header: custom General: Request URL: https://furion.net/ Request Method: GET Status Code: 200 OK HTTP Version: 1.1 HTTP Content: Content Type: HttpClient Name: Request Duration (ms): 149.00 Response Headers: Server: nginx/1.22.1 Date: Thu, 14 Nov 2024 15:35:41 GMT Connection: keep-alive Vary: Accept-Encoding ETag: "67091697-f32f" Cache-Control: max-age=315360000 Accept-Ranges: bytes Content-Type: text/html Content-Length: 62255 Last-Modified: Fri, 11 Oct 2024 12:14:15 GMT Expires: Thu, 31 Dec 2037 23:55:55 GMT ``` > **Note on `Blazor WebAssembly` Projects** In `Blazor WebAssembly` applications, the request analysis tool's content is displayed in the client-side (i.e., browser) developer tools console. Make sure to check this console during development to obtain the relevant analysis information. In addition to enabling the analysis tool for a single request, you can also register it globally to enable it in `HttpClient`: ```cs showLineNumbers {3,7,10,13-14,17-18,21-22} // Enable for the default client services.AddHttpClient(string.Empty) .AddProfilerDelegatingHandler(); // You can also provide conditional disabling, for example disabling in production services.AddHttpClient(string.Empty) .AddProfilerDelegatingHandler(disableIn: () => builder.Environment.EnvironmentName == "Production"); services.AddHttpClient(string.Empty) .AddProfilerDelegatingHandler(disableInProduction: true); // Enable for a specific client //services.AddHttpClient("weixin") // .AddProfilerDelegatingHandler(); // You can also enable it for all clients in one go services.ConfigureHttpClientDefaults(clientBuilder => clientBuilder.AddProfilerDelegatingHandler()); // Or use the IHttpRemoteBuilder extension method for one-click configuration services.AddHttpRemote() .ConfigureHttpClientDefaults(clientBuilder => clientBuilder.AddProfilerDelegatingHandler()); ``` By enabling the request analysis tool, developers can observe and debug `HTTP` requests more intuitively and conveniently, thereby improving development efficiency and debugging accuracy. > **Disable in Production** To ensure optimal performance and security in production, it is recommended to **disable** the request analysis tool in production environments. In addition, printing request content may cause the `Stream` object to be read repeatedly or become unreadable, because the stream is read into memory ahead of time and its `Position` moves to the end accordingly. **Additional note:** By default, the request analysis tool only displays up to `5KB` of the request or response content. --- # 3.53 Setting the Client-Preferred Language and Region > Source: https://http.furion.net/en/docs/request-builder/setting-the-client-preferred-language-and-region/ Globalization is a development trend for internet application products, so applications targeting a global audience should provide internationalization capabilities. When sending an `HTTP` request, you can specify the client's preferred natural language and region by adding the `Accept-Language` header. ```cs showLineNumbers {2,5,8} HttpRequestBuilder.Get("https://furion.net") .AcceptLanguage("en-US"); HttpRequestBuilder.Get("https://furion.net") .AcceptLanguage("zh-CN,en;q=0.5"); HttpRequestBuilder.Get("https://furion.net") .AcceptLanguage("fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5"); ``` --- # 3.54 Setting HttpRequestMessage Properties > Source: https://http.furion.net/en/docs/request-builder/setting-httprequestmessage-properties/ In specific scenarios, we may need to add additional properties to the `HttpRequestMessage` request rather than through request headers. In that case, you can do the following: ```cs showLineNumbers {2-4} HttpRequestBuilder.Get("https://furion.net") .WithProperty("key1", "vallue2") // Set a single property .WithProperties(new Dictionary {}) // Set multiple properties .WithProperties(new { id = 1, name = "Furion" }); // Set multiple properties ``` These properties are added to the `Options` property of the `HttpRequestMessage` object ([reference documentation](https://learn.microsoft.com/zh-cn/dotnet/api/system.net.http.httprequestmessage.options)). To retrieve these values, you can do the following: ```cs showLineNumbers httpRequestMessage.Options.TryGetValue(new HttpRequestOptionsKey("key1"), out var value); ``` If a property key is duplicated, the value set later will override the previous setting. > **Tip** This feature is commonly integrated into custom `DelegatingHandler` and `IHttpRequestEventHandler` components. --- # 3.55 Enabling Standard Request Headers > Source: https://http.furion.net/en/docs/request-builder/enabling-standard-request-headers/ To improve the compatibility of network requests sent by the application through the `HTTP` client and avoid being blocked by `WAF` (`Web` Application Firewall), the framework provides a one-click configuration method for quickly and uniformly setting standard request headers: ```cs showLineNumbers {2,5} HttpRequestBuilder.Get("https://furion.net") .UseStandardRequestHeaders(); HttpRequestBuilder.Get("https://furion.net") .UseStandardRequestHeaders(false); // Disable standard request headers ``` In addition to enabling standard header configuration for a single request, you can also register it globally to enable it in `HttpClient`: ```cs showLineNumbers {4} // Enable for the default client services.AddHttpClient(string.Empty, client => { client.UseStandardRequestHeaders(); }); services.AddHttpRemote(); ``` After enabling standard request headers, the request will automatically add the following headers: - **`Accept`**: `application/json`, `text/plain;q=0.9`, `*/*;q=0.8` (explicit media type priority to avoid being blocked by `WAF`) - **`Connection`**: enables persistent connections (`Keep-Alive`), reducing the overhead of establishing and closing `TCP` connections --- # 3.56 Setting the Automatic Host Header > Source: https://http.furion.net/en/docs/request-builder/setting-the-automatic-host-header/ The `Host` header is a required header in the `HTTP/1.1` protocol. The `Host` header specifies the hostname and port number of the target server for the request, ensuring that the server can correctly distinguish different domains on the same `IP` address and handle them accordingly. The framework provides a convenient method for setting it: ```cs showLineNumbers {2,5} HttpRequestBuilder.Get("https://furion.net") .AutoSetHostHeader(); // Enable HttpRequestBuilder.Get("https://furion.net") .AutoSetHostHeader(false); // Disable the automatic Host header ``` Once enabled, the `Host: furion.net` header is automatically added when sending an `HTTP` remote request. > **Tip** When integrating with `API` interfaces provided by legacy programs, it is recommended to enable this configuration to improve compatibility. > **`Host` Issues Caused by `HttpClient` Automatic Redirection** When sending an `HTTP` remote request, if the target server returns a redirect response (such as `301 Moved Permanently` or `302 Found`), the framework automatically follows the redirect by default. However, when the automatic `Host` header is enabled, you may encounter an issue where the `Host` header cannot be updated. In that case, you can disable the `AllowAutoRedirect` option so that the framework can handle redirection correctly: ```cs showLineNumbers {3,5} // Configure the default client services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { AllowAutoRedirect = false }); ``` In addition, to set the maximum number of redirections for the framework's built-in redirection behavior, you can use the following approach: ```cs showLineNumbers {2,4} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { MaximumAutomaticRedirections = 20; }); ``` This way, you can ensure that the redirection behavior meets expectations while avoiding incorrect `Host` header settings. --- # 3.57 Configuring the Request Base Address > Source: https://http.furion.net/en/docs/request-builder/configuring-the-request-base-address/ When integrating with multiple third-party `API`s, we usually register and configure the `BaseAddress` of multiple `HttpClient` instances globally. For example: ```cs showLineNumbers {4,10} // Configure the base address of the default client services.AddHttpClient(string.Empty, client => { client.BaseAddress = new Uri("https://furion.net/"); }); // Configure the base address of the GitHub client services.AddHttpClient("github", client => { client.BaseAddress = new Uri("https://github.com/"); }); ``` You can then specify the client to use via the `.SetHttpClientName(client name)` method. In addition to global configuration, the framework also supports locally setting the base address for a single request, allowing it to be specified dynamically when building the request: ```cs showLineNumbers {3,7,11} // Set the base address using a string HttpRequestBuilder.Get("/api/test") .SetBaseAddress("https://furion.net"); // Set the base address using a Uri object HttpRequestBuilder.Get("/api/test") .SetBaseAddress(new Uri("https://furion.net")); // Can be used as a prefix HttpRequestBuilder.Get("/api/test") .SetBaseAddress("/furion"); ``` > **Special Note** Make sure that the request base address you set is an absolute path, that is, it starts with `http://` or `https://`. **Processing logic**: - If the request address is an absolute address, the request is sent directly using that address. - If the request address is a relative address: - When no local `BaseAddress` is set, it is concatenated with the `BaseAddress` of the global `HttpClient` instance as the final request address. - When a local `BaseAddress` is set: - If the local `BaseAddress` is a relative address, the local `BaseAddress` is first concatenated before the request address, and then concatenated with the global `BaseAddress`. - If the local `BaseAddress` is an absolute address, that absolute address is concatenated directly with the request address as the final request address (the global `BaseAddress` is ignored in this case). > **Configuration Parameter Support** The request base address supports configuration parameters for reading configuration information to perform substitution. Configuration parameters use the `[[key]]` syntax. --- # 3.58 Configuring the Referer Referrer Address > Source: https://http.furion.net/en/docs/request-builder/configuring-the-referer-referrer-address/ When accessing certain third-party servers, the server may validate the `Referer` referrer address in the request headers. For example, when downloading images, an anti-hotlinking mechanism may be triggered, causing the obtained image to not match expectations. In that case, you can use the `SetReferer` method to set the `Referer` request header, simulating the source page to bypass hotlink protection. ```cs showLineNumbers {2} HttpRequestBuilder.Get("https://furion.net/logo.png") .SetReferer("https://furion.net/"); // Forge a request originating from the homepage ``` To simplify configuration, the framework provides the built-in template string `"{BASE_ADDRESS}"`, which automatically extracts the base address of the request address as the `Referer`: ```cs showLineNumbers {2} HttpRequestBuilder.Get("https://furion.net/logo.png") .SetReferer("{BASE_ADDRESS}"); // Automatically replaces {BASE_ADDRESS} with https://furion.net/ when sending ``` --- # 3.59 Configuring User-Agent > Source: https://http.furion.net/en/docs/request-builder/configuring-user-agent/ The `User-Agent` request header is a characteristic string that allows servers and peer networks to identify the application, operating system, vendor, or version information of the user agent that issued the request. The framework provides the `SetUserAgent` method to set the `User-Agent` request header. ```cs showLineNumbers {2-6} HttpRequestBuilder.Get("https://furion.net/logo.png") .SetUserAgent(UserAgents.Chrome.PC) // Conveniently set using the UserAgents static class .SetUserAgent(UserAgents.Chrome.Mobile) // Set to a mobile User-Agent .SetUserAgent(UserAgents.GetRandom()) // Randomly get a browser-type User-Agent .SetUserAgent(UserAgents.GetByBrowser("Safari")) // Get the User-Agent of a specified browser type .SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"); // Or set the User-Agent string directly ``` > **Default `User-Agent`** If the request does not explicitly set a `User-Agent`, the framework uses `UserAgents.Edge.PC` as its value by default. `UserAgents` is a static class provided by the framework for conveniently setting the `User-Agent` identifiers of mainstream browsers, and it contains the following members: - **Static methods**: - `GetRandom(isMobile)`: randomly gets a browser's `User-Agent`. - `GetByBrowser(browser, isMobile)`: gets the `User-Agent` of a specified browser type. - **Static members**: - `Chrome`: Google Chrome browser - `PC`: desktop `User-Agent` identifier - `Mobile`: mobile `User-Agent` identifier - `Firefox`: Mozilla Firefox browser - `PC`: desktop `User-Agent` identifier - `Mobile`: mobile `User-Agent` identifier - `Safari`: Apple Safari browser - `PC`: desktop `User-Agent` identifier - `Mobile`: mobile `User-Agent` identifier - `Edge`: Microsoft Edge browser - `PC`: desktop `User-Agent` identifier - `Mobile`: mobile `User-Agent` identifier - `Opera`: Opera browser - `PC`: desktop `User-Agent` identifier - `Mobile`: mobile `User-Agent` identifier - `Generic`: generic browser - `PC`: desktop `User-Agent` identifier - `Mobile`: mobile `User-Agent` identifier --- # 3.60 Configuring the HTTP Version > Source: https://http.furion.net/en/docs/request-builder/configuring-the-http-version/ When making an `HTTP` remote request, the default `HTTP` protocol version used is `1.1`. However, when accessing some third-party servers, these servers may validate the `HTTP` version (for example, requiring version `2.0`). In that case, you can configure it in the following two ways: - **Per-request setting** ```cs showLineNumbers {2-4} HttpRequestBuilder.Post("https://furion.net/") .SetVersion(HttpVersion.Version20); // Recommended .SetVersion("1.2"); // Overloaded method .SetVersion(new Version("1.2")); // Overloaded method ``` - **Global configuration** ```cs showLineNumbers {2,4,8,10} // Configure the default client services.AddHttpClient(string.Empty, client => { client.DefaultRequestVersion = HttpVersion.Version10; }); // Configure a specific client services.AddHttpClient("weixin", client => { client.DefaultRequestVersion = HttpVersion.Version10; }); ``` --- # 3.61 Exception Suppression Mechanism (Silent Handling) > Source: https://http.furion.net/en/docs/request-builder/suppression/ When initiating an `HTTP` remote request, you may encounter the following exceptions: - The target host is unreachable - The request is canceled - The request times out - Other network exceptions By default, these exceptions interrupt program execution. Although developers usually use `try/catch` for exception handling, in some scenarios we prefer that exceptions silently return `null` without interrupting the flow. To that end, the framework provides flexible exception suppression functionality. - **Suppress all request exceptions** ```cs showLineNumbers {2} var httpResponseMessage = httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://furion.net/") .SuppressExceptions()); // Suppress all exceptions ``` When an exception occurs during the request, execution is not interrupted; instead `null` is returned, i.e. the value of `httpResponseMessage` is `null`. In some scenarios, while suppressing exceptions, we still want to capture the exception information (for example, to write it to a log) without interrupting the normal execution of the program. In this case, you can use the `SetOnRequestFailed` callback: ```cs showLineNumbers {2-3} HttpRequestBuilder.Post("https://furion.net/") .SuppressExceptions() .SetOnRequestFailed((exception, responseMessage) => // Note: responseMessage may be null { Console.WriteLine(exception.Message); }); ``` This method allows you to safely handle error information after an exception has been suppressed, and is suitable for log recording, monitoring, or other error response logic. - **Suppress only specific types of exceptions** The framework also supports suppressing only specific types of exceptions. For example, you can suppress only timeout exceptions and request cancellation exceptions: ```cs showLineNumbers {2} var httpResponseMessage = httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://furion.net/") .SuppressExceptions([typeof(TimeoutException), typeof(TaskCanceledException)])); // Suppress timeout and cancellation exceptions ``` - **Disable exception suppression configuration** To restore the default behavior (i.e. interrupt the program when an exception occurs), you can explicitly disable exception suppression: ```cs showLineNumbers {2} var httpResponseMessage = httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://furion.net/") .SuppressExceptions(false); // Restore default configuration ``` This configuration is equivalent to not calling `SuppressExceptions()`; when any exception occurs, program execution will be interrupted. > **Notes** When exception suppression is enabled, note the following: 1. **Override rule** When `SuppressExceptions()` or related configurations are called multiple times, **only the last call takes effect**. 2. **Priority between status code checking and exception suppression** Even if `EnsureSuccessStatusCode()` is configured, suppressed exceptions still return `null` and do not trigger the status code checking logic. 3. **Priority of exception suppression** Exception suppression takes priority over status code checking. If both status code checking and exception suppression are enabled, exception suppression takes effect first. 4. **Request interceptors still work** If exceptions are captured via `SetOnRequestFailed(ex, res)` or other request handling mechanisms, interceptors or callbacks will still be invoked even when the exception is suppressed. 5. **Advice on choosing exception types** Carefully choose the exception types to suppress based on the specific business scenario, to avoid masking potential problems by over-suppressing exceptions. 6. **Automatic suppression log output** When an exception is successfully suppressed, the framework automatically outputs a `Warning`-level log (for example, `"An exception occurred but was suppressed by SuppressExceptionPipelineHandler."`) to aid troubleshooting. --- # 3.62 Removing the Default Content-Type of Content > Source: https://http.furion.net/en/docs/request-builder/removing-the-default-content-type-of-content/ When integrating with some older `HTTP` services, setting the `Content-Type` request header when sending request content may cause request handling exceptions. Modern `HTTP` interfaces usually do not have such limitations. If you need to remove the `Content-Type` request header corresponding to the request content when sending a request, you can configure it as follows: ```cs showLineNumbers {2} HttpRequestBuilder.Post("https://furion.net") .SetOmitContentType(true); // Remove the content's default Content-Type ``` In addition to the method above, another approach is to use the `SetOnPreSetContent(Action)` method to modify the request headers before setting the request content, setting `Content-Type` to `null`: ```cs showLineNumbers {2,4} HttpRequestBuilder.Post("https://furion.net") .SetOnPreSetContent(httpContent => { httpContent.Headers.ContentType = null; }); ``` --- # 3.63 Conditional Configuration Builder > Source: https://http.furion.net/en/docs/request-builder/conditional-configuration-builder/ When building a builder instance for an `HTTP` remote request, you often need to dynamically configure request parameters based on different conditions. For example, when the user performs a search operation, the `?search=keyword` query parameter should be added to the request; otherwise, it need not be added. For such scenarios, `HttpRequestBuilder` provides the `When` method to execute the corresponding configuration operation based on a specified condition. This method supports chained calls, making the code cleaner and clearer. ```cs showLineNumbers {2-3} HttpRequestBuilder.Post("https://furion.net") .When(!string.IsNullOrEmpty(token), b => b.AddBearerAuthentication(token)) .When(!string.IsNullOrEmpty(keyword), b => b.WithQueryParameter("search", keyword)); ``` **Explanation of the sample code:** - When `token` is not empty or `null`, `Bearer` authentication is automatically added. - When `keyword` is not empty or `null`, the `search` query parameter is added to the request. This approach can be flexibly applied to various conditional judgment scenarios, improving the maintainability and readability of the code. --- # 3.64 Enabling Assertions > Source: https://http.furion.net/en/docs/request-builder/enabling-assertions/ When developing or writing unit tests and integration tests, we often need to verify whether the request content and response results meet expectations; this process is usually called "assertion". Assertions fall into two categories: - **Request assertions**: executed after building the `HttpRequestMessage` and before sending it, used to validate the request's `URI`, method, headers, body, and so on. If they fail, the request is not sent. - **Response assertions**: executed after receiving the `HttpResponseMessage`, used to validate the status code, response headers, response body, elapsed time, and so on. If an assertion fails, the system throws an `HttpAssertionException`. To enable assertions, call the `UseAssertions()` method and use it together with the `Asserts(configure)` method: ```cs showLineNumbers {2,5} HttpRequestBuilder.Get("https://furion.net") .UseAssertions(); HttpRequestBuilder.Get("https://furion.net") .UseAssertions(false); // Disable assertions ``` --- # 3.65 Configuring Assertion Logic > Source: https://http.furion.net/en/docs/request-builder/configuring-assertion-logic/ After enabling assertions, you can uniformly configure request assertions and response assertions through the `Asserts(configure)` method: ```cs showLineNumbers {2-7} HttpRequestBuilder.Get("https://furion.net") .UseAssertions() .Asserts(ast => ast .RequestMethod(HttpMethod.Get) // Request assertion .ResponseStatusCode(200) // Response assertion .ResponseHeaderExists("encoding") ); ``` Here, the `ast` parameter is of type `HttpAssertionBuilder`, which has a rich set of built-in assertion methods (custom extensions supported): ### Request assertion methods (executed before sending) - **`RequestUri(expectedUri)`**: asserts that the request `URI` equals the specified string - On failure, throws: `Expected request URI to be '{expectedUri}', but found '{actual}'.` - **`RequestMethod(expectedMethod)`**: asserts that the `HTTP` method equals the specified `HttpMethod` - On failure, throws: `Expected request method to be {expectedMethod}, but found {actual}.` - **`RequestHeaderExists(name)`**: asserts that the specified request header exists (including content headers) - On failure, throws: `Expected request header '{name}' to exist, but it was not found.` - **`RequestHeaderEquals(name, expectedValue)`**: asserts that the first value of the request header strictly equals the specified string (case-sensitive) - On failure, throws: `Expected request header '{name}' to be '{expectedValue}', but found '{actual}'.` - **`RequestHeaderContains(name, expectedValue)`**: asserts that any value of the request header contains the specified substring (case-insensitive) - On failure, throws: `Expected request header '{name}' to contain '{expectedValue}', but the header was not found.` or `Expected request header '{name}' to contain '{expectedValue}', but actual values were: [{...}].` - **`RequestContentContains(expectedSubstring)`**: asserts that the request content contains the specified substring (case-insensitive) - On failure, throws: `Expected request content to contain '{expectedSubstring}', but it was not found.` - **`RequestContentEquals(expected)`**: asserts that the request content completely equals the specified string - On failure, throws: `Expected request content to be '{expected}', but found '{actual}'.` - **`RequestSatisfies(assertion)`**: custom request assertion (synchronous or asynchronous), directly operating on `HttpRequestMessage` - The asynchronous overload accepts `Func`. ### Response assertion methods (executed after receiving the response) - **`AddAssertion(assertion)`**: adds a custom assertion delegate (treated as a response assertion by default), such as `ast.AddAssertion(async context => await ...)`. - **`ResponseStatusCode(statusCode)`**: asserts that the response status code equals the specified value (an integer or `HttpStatusCode`) - On failure, throws: `Expected response status code to be {expected}, but found {actual}.` - **`ResponseStatusCodeIn(allowedStatusCodes)`**: asserts that the status code is in the allowed list - On failure, throws: `Expected response status code to be one of [{string.Join(", ", allowedStatusCodes)}], but found {actual}.` - **`ResponseIsSuccessStatusCode()`**: asserts that the request succeeded (status code is `2xx`) - On failure, throws: `Expected response to be successful (2xx status code), but found status code {(int)context.StatusCode}.` - **`ResponseContentContains(expectedSubstring)`**: asserts that the response content contains the specified substring (case-insensitive) - On failure, throws: `Expected response content to contain '{expectedSubstring}', but it was not found.` - **`ResponseContentEquals(expected)`**: asserts that the response content completely equals the specified string - On failure, throws: `Expected response content to be '{expected}', but found '{content}'.` - **`ResponseContentMatches(pattern)`**: asserts that the response content matches the specified regular expression - On failure, throws: `Expected response content to match regex '{pattern}', but it did not.` - **`ResponseContentNotEmpty()`**: asserts that the response content is not empty - On failure, throws: `Expected response content not to be empty.` - **`ResponseHeaderExists(name)`**: asserts that the specified response header exists (including content headers) - On failure, throws: `Expected response header '{name}' to exist, but it was not found.` - **`ResponseHeaderEquals(name, expectedValue)`**: asserts that the first value of the response header strictly equals the specified string (case-sensitive) - On failure, throws: `Expected response header '{name}' to be '{expectedValue}', but found '{actualValue}'.` - **`ResponseHeaderContains(name, expectedValue)`**: asserts that any value of the response header contains the specified substring (case-insensitive) - On failure, throws: `Expected response header '{name}' to contain '{expectedValue}', but the header was not found.` or `Expected response header '{name}' to contain '{expectedValue}', but actual values were: [{string.Join(", ", values)}].` - **`ResponseHeaderNotExists(name)`**: asserts that the specified response header does not exist (including content headers) - On failure, throws: `Expected response header '{name}' not to exist, but it was found.` - **`ResponseDurationUnder(maxMilliseconds)`**: asserts that the request elapsed time is under the specified number of milliseconds - On failure, throws: `Expected response duration to be under {maxDuration.TotalMilliseconds:F2}ms, but it took {actualDuration.TotalMilliseconds:F2}ms.` - **`ResponseSatisfies(assertion)`**: custom response assertion (synchronous or asynchronous), directly operating on `HttpResponseMessage` - The asynchronous overload accepts `Func`. ### Custom assertion methods In addition to the built-in methods, you can add custom assertion logic to `HttpAssertionBuilder` through extension methods. For example, implement a `ResponseIsJson` method to verify whether the response content is of type `application/json`: ```cs showLineNumbers {1,3,5,11-16} public static class HttpAssertionBuilderExtensions { public static HttpAssertionBuilder ResponseIsJson(this HttpAssertionBuilder httpAssertionBuilder) { return httpAssertionBuilder.AddAssertion(async context => { var contentType = context.ResponseMessage?.Content?.Headers.ContentType?.MediaType; const string jsonMediaType = "application/json"; // Allow "application/json" or "application/json; charset=utf-8" and similar if (string.IsNullOrEmpty(contentType) || !contentType.StartsWith(jsonMediaType, StringComparison.OrdinalIgnoreCase)) { await HttpAssertionException.ThrowAsync( $"Expected response Content-Type to be '{jsonMediaType}' (or a subtype with parameters), but found '{contentType}'."); } }); } } ``` Here, the `context` parameter is of type `HttpAssertionContext`, which contains the following properties and methods: - **Properties**: - `RequestMessage`: the sent request message (`HttpRequestMessage?`), available during the request assertion phase - `ResponseMessage`: the response message (`HttpResponseMessage?`), available during the response assertion phase - `StatusCode`: the response status code (of type `HttpStatusCode`) - `IsSuccessStatusCode`: whether the request succeeded (of type `bool`) - `RequestDuration`: the request elapsed time (milliseconds, of type `long`) - `ServiceProvider`: the service provider (of type `IServiceProvider`) - **Methods**: - `ReadResponseAsStringAsync()`: reads the response content string (automatically cached, can be read multiple times) - `ReadRequestAsStringAsync()`: reads the request content string (automatically cached, can be read multiple times) Example of using a custom method: ```cs showLineNumbers {2-3} HttpRequestBuilder.Get("https://furion.net") .UseAssertions() .Asserts(ast => ast.ResponseIsJson().ResponseStatusCode(200)); // Chained calls supported ``` Using `C#` extension methods, you can flexibly extend the functionality of `HttpAssertionBuilder`, improving the maintainability and reusability of the code. --- # 3.66 Enabling the JSON Response Deserialization Wrapper > Source: https://http.furion.net/en/docs/request-builder/enabling-the-json-response-deserialization-wrapper/ When communicating with a third-party `API` over `HTTP`, a unified `JSON` response structure is usually returned, such as the `ApiResult` type, where the actual data is stored in the `Data` property: ```cs showLineNumbers {1,4} public class ApiResult { public bool Success { get; set; } public T? Data { get; set; } // Actual returned data } ``` When the `JSON` response deserialization wrapper is not enabled, each call requires explicitly specifying the `ApiResult` type: ```cs showLineNumbers {1} var content = await httpRemoteService.SendAsAsync>( HttpRequestBuilder.Get("https://furion.net")); ``` ### Enabling #### 1. Enable for a single request To simplify the calling process, you can configure the `JSON` response deserialization wrapper so that it automatically extracts the content of the `Data` property: ```cs showLineNumbers {2-3,5} // Configure the default HTTP client services.AddHttpClient(string.Empty) .ConfigureOptions(options => { options.JsonResponseWrapper = new JsonResponseWrapper(typeof(ApiResult<>), nameof(ApiResult<>.Data)); }); ``` After configuration, enable the feature by calling `UseJsonResponseWrapper()`, and afterwards you only need to specify the target data type without repeatedly declaring `ApiResult`: ```cs showLineNumbers {1-2} var content = await httpRemoteService.SendAsAsync( HttpRequestBuilder.Get("https://furion.net").UseJsonResponseWrapper()); ``` The framework will automatically create an `ApiResult` instance at runtime and return the value of its `Data` property. #### 2. Enable globally (applies to all requests by default) You can also enable the `JSON` response deserialization wrapper globally by simply setting `UseJsonResponseWrapper` to `true`: ```cs showLineNumbers {2-3,6} // Configure the default HTTP client services.AddHttpClient(string.Empty) .ConfigureOptions(options => { options.JsonResponseWrapper = new JsonResponseWrapper(typeof(ApiResult<>), nameof(ApiResult<>.Data)); options.UseJsonResponseWrapper = true; }); ``` After enabling globally, all requests use the wrapper by default: ```cs showLineNumbers {2} var content = await httpRemoteService.SendAsAsync( HttpRequestBuilder.Get("https://furion.net")); // No need to explicitly call UseJsonResponseWrapper() ``` #### 3. Disabling Once (Overriding Global Settings) If you need to disable this feature for a specific request, call the following method: ```cs showLineNumbers {1,2} var content = await httpRemoteService.SendAsAsync>( HttpRequestBuilder.Get("https://furion.net").UseJsonResponseWrapper(false)); ``` By default, not calling `UseJsonResponseWrapper()` means the feature is not enabled, in which case you must pass the complete response type, unless `UseJsonResponseWrapper = true` is configured globally. ### Custom Result Handling (`ResultHandler`) Sometimes, in addition to extracting `Data`, you need to perform additional validation or transformation on the response. This can be achieved through the `ResultHandler` callback: ```cs showLineNumbers {7,12,16,19} // Configure the default HTTP client services.AddHttpClient(string.Empty) .ConfigureOptions(options => { options.JsonResponseWrapper = new JsonResponseWrapper(typeof(ApiResult<>), nameof(ApiResult<>.Data)) { ResultHandler = context => { if (context.Instance is { } instance) { // You can access the wrapper type instance and get any of its properties var success = context.GetPropertyValue(nameof(ApiResult<>.Success)); } // For example, ensure the request succeeds context.ResponseMessage.EnsureSuccessStatusCode(); // Return the final target result (i.e., the value of Data) return context.Result; } }; }); ``` Through `ResultHandler`, you can execute any custom logic (such as validation, transformation, or exception handling) before returning the final data, making request processing more flexible. The type of the `context` parameter is `JsonResponseWrapperContext`, which contains the following properties and methods: - **Properties**: - `Instance`: The concrete instance of the wrapper type (such as `ApiResult`, of type `object?`). - `Result`: The target result (i.e., the value of `Data`, of type `object?`). - `ResponseMessage`: The response message (of type `HttpResponseMessage`). - **Methods**: - `GetPropertyValue(propertyName)`: Gets the value of a specified property from the concrete type of the wrapper (i.e., `Instance`). --- # 3.67 Handling Double Serialization of Response JSON > Source: https://http.furion.net/en/docs/request-builder/handling-double-serialization-of-response-json/ When performing `HTTP` remote communication with a third-party `API`, in very rare cases you may encounter `JSON` data returned by the server that has been accidentally double-serialized (sometimes it may even be intentional). For example, the server should have returned `"{\"id\":1,\"name\":\"furion\"}"`, but due to double serialization it became `"\"{\\\"id\\\":10, \\\"name\\\":\\\"furion\\\"}\""`. For such cases, the framework provides unwrapping support: ```cs showLineNumbers {2} var content = await httpRemoteService.SendAsAsync( HttpRequestBuilder.Get("https://furion.net").UseJsonResponseStringUnwrap()); ``` By calling the `UseJsonResponseStringUnwrap()` method, you enable unwrapping of the `JSON` response content, which correctly converts the double-serialized `JSON` string into the target type (`YourModel`). --- # 3.68 Setting an Operation for Building the Final Request URL > Source: https://http.furion.net/en/docs/request-builder/setting-an-operation-for-building-the-final-request-url/ In certain special scenarios (for example, when you need to dynamically concatenate a service path or query parameters), you can modify the request `URL` through custom logic. Using the `SetOnUriBuilding` method allows you to adjust the individual components of the `UriBuilder` object, thereby building the final request address. ```cs showLineNumbers {2,4} HttpRequestBuilder.Post("https://furion.net/") .SetOnUriBuilding(uriBuilder => { uriBuilder.Query = "?id=10"; }); ``` **Note**: The `SetOnUriBuilding` method supports multiple calls, and the result of each call is accumulated. --- # 3.69 Setting Additional Configuration on Redirect > Source: https://http.furion.net/en/docs/request-builder/setting-additional-configuration-on-redirect/ When a request undergoes automatic redirection (for example, status codes such as `301`, `302`, `307`, `308`), the framework clones a new builder based on the current builder for the redirect request. If you need to perform additional processing on the new builder during redirection (for example, removing the cross-origin `Authorization` header, adjusting request headers, modifying the timeout, etc.), you can register a custom callback using the `SetOnRedirect` method. ```cs showLineNumbers {2,5} HttpRequestBuilder.Post("https://furion.net/") .SetOnRedirect((originalBuilder, redirectBuilder) => { // For example, remove the Authorization header on redirect redirectBuilder.Headers?.Remove("Authorization"); }); ``` **Parameter description**: - `originalBuilder`: The original builder before redirection (or the builder from the previous redirect), from which the original configuration can be read. - `redirectBuilder`: The cloned builder about to be used for the redirect; you can modify any of its properties. **Note**: The `SetOnRedirect` method supports multiple calls, and each registered callback is executed in turn upon redirection. --- # 3.70 Clearing Request Content > Source: https://http.furion.net/en/docs/request-builder/clearing-request-content/ When writing `HTTP` proxies or unit tests, sometimes you need to clear the already-set request content and its related configuration in one go. The framework provides the `RemoveContent()` method to achieve this. ```cs showLineNumbers {3,6-7} HttpRequestBuilder.Post("https://furion.net/") .SetContent(new {}, "application/json", Encoding.UTF8) .RemoveContent(); HttpRequestBuilder.Post("https://furion.net/") .SetContent(new MemoryStream(), "application/json", Encoding.UTF8) .RemoveContent(disposeRawContent: true); // Dispose the raw content when it is removed ``` After calling `RemoveContent()`, `ContentType`, `ContentEncoding`, `RawContent`, `MultipartFormDataBuilder`, and `OmitContentType` are all reset to their initial state; if `RawContent` implements `IDisposable` and was previously added to the disposable list via `AddDisposable(rawContent)`, this method also automatically removes it. Based on the newly added merge logic, the documentation has been supplemented and adjusted as follows: --- --- # 3.71 Appending Request Content > Source: https://http.furion.net/en/docs/request-builder/appending-request-content/ When you need to dynamically add more data to existing request content without affecting the already-set `ContentType` and `ContentEncoding`, you can use the `AppendContent()` method. This method intelligently merges based on the types of the existing content and the incoming content: | Existing content type | Incoming content type | Merge behavior | | :----------------------------- | :------------------------------------------------------ | :-------------------------------------------------------------- | | `string` | `string` | Concatenated using the `&` symbol (commonly used for `application/x-www-form-urlencoded`) | | `StringBuilder` | `string` | The incoming string is appended to the end of the `StringBuilder` | | `IDictionary` | `IDictionary` | The two dictionaries are merged, and the value is updated for existing keys | | `NameValueCollection` | `NameValueCollection` or `IDictionary` | Key-value pairs are merged, and a key with the same name can retain multiple values; dictionary values are converted to strings before being added | | `IList` | `IEnumerable` (non-string) | The elements of the incoming collection are appended to the list in order | | Any other type | Any type | Directly overwrites the original content | ```cs showLineNumbers {3-4,9-10,15-16,21-22,27-28,32-33} // String concatenation: one call appends, and multiple calls can append successively HttpRequestBuilder.Post("https://furion.net/") .SetContent("a=1", "application/x-www-form-urlencoded") .AppendContent("b=2"); // Result: a=1&b=2 // Append a string to a StringBuilder var sb = new StringBuilder("a=1"); HttpRequestBuilder.Post("https://furion.net/") .SetContent(sb, "application/x-www-form-urlencoded") .AppendContent("&b=2"); // sb content becomes "a=1&b=2" // Dictionary merge: existing key values are updated var dict = new Dictionary { ["key1"] = "val1" }; HttpRequestBuilder.Post("https://furion.net/") .SetContent(dict, "application/x-www-form-urlencoded") .AppendContent(new Dictionary { ["key2"] = "val2" }); // Merged into { key1: val1, key2: val2 } // NameValueCollection merge (a key with the same name retains multiple values) var nvc = new NameValueCollection { ["id"] = "1" }; HttpRequestBuilder.Post("https://furion.net/") .SetContent(nvc, "application/x-www-form-urlencoded") .AppendContent(new NameValueCollection { ["name"] = "furion", ["name"] = "dotnet" }); // "name" has two values // List append: add new elements to an existing list var list = new List { "item1" }; HttpRequestBuilder.Post("https://furion.net/") .SetContent(list, "application/json") .AppendContent(new[] { "item2" }); // Result: ["item1", "item2"] // Other type overwrite: replace the old content with the new content HttpRequestBuilder.Post("https://furion.net/") .SetContent("original") .AppendContent(new { id = 1 }); // RawContent becomes { id = 1 } ``` > **Notes** - Before using the `AppendContent` method, request content must have been set via methods such as `SetContent`; otherwise, the append operation takes no effect. - This method supports multiple calls and can append different content successively, with each append following the same merge rules. - If a `null` value is passed, it is likewise ignored, and the original request content remains unchanged. - This method does **not** modify the already-set `ContentType` and `ContentEncoding`. --- # 3.72 Disabling Automatic Access Token Management > Source: https://http.furion.net/en/docs/request-builder/disabling-automatic-access-token-management/ The framework has a built-in automatic `Access Token` management feature: you only need to implement the `IHttpAccessTokenProvider` interface and write the logic for obtaining the `Access Token` in the `GetAsync` method. Usually, obtaining an `Access Token` requires a separate `HTTP` request, but if you directly use `IHttpRemoteService` inside `GetAsync` to send a request, it will trigger the automatic management mechanism and fall into infinite recursive calls. In this case, you need to explicitly disable the automatic `Access Token` management for the current request via `.WithoutTokenManagement()` to avoid recursive calls. ```cs showLineNumbers {1,6,8} public class CustomHttpAccessTokenProvider(IHttpRemoteService httpRemoteService): IHttpAccessTokenProvider { /// public async Task GetAsync(CancellationToken cancellationToken) { var serverToken = await httpRemoteService.SendAsAsync(HttpRequestBuilder.Post("https://furion.net") .SetJsonContent(new { username = "furion", password = "your-password"}) .WithoutTokenManagement()); // Skip token management to avoid recursive calls (Declarative Requests use [SuppressTokenManagement]) return new HttpAccessToken(serverToken.Token, serverToken.ExpiresAt) }; } ``` --- # 3.73 Setting Custom Data for the Access Token Request > Source: https://http.furion.net/en/docs/request-builder/setting-custom-data-for-the-access-token-request/ When using `IHttpAccessTokenProvider` to automatically obtain an `Access Token`, you sometimes need to pass additional dynamic parameters (such as a username, password, client secret, etc.) to the `GetAsync` method. Through the `WithAccessTokenData` method, you can preset these parameters when building the request, and the framework automatically copies them into `HttpAccessTokenContext.Items` for use by `GetAsync`. ```cs showLineNumbers {3-4} HttpRequestBuilder.Post("https://furion.net/") .SetHttpClientName("myapi") // Optional .WithAccessTokenData("username", "admin") .WithAccessTokenData("password", "123456") ``` Then, in the `IHttpAccessTokenProvider.GetAsync` method, you can retrieve these values from `context.Items`: ```cs showLineNumbers {3-4} public async Task GetAsync(HttpAccessTokenContext context, CancellationToken cancellationToken) { context.Items.TryGetValue("username", out var username); context.Items.TryGetValue("password", out var password); // Use username and password to obtain a Token... } ``` **Note**: This method supports multiple calls, and duplicate keys are overwritten by the latest value. --- # 3.74 Removing the Trailing / from the URL > Source: https://http.furion.net/en/docs/request-builder/removing-the-trailing--from-the-url/ Some servers are sensitive to the trailing `/` in a path (for example, `/api/` vs `/api`), which may cause a `301` redirect or route matching failure. When this feature is enabled, the framework automatically removes the trailing `/` from the path when constructing the final request address. ```cs showLineNumbers {2,5} HttpRequestBuilder.Post("https://furion.net/") .RemoveTrailingSlash(); HttpRequestBuilder.Post("https://furion.net/") .RemoveTrailingSlash(false); // Disable this feature (default value) ``` The request address will become `https://furion.net`. --- # 3.75 Setting the Request Interface Quota Key > Source: https://http.furion.net/en/docs/request-builder/setting-the-request-interface-quota-key/ Specifies a quota key for the current request, used to associate it with the quota limit rules configured in `HttpClientOptions`. ```cs showLineNumbers {2} HttpRequestBuilder.Post("https://furion.net/") .SetQuotaKey("wechat/accesstoken"); ``` > **Notes on the `QuotaKey` Configuration Key** - If no quota key is specified, or the specified key does not exist in `QuotaLimits`, no quota check is performed and the request is sent normally. - The quota key can be any custom string; a name related to the interface path is recommended for easier identification and management. --- # 3.76 Enabling ETag Response Caching > Source: https://http.furion.net/en/docs/request-builder/enabling-etag-response-caching/ `ETag` (entity tag) is a mechanism in the `HTTP` protocol used to identify the version of a resource. The server returns the resource's `ETag` value in the response headers (for example, `"abc123"`), and the client can carry this value via the `If-None-Match` header in subsequent requests. If the resource has not changed, the server returns `304 Not Modified` and does not need to retransmit the content; otherwise, it returns the new content and a new `ETag`. After `ETag` caching is enabled, the framework handles this process automatically: the first request caches the response and the `ETag`, subsequent requests automatically attach `If-None-Match`; when a `304` status code is received, the cached content is reused directly, reducing data transmission and improving request efficiency. In weak network environments or mobile data-billing scenarios, this mechanism can significantly reduce bandwidth consumption while speeding up response times. ```cs showLineNumbers {2,5} HttpRequestBuilder.Get("https://furion.net/") .UseETag(); HttpRequestBuilder.Get("https://furion.net/") .UseETag(false); // Disable this feature (default value) ``` > **Notes on `ETag` Response Caching** - Only applies to `GET` and `HEAD` requests. - If the request explicitly calls `DisableCache()`, the `ETag` feature is automatically skipped. - The cache is stored in memory by default, and can be replaced with a distributed cache (such as `Redis`) by implementing the `IHttpETagCache` interface. - The default in-memory cache does not limit the number of cache entries or the size of a single response; a large number of unique `URL`s or large responses may cause memory to grow continuously. To avoid this, it is recommended to implement the `IHttpETagCache` interface and replace the default implementation, for example: ```cs showLineNumbers services.Replace(ServiceDescriptor.Singleton()); ``` - If the global request analysis tool (`AddProfilerDelegatingHandler`) is also enabled and you find that the response content is not printed, explicitly calling the `Profiler()` method on the request will resolve it. --- # 3.77 Setting the SOAP Request Header (WebService) > Source: https://http.furion.net/en/docs/request-builder/setting-the-soap-request-header-webservice/ When calling a `Web` service based on the `SOAP` protocol, you usually need to specify `SOAPAction` in the request headers so that the server can identify the operation to be performed. Use the `SetSOAPAction` method to quickly set this header. ```cs showLineNumbers {2,6} HttpRequestBuilder.Get("http://your-host-address/Share/DatabaseManager.asmx") .SetSOAPAction("http://tempuri.org/GetDatabaseList"); // Automatically wrap the SOAPAction value in double quotes (per the SOAP 1.1 specification recommendation) HttpRequestBuilder.Get("http://your-host-address/Share/DatabaseManager.asmx") .SetSOAPAction("http://tempuri.org/GetDatabaseList", addQuotes: true); ``` --- # 3.78 Simulating Request Responses and Exceptions (Mock) > Source: https://http.furion.net/en/docs/request-builder/simulating-request-responses-and-exceptions-mock/ `MockResponse` and `MockException` are designed specifically for unit testing. They let you directly return a preset response or throw a preset exception without actually sending an `HTTP` request, with zero intrusion into business code. ```cs showLineNumbers {3,7,10-15,19,23-24} // Simulate a JSON response (auto-serialized) HttpRequestBuilder.Get("https://api.furion.net/weather") .MockResponse(new { Temperature = 25, Condition = "Sunny" }); // Simulate a custom status code and content type HttpRequestBuilder.Post("https://api.furion.net/upload") .MockResponse(new { Id = 123 }, HttpStatusCode.Created, "application/json"); // Simulate a complete HttpResponseMessage (for complex scenarios such as file streams) var content = new StreamContent(File.OpenRead("test.pdf")); content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf"); var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = content }; HttpRequestBuilder.Get("https://example.com/download") .MockResponse(response); // Simulate an exception (timeout, network interruption, etc.) HttpRequestBuilder.Get("https://api.furion.net/data") .MockException(new HttpRequestException("Connection timed out")); // Clear all mock settings HttpRequestBuilder.Get("https://api.furion.net/data") .MockResponse(new { }) .ClearMock(); // After clearing, the request will be sent normally ``` > **Mock Feature Notes** - `MockResponse(T content, ...)`: serializes `content` to `JSON` and constructs an `HttpResponseMessage`, suitable for the vast majority of `REST API` scenarios. - `MockResponse(HttpResponseMessage)`: allows fully customizing the `HttpResponseMessage`, supporting arbitrary response content such as file streams and binary data. - `MockException(Exception)`: sets a mock exception, which takes priority over `MockResponse`. If both are set, the exception is thrown first. - `ClearMock()`: clears all mock settings and releases the occupied `HttpResponseMessage` resources. - `IsMocked()`: checks whether the current builder is in a mocked state. > **Notes** - **Test environments only**: mock features should be used only in unit tests; avoid misusing them in production code. - **Resource release**: the configured `HttpResponseMessage` is automatically released when calling `ClearMock()` or calling `MockResponse` again, so no manual `Dispose` is required. - **Mutual exclusivity**: `MockResponse` and `MockException` are mutually exclusive; setting one automatically clears the other. When combined with `HttpRemoteService`, this feature can completely replace real network requests, greatly improving the isolation and execution speed of unit tests. --- # 3.79 Cloning and Copying > Source: https://http.furion.net/en/docs/request-builder/cloning-and-copying/ The framework provides `Clone()` and `CopyTo()` methods for copying or migrating the configuration of an `HttpRequestBuilder`. This is especially useful when you need to reuse a set of common configuration (such as authentication headers, timeout values, etc.) and apply it to multiple different requests. ```cs showLineNumbers {3,7} var httpRequestBuilder = HttpRequestBuilder.Get("https://furion.net/").Profiler(); // Clone operation: returns a brand-new builder var newBuilder = httpRequestBuilder.Clone(); // Full clone var newBuilder = httpRequestBuilder.Clone("RequestUri", "HttpMethod"); // Exclude specific properties // Copy operation: copies the source builder's configuration to the target builder var sourceBuilder = HttpRequestBuilder.Get("https://furion.net/api/user").UseETag().SetQuotaKey("api/user"); sourceBuilder.CopyTo(httpRequestBuilder); // Full copy sourceBuilder.CopyTo(httpRequestBuilder, "RequestUri", "HttpMethod"); // Exclude specific properties ``` --- # 3.80 Setting Custom Data > Source: https://http.furion.net/en/docs/request-builder/setting-custom-data/ In certain special scenarios you may need to inject custom data into the `HttpRequestBuilder` object so that downstream internal components or interceptors can access it. Use the `WithData` method: ```cs showLineNumbers {2} HttpRequestBuilder.Post("https://furion.net/") .WithData("somekey", "somedata"); ``` The stored data is kept in the `httpRequestBuilder.Items` dictionary and can be retrieved by key: ```cs showLineNumbers {3-4} httpRequestBuilder.Items.TryGetValue("somekey", out var somedata); ``` **Note**: the method can be called multiple times — a repeated key is overwritten with the latest value. --- # 3.81 Getting the Request Builder or Request Message (Pre-flight Request) > Source: https://http.furion.net/en/docs/request-builder/getting-the-request-builder-or-request-message-pre-flight-request/ In some cases, you may want to **only obtain the `HTTP` request object itself without actually sending the request**. For example: verifying in unit tests that the generated request matches expectations, obtaining the request builder and then manually modifying it further, or passing the request message to another system for execution. For this purpose, when the return type of a method that sends an `HTTP` remote request is `HttpRequestBuilder` or `HttpRequestMessage`, the framework directly builds and returns that object, **skipping the actual network transfer**. Example: ```cs showLineNumbers {2,4,7-8} // Get the builder; you can continue fluent configuration and then send manually var builder = await httpRemoteService.GetAsAsync("https://furion.net"); // Does not send the request builder.WithHeader("X-Custom", "value"); var httpResponseMessage = await httpRemoteService.SendAsync(builder); // Initiates the network request // Get the HttpRequestMessage for assertions or external passing var httpRequestMessage = await httpRemoteService.GetAsAsync("https://furion.net"); // Does not send the request Assert.Equal("https://furion.net/", httpRequestMessage.RequestUri?.ToString()); ``` ### Use Cases - **Pre-flight Check**: before formally sending, inspect whether the generated request object matches expectations. After confirming that the `URL`, request headers, `Token` injection, etc. are all correct, send it manually or continue processing. - **Unit testing**: without simulating a network environment, directly verify that the generated `HttpRequestMessage` contains the correct parameters, headers, and authentication information. - **Request object passing**: pass the constructed `HttpRequestMessage` to other services, libraries, or processes for execution, achieving separation between request construction and request execution. - **Hybrid programming**: first complete most of the configuration via the builder or declarative approach (parameter mapping, `Token` injection, etc.), then obtain the builder, make minor dynamic modifications, and send manually — combining the simplicity of the declarative style with the flexibility of the imperative style. > **Note** - Methods that return `HttpRequestBuilder` or `HttpRequestMessage` **do not initiate a network request**; the framework only completes construction in memory. - If a method returns another type (such as `string`, `HttpResponseMessage`, etc.), the framework sends the request normally and returns the corresponding result. - This feature complements [frozen parameter types](/en/docs/declarative/frozen-parameter-types/) (such as `Action`), which inject configuration before sending but cannot prevent the send. --- # 3.82 HttpRequestBuilder Unified Configurator > Source: https://http.furion.net/en/docs/request-builder/httprequestbuilder-unified-configurator/ When constructing `HttpRequestMessage` objects through the `HttpRequestBuilder` class, if you need to globally configure all requests, the framework provides a unified configuration mechanism. Developers can implement the `IHttpRequestBuilderConfigurator` interface to apply unified settings to `HttpRequestBuilder` instances. For example, the following `RequestBuilderConfigurator` class adds a common request header to all requests in its `Configure` method: ```cs showLineNumbers {1,4} public class HttpRequestBuilderConfigurator : IHttpRequestBuilderConfigurator { /// public void Configure(HttpRequestBuilder httpRequestBuilder) { httpRequestBuilder.WithHeader("global", "form_furion"); } } ``` After implementing a custom configurator, you need to assign it to the `RequestBuilderConfigurator` property when configuring `HttpRemoteOptions`: ```cs showLineNumbers {2,4} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { options.RequestBuilderConfigurator = new HttpRequestBuilderConfigurator(); }); ``` Once the configuration takes effect, all `HttpRequestBuilder` instances will execute this unified configuration logic before calling the `Build()` method to construct an `HttpRequestMessage`. --- # 3.83 Custom HttpRequestBuilder Extension Methods > Source: https://http.furion.net/en/docs/request-builder/custom-httprequestbuilder-extension-methods/ In addition to the built-in `HttpRequestBuilder` methods, you can simplify code and reduce duplicate logic through custom extension methods. For example, add a `SetAccept` method to quickly set the `Accept` field in the HTTP request header: ```cs showLineNumbers {1,3,8} public static class HttpRequestBuilderExtensions { public static HttpRequestBuilder SetAccept(this HttpRequestBuilder httpRequestBuilder, string accept) { // Parameter validation: ensure accept is not empty ArgumentException.ThrowIfNullOrWhiteSpace(accept); return httpRequestBuilder.WithHeader("Accept", accept, replace: true); } } ``` Once defined, you can chain-call this method on `HttpRequestBuilder` instances: ```cs showLineNumbers {2} HttpRequestBuilder.Get("https://furion.net") .SetAccept("text/html"); ``` With the power of `C#` extension methods, you can easily extend the functionality of `HttpRequestBuilder`, improving code **readability** and **maintainability** while effectively reducing duplicate code. --- # 3.84 Custom Extensions for Other Builders > Source: https://http.furion.net/en/docs/request-builder/custom-extensions-for-other-builders/ The following builders all derive from the `HttpRequestBuilderConfigurator` abstract type: - `HttpFileDownloadBuilder` - `HttpFileUploadBuilder` - `HttpLongPollingBuilder` - `HttpServerSentEventsBuilder` - `HttpStressTestHarnessBuilder` These builders share a unified extension mechanism. For example, they all support configuring additional request parameters for the underlying `HttpRequestBuilder` through the `With(builder => { ... })` method. `HttpRequestBuilderConfigurator` already includes some common methods (such as `Profiler()`). If you need to extend it further, you can write extension methods as follows: ```cs showLineNumbers {1,11-12,14} public static class HttpRequestBuilderConfiguratorExtensions { /// /// Throws an exception when the HTTP response's IsSuccessStatusCode property is false. /// /// /// /// /// The concrete type of the derived builder /// Returns the builder instance itself, supporting chained calls. public static THttpBuilder EnsureSuccessStatusCode(this HttpRequestBuilderConfigurator configurator) where THttpBuilder : HttpRequestBuilderConfigurator { return configurator.With(builder => builder.EnsureSuccessStatusCode()); } } ``` In this way, all of the above builders can directly call the `EnsureSuccessStatusCode()` method and return the builder instance itself, maintaining a fluent chained-call experience. --- # 4.1 HttpMultipartFormDataBuilder Form Builder > Source: https://http.furion.net/en/docs/multipart-builder/httpmultipartformdatabuilder-form-builder/ In internet applications, the most common way to save user-defined data is form submission using `Form`. `Form` forms can transmit not only text data but also binary data (such as files). To build content containing these multipart forms, we use the `HttpMultipartFormDataBuilder` form builder. This builder ultimately produces a `MultipartFormDataContent` object, sets it as the `Content` property of the `HttpRequestMessage`, and specifies the request's content type as `multipart/form-data`. --- # 4.2 Creating a Builder Instance > Source: https://http.furion.net/en/docs/multipart-builder/creating-a-builder-instance/ Because the constructor of `HttpMultipartFormDataBuilder` is private, it cannot be instantiated directly using the `new` keyword. To set multipart form content in an `HTTP` remote request, you must configure it through the `SetMultipartContent(Action)` method provided by the `HttpRequestBuilder` object. ```cs showLineNumbers {2} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { // The type of multipart is HttpMultipartFormDataBuilder }); ``` > **Important** Using the `SetMultipartContent` method will override other content-setting methods (`SetJsonContent`, `SetHtmlContent`, `SetXmlContent`, `SetTextContent`, `SetRawStringContent`, `SetFormUrlEncodedContent`, and `SetContent`). ```cs showLineNumbers {2-5} HttpRequestBuilder.Post("https://furion.net/") .SetMultipartContent(multipart => { multipart.AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file"); }) .SetContent(new { id = 1, name = "Furion" }, "application/json"); // will be overridden ``` --- # 4.3 Setting the Content Boundary > Source: https://http.furion.net/en/docs/multipart-builder/setting-the-content-boundary/ When building multipart form content, you can set the boundary (`Boundary`) for the multipart form content through the following chained-call methods: ```cs showLineNumbers {5,8} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { // Set the boundary via property (not recommended) multipart.Boundary = "--------------------"; // Set the boundary via method (recommended), supports chained calls multipart.SetBoundary("--------------------"); }); ``` > **Tip** The framework provides `Boundary` by default, whose default value is `$"----{DateTime.Now.Ticks:x}"`. Additionally, although both approaches can set the boundary, using the `SetBoundary` method is recommended because it supports chained calls, making the code more concise and readable. --- # 4.4 Keeping the Default Content-Type of the Content > Source: https://http.furion.net/en/docs/multipart-builder/keeping-the-default-content-type-of-the-content/ **When integrating with some older `HTTP` services, the `Content-Type` of the multipart form content should not be set when submitting form data, otherwise it may cause exceptions.** Modern `HTTP` interfaces, however, do not have this limitation. Therefore, by default, when submitting form data, the framework automatically removes the `Content-Type` of the multipart form content. If you need to disable this behavior, you can configure it as follows: ```cs showLineNumbers {4,6,14} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.OmitContentType = false; // Keep the default Content-Type of the multipart content // Or use multipart.SetOmitContentType(false); }); // [Recommended] Use the SetMultipartContent(Action configure, bool omitContentType) overload HttpRequestBuilder.Post("https://furion.net/") .SetMultipartContent(multipart => { // ... }, false); // Keep the default Content-Type of the multipart content ``` --- # 4.5 Adding a Single Form Item > Source: https://http.furion.net/en/docs/multipart-builder/adding-a-single-form-item/ Adds an independent item to the multipart form content, i.e., adds a single form property. ```cs showLineNumbers {4-5} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddFormItem(1, "id"); // Will be assigned to the Id property of FormClass multipart.AddFormItem("Furion", "name"); // Will be assigned to the Name property of FormClass }); ``` The above code corresponds to the class definition received on the server side, for example: ```cs showLineNumbers {3-4} public class FormClass { public int Id { get; set; } public string Name { get; set; } // Other properties } ``` --- # 4.6 Adding JSON content > Source: https://http.furion.net/en/docs/multipart-builder/adding-json-content/ When you need to add `JSON` data to multipart form content, `HttpMultipartFormDataBuilder` provides flexible handling depending on whether a form name is specified. **1. No form name specified**: In this case, the `JSON` data is parsed and traversed, and its properties are set as individual form items. Whether you pass an anonymous type or a `JSON` string, the result is the same. ```cs showLineNumbers {4-5} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddJson(new { id = 1, name = "furion" }); // No form name specified (will be assigned to the Id and Name properties of FormClass) // multipart.AddJson("{\"id\":1,\"name\":\"furion\"}"); // Supports JSON strings. Same as above. }); ``` The above code will generate two form items: `Id` and `Name`, which correspond to the class definition received on the server side, such as: ```cs showLineNumbers {3-4} public class FormClass { public int Id { get; set; } public string Name { get; set; } // other properties } ``` **2. Form name specified**: If a form name is specified for the `JSON` data, the entire `JSON` object is set as a nested item of the form. This is typically used when the server expects to receive an object with a nested structure. ```cs showLineNumbers {5-6} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddJson(new { id = 1, name = "furion" }); // No form name specified (will be assigned to the Id and Name properties of FormClass) multipart.AddJson(new { id = 1, name = "furion" }, "child"); // Form name specified, will be assigned to the Child property of FormClass // multipart.AddJson("{\"id\":1,\"name\":\"furion\"}", "child"); // Supports JSON strings. Same as above. }); ``` In this case, the class definition received on the server side should include a nested class, such as: ```cs showLineNumbers {3-5} public class FormClass { public int Id { get; set; } public string Name { get; set; } public ChildClass Child { get; set; } // nested class // other properties... } public class ChildClass { public int Id { get; set; } public string Name { get; set; } // other properties... } ``` > **Recommended: use [raw string literals](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/tokens/raw-string) to set `JSON`** It is recommended to use [raw string literals](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/tokens/raw-string) to set `JSON` data. This feature was newly added in `C# 11`, allowing strings wrapped in three double quotes (`"""`) to contain multi-line text, while escape characters within the string (such as `\n`, `\t`, etc.) are treated as ordinary characters without needing to be escaped. For example: ```cs showLineNumbers {4-9} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddJson(""" { "id": 1, "name": "Furion" } """); }); ``` If you need to insert variables into a raw string, simply add `$$` before the first `"""` and use the `{{variableName}}` template as a placeholder. For example: ```cs showLineNumbers {1,6,9} var val = "Furion"; HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddJson($$""" { "id": 1, "name": "{{val}}" } """); }); ``` Using raw string literals to set `JSON` data simplifies the code and avoids the tedious work of handling escape characters. > **`JSON` String Notes** If the passed `JSON` string has an invalid format, a `JsonException` will be thrown. --- # 4.7 Adding HTML content > Source: https://http.furion.net/en/docs/multipart-builder/adding-html-content/ Add `HTML` content to multipart form content. ```cs showLineNumbers {4} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddHtml("", "data"); }); ``` > **Recommended: use [raw string literals](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/tokens/raw-string) to set `HTML`** Refer to 4.6 Setting `JSON` content. --- # 4.8 Adding XML content > Source: https://http.furion.net/en/docs/multipart-builder/adding-xml-content/ Add `XML` content to multipart form content. ```cs showLineNumbers {4} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddXml("", "data"); }); ``` > **Recommended: use [raw string literals](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/tokens/raw-string) to set `XML`** Refer to 4.6 Setting `JSON` content. --- # 4.9 Adding text content > Source: https://http.furion.net/en/docs/multipart-builder/adding-text-content/ Add text content to multipart form content. ```cs showLineNumbers {4} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddText("Furion", "data"); }); ``` > **Recommended: use [raw string literals](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/tokens/raw-string) to set text** Refer to 4.6 Setting `JSON` content. --- # 4.10 Adding object content (complex forms / file uploads) > Source: https://http.furion.net/en/docs/multipart-builder/adding-object-content-complex-forms--file-uploads/ When you need to add an object to multipart form content, `HttpMultipartFormDataBuilder` provides flexible handling depending on whether a form name is specified. **1. No form name specified**: In this case, the object is parsed and traversed, and its properties are set as individual form items. ```cs showLineNumbers {4} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddObject(new { id = 1, name = "furion" }); // No form name specified (will be assigned to the Id and Name properties of FormClass) }); ``` The above code will generate two form items: `Id` and `Name`, which correspond to the class definition received on the server side, such as: ```cs showLineNumbers {3-4} public class FormClass { public int Id { get; set; } public string Name { get; set; } // other properties } ``` **2. Form name specified**: If a form name is specified for the object, the entire object is set as a nested item of the form. This is typically used when the server expects to receive an object with a nested structure. ```cs showLineNumbers {5} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddObject(new { id = 1, name = "furion" }); // No form name specified (will be assigned to the Id and Name properties of FormClass) multipart.AddObject(new { id = 1, name = "furion" }, "child"); // Form name specified, will be assigned to the Child property of FormClass }); ``` In this case, the class definition received on the server side should include a nested class, such as: ```cs showLineNumbers {3-5} public class FormClass { public int Id { get; set; } public string Name { get; set; } public ChildClass Child { get; set; } // nested class // other properties... } public class ChildClass { public int Id { get; set; } public string Name { get; set; } // other properties... } ``` ### Complex forms containing files (or binary data) In addition to basic data types, object fields can also contain files or binary data (such as `Stream`, `IFormFile`, `FileInfo`, or `MultipartFile`). It is recommended to use the `MultipartFile` type to declare file fields. The sample code is as follows: ```cs showLineNumbers {4} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddObject(new FormClass { Id = 1, Name = "furion", File = MultipartFile.CreateFromPath("file path") }); }); ``` The corresponding model class definition is as follows: ```cs showLineNumbers {5,7} public class FormClass // Supports the [AliasAs] attribute to define aliases { public int Id { get; set; } public string Name { get; set; } public MultipartFile File { get; set; } // public IFormFile File { get; set; } // Note: requires configuration following the steps below } ``` **Note**: If you use `IFormFile` instead of `MultipartFile`, you must ensure that `FormFileContentProcessor` is registered. Registration can be completed by calling `.AddHttpContentProcessors(() => [new FormFileContentProcessor()])` globally or locally: - Single request configuration: ```cs showLineNumbers {2} HttpRequestBuilder.Post("https://furion.net/") .AddHttpContentProcessors(() => [ new FormFileContentProcessor() ]) ``` - Global configuration: In the `Startup.cs` or `Program.cs` file, configure and register the `HttpRemote` service to enable the `IFormFile` content processor functionality: ```cs showLineNumbers {3} services.AddHttpRemote(builder => { builder.AddHttpContentProcessors(() => [ new FormFileContentProcessor() ]); }); ``` > **`JSON` Serialization Configuration Notes** **Note**: When a typed object is passed in, the framework first converts the object to the `IDictionary` type and then adds it item by item as form items. Therefore, this process does not directly use the `JSON` serialization configuration. If you need to specify an alias for a property, define it via the `[AliasAs]` attribute or `multipart.SetFormNameTransformer(namingPolicy)`. > **Tip** The `AddJson`, `AddFormItem`, `AddHtml`, `AddXml`, and `AddText` methods all internally call the `AddObject` method. --- # 4.11 Adding internet file content > Source: https://http.furion.net/en/docs/multipart-builder/adding-internet-file-content/ Add file content from an internet address to multipart form content. ```cs showLineNumbers {4-7} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddFileFromRemote("https://furion.net/img/furionlogo.png", "file"); multipart.AddFileFromRemote("https://furion.net/img/furionlogo.png", "file", "logo.png"); // Custom file name multipart.AddFileFromRemote("https://furion.net/img/furionlogo.png", "file", "logo.png", "image/png"); // Custom media type; if Content-Type is not passed, it will be resolved automatically based on the file extension multipart.AddFileFromRemote("https://furion.net/img/furionlogo.png", configure: request => {}); // Supports configuring HttpClient and HttpRequestMessage instances }); ``` **Note**: When adding a file from an internet address, the file size is limited to `100MB`. > **Notes on the `contentType` Parameter** - If the `contentType` parameter is provided, that value is used. - If `contentType` is not provided but `fileName` is provided, the `MIME` type is resolved based on the file name extension. - If neither is provided, an attempt is made to resolve based on the `URL` file name extension. - If resolution fails, `application/octet-stream` is used by default. --- # 4.12 Adding Base64 string file content > Source: https://http.furion.net/en/docs/multipart-builder/adding-base64-string-file-content/ Add file content from a `Base64` string to multipart form content. ```cs showLineNumbers {4-5} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddFileFromBase64String("77u/5rWL6K+V5paH5Lu25YaF5a65", "file", "test.txt"); multipart.AddFileFromBase64String("77u/5rWL6K+V5paH5Lu25YaF5a65", "file", "test.txt", "text/plain"); // Custom media type; if Content-Type is not passed, it will be resolved automatically based on the file extension }); ``` **Note**: When adding a file from a `Base64` string, the file size is limited to `100MB`. > **Notes on the `contentType` Parameter** - If the `contentType` parameter is provided, that value is used. - If `contentType` is not provided but `fileName` is provided, the `MIME` type is resolved based on the file name extension. - If neither is provided, `application/octet-stream` is used by default. --- # 4.13 Adding local path file content (progress) > Source: https://http.furion.net/en/docs/multipart-builder/adding-local-path-file-content-progress/ Add file content from a local path to the multipart form content. ```cs showLineNumbers {5-7,10-12} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { // File stream approach multipart.AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file"); multipart.AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file", "test.jpg"); // Custom file name multipart.AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file", "test.jpg", "image/jpeg"); // Custom media type; if Content-Type is not provided, it will be automatically resolved based on the file extension // Byte array approach multipart.AddFileAsByteArray(@"C:\Workspaces\httptest.jpg", "file"); multipart.AddFileAsByteArray(@"C:\Workspaces\httptest.jpg", "file", "test.jpg"); // Custom file name multipart.AddFileAsByteArray(@"C:\Workspaces\httptest.jpg", "file", "test.jpg", "image/jpeg"); // Custom media type; if Content-Type is not provided, it will be automatically resolved based on the file extension }); ``` Additionally, the system provides the `AddFileWithProgressAsStream` method, which, compared to `AddFileAsStream`, allows you to obtain file transfer progress in real time. For example: ```cs showLineNumbers {2,7,11-17} // Create a channel for file transfer progress information var progressChannel = Channel.CreateUnbounded(); HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddFileWithProgressAsStream(@"C:\Workspaces\httptest.jpg", progressChannel, "file"); }); // Subscribe to file transfer progress notifications await foreach (var fileTransferProgress in progressChannel.Reader.ReadAllAsync(cancellationToken)) { Console.WriteLine(fileTransferProgress.ToSummaryString()); // Delay one second each iteration await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); } ``` In this way, transfer progress information is printed every second during the file transfer. react-error-boundary > **Notes on the `contentType` parameter** - If the `contentType` parameter is provided, that value is used. - If `contentType` is not provided but `fileName` is, the `MIME` type is resolved based on the file name extension. - If neither is provided, the system attempts to resolve based on the path file name extension. - If resolution fails, `application/octet-stream` is used by default. > **Disabling the request analysis tool** When printing request content, the `Stream` object may be read multiple times or become unreadable. This is because the stream is read into memory in advance and its position pointer moves to the end. This makes it impossible to accurately obtain the upload progress. Therefore, when using `AddFileWithProgressAsStream` to upload resources, it is recommended to disable the request analysis tool to ensure accurate upload progress information can be obtained. --- # 4.14 Adding Stream content > Source: https://http.furion.net/en/docs/multipart-builder/adding-stream-content/ Add `Stream` content to the multipart form content. ```cs showLineNumbers {4-7} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddStream(stream, "file"); multipart.AddStream(stream, "file", "test.txt"); // Set the file name multipart.AddStream(stream, "file", "test.txt", "text/plain"); // Set the media type; if Content-Type is not provided, it will be automatically resolved based on the file extension multipart.AddStream(stream, "file", "test.txt", "text/plain", disposeResourcesOnRequestCompletion: true); // Can set resources to be automatically released after the request completes }); ``` > **Notes on the `contentType` parameter** - If the `contentType` parameter is provided, that value is used. - If `contentType` is not provided but `fileName` is, the `MIME` type is resolved based on the file name extension. - If neither is provided, `application/octet-stream` is used by default. --- # 4.15 Adding byte array content > Source: https://http.furion.net/en/docs/multipart-builder/adding-byte-array-content/ Add byte array content to the multipart form content. ```cs showLineNumbers {4-6} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddByteArray(bytes, "file"); multipart.AddByteArray(bytes, "file", "test.txt"); // Set the file name multipart.AddByteArray(bytes, "file", "test.txt", "text/plain"); // Set the media type; if Content-Type is not provided, it will be automatically resolved based on the file extension }); ``` > **Notes on the `contentType` parameter** - If the `contentType` parameter is provided, that value is used. - If `contentType` is not provided but `fileName` is, the `MIME` type is resolved based on the file name extension. - If neither is provided, `application/octet-stream` is used by default. --- # 4.16 Adding MultipartFile content > Source: https://http.furion.net/en/docs/multipart-builder/adding-multipartfile-content/ The `MultipartFile` type is designed specifically for handling multipart form files. Its constructor is private, so it cannot be instantiated directly using the `new` keyword. However, the framework provides several static overload methods `MultipartFile.CreateFrom[Source]` to create instances of `MultipartFile`. Examples are as follows: ```cs showLineNumbers {5,7,9,11,13} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { // Add a file from a byte array multipart.AddFile(MultipartFile.CreateFromByteArray(bytes, "files")); // Add a file from a Stream multipart.AddFile(MultipartFile.CreateFromStream(stream, "files")); // Add a file from a local path multipart.AddFile(MultipartFile.CreateFromPath(@"C:\Workspaces\httptest.jpg", "files")); // Add a file from a Base64 string multipart.AddFile(MultipartFile.CreateFromBase64String("77u/5rWL6K+V5paH5Lu25YaF5a65", "files")); // Add a file from an internet URL multipart.AddFile(MultipartFile.CreateFromRemote("https://furion.net/img/furionlogo.png", "files")); }); ``` > **Notes on the `Create` static methods of the `MultipartFile` type** The multiple `Create` static methods provided by `MultipartFile` are actually built by calling the corresponding methods of `HttpMultipartFormDataBuilder`, as follows: - `CreateFromByteArray`: calls the `AddByteArray` method of `HttpMultipartFormDataBuilder`. - `CreateFromStream`: calls the `AddStream` method of `HttpMultipartFormDataBuilder`. - `CreateFromPath`: calls the `AddFileAsStream` method of `HttpMultipartFormDataBuilder`. - `CreateFromBase64String`: calls the `AddFileFromBase64String` method of `HttpMultipartFormDataBuilder`. - `CreateFromRemote`: calls the `AddFileFromRemote` method of `HttpMultipartFormDataBuilder`. To learn more, visit [HttpAgent - official repository - `HttpMultipartFormDataBuilder`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Builders/HttpMultipartFormDataBuilder.cs#L434) for reference. --- # 4.17 Adding IFormFile and IFormFileCollection content > Source: https://http.furion.net/en/docs/multipart-builder/adding-iformfile-and-iformfilecollection-content/ In `ASP.NET Core`, the `IFormFile` interface is used to handle single file uploads, while the `IFormFileCollection` interface manages multiple file uploads. These two interfaces simplify the implementation of file upload functionality. The framework provides the `AddFile(IFormFile)` and `AddFiles(IFormFileCollection)` extension methods to make it easy to add files to multipart form content. Examples are as follows: ```cs showLineNumbers {4-7,9-10} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddFile(formFile); multipart.AddFile(formFile, "file"); // Custom form name multipart.AddFile(formFile, "file", "test.txt"); // Custom file name multipart.AddFile(formFile, "file", "test.txt", "text/plain"); // Custom media type multipart.AddFiles(formFiles); multipart.AddFiles(formFiles, "files"); // Custom form name }); ``` **Note**: If `IFormFile` is used as the type of a model property, for example: ```cs showLineNumbers {5} public class FormClass // Supports the [AliasAs] attribute to define an alias { public int Id { get; set; } public string Name { get; set; } public IFormFile File { get; set; } } ``` then you need to ensure that `FormFileContentProcessor` is registered. Registration can be done by calling `.AddHttpContentProcessors(() => [new FormFileContentProcessor()])` globally or locally: - Per-request configuration: ```cs showLineNumbers {2} HttpRequestBuilder.Post("https://furion.net/") .AddHttpContentProcessors(() => [ new FormFileContentProcessor() ]) ``` - Global configuration: In the `Startup.cs` or `Program.cs` file, configure and register the `HttpRemote` service to enable the `IFormFile` content processor feature: ```cs showLineNumbers {3} services.AddHttpRemote(builder => { builder.AddHttpContentProcessors(() => [ new FormFileContentProcessor() ]); }); ``` --- # 4.18 Adding IBrowserFile and IEnumerable content > Source: https://http.furion.net/en/docs/multipart-builder/adding-ibrowserfile-and-ienumerableibrowserfile-content/ In `Blazor`, the `IBrowserFile` interface is used to handle single file uploads, while the `IEnumerable` interface manages multiple file uploads. These two interfaces simplify the implementation of file upload functionality. The framework provides the `AddFile(IBrowserFile)` and `AddFiles(IEnumerable)` extension methods to make it easy to add files to multipart form content. Examples are as follows: ```cs showLineNumbers {4-8,10-12} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddFile(browserFile); multipart.AddFile(browserFile, "file"); // Custom form name multipart.AddFile(browserFile, "file", "test.txt"); // Custom file name multipart.AddFile(browserFile, "file", "test.txt", "text/plain"); // Custom media type multipart.AddFile(browserFile, "file", "test.txt", "text/plain", maxAllowedSize: 512000); // The maximum number of bytes the stream can provide multipart.AddFiles(browserFiles); multipart.AddFiles(browserFiles, "files"); // Custom form name multipart.AddFiles(browserFiles, "files", maxAllowedSize: 512000); // The maximum number of bytes the stream can provide }); ``` --- # 4.19 Adding FileInfo content > Source: https://http.furion.net/en/docs/multipart-builder/adding-fileinfo-content/ If you need to upload a local file, you usually need to first create a `FileInfo` instance and then obtain the stream via `OpenRead()` to upload it. The framework provides the `AddFile(FileInfo)` extension method to make it easy to add files to multipart form content. Examples are as follows: ```cs showLineNumbers {4-5} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddFile(fileInfo); multipart.AddFile(fileInfo, "file"); // Custom form name }); ``` --- # 4.20 Adding URL-encoded form content > Source: https://http.furion.net/en/docs/multipart-builder/adding-url-encoded-form-content/ Add `URL`-encoded form content to the multipart form content. ```cs showLineNumbers {4-5,8} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddFormUrlEncoded(new { id = 1 ,name = "Furion" }, "form"); multipart.AddFormUrlEncoded(new { id = 1 ,name = "Furion" }, "form", useStringContent: true); // Use StringContent to solve the encoding problem of FormUrlEncodedContent // Supports the URL-encoded string format multipart.AddFormUrlEncoded("id=1&name=Furion", "form", useStringContent: true); }); ``` > **Notes on `URL`-encoded form content** - **By default, `URL`-encoded forms are built using the [`FormUrlEncodedContent`](https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Net.Http/src/System/Net/Http/FormUrlEncodedContent.cs#L44) type, but this type does not support custom request content encoding; it uses `Encoding.Latin1` by default instead of `UTF-8`.** This may cause exceptions when submitting to certain endpoints. To solve this problem, you can set the `useStringContent` parameter to `true` to build the form data using `StringContent`, thereby allowing custom encoding to `UTF-8`. ```cs showLineNumbers {3} var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddURLForm", builder => builder .SetFormUrlEncodedContent(new { id = 1, name = "furion" }, useStringContent: true)); ``` - Some servers require an explicit charset declaration, in which case you can specify the encoding via the `contentEncoding` parameter, for example using `UTF-8`: ```cs showLineNumbers {3} var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddURLForm", builder => builder .SetFormUrlEncodedContent(new { id = 1, name = "furion" }, Encoding.UTF8)); ``` When sending the remote request, this setting generates the following `Content-Type` request header: `application/x-www-form-urlencoded; charset=UTF-8`. --- # 4.21 Adding multipart form content > Source: https://http.furion.net/en/docs/multipart-builder/adding-multipart-form-content/ The need to add multipart form content within multipart form content is uncommon. ```cs showLineNumbers {4} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddMultipartFormData(new MultipartFormDataContent(), "form"); }); ``` --- # 4.22 Adding HttpContent content > Source: https://http.furion.net/en/docs/multipart-builder/adding-httpcontent-content/ Add all request content derived from `HttpContent`. ```cs showLineNumbers {4-13} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.Add(new StringContent("test")); multipart.Add(new StringContent("test"), "name"); // Set the form name multipart.Add(new FormUrlEncodedContent([ new KeyValuePair("id", "1"), new KeyValuePair("name", "furion") ])); multipart.Add(JsonContent.Create(new { id = 1, name = "Furion" }); multipart.Add(new StreamContent(stream)); multipart.Add(new ByteArrayContent(bytes), "bytes"); multipart.Add(new ReadOnlyMemoryContent(new ReadOnlyMemory(bytes)); multipart.Add(new MultipartFormDataContent()); }); ``` > **Cases where the form name and content type are not set** When using the `Add` method to add `HttpContent`: - If no form name is specified, the system automatically resolves the name from the `Name` property of `HttpContent.Headers.ContentDisposition`. - If no content type is set, the system attempts to automatically infer the file's `MIME` type from the `FileName` property of `HttpContent.Headers.ContentDisposition` as the content type. --- # 4.23 Setting the Operation Before Adding Form Item Content > Source: https://http.furion.net/en/docs/multipart-builder/setting-the-operation-before-adding-form-item-content/ Before adding a `HttpContent` instance to a `MultipartFormDataContent` object, you can perform some preprocessing operations. For example, when integrating with certain object storage services (such as Alibaba Cloud `OSS`), you may need to remove the `Content-Type` setting (the framework has built in this operation). ```cs showLineNumbers {7-10} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.SetBoundary("--------------------------") .AddFormItem("1", "id") .AddFormItem("Furion", "name") .SetOnPreAddContent((content, name) => // The delegate parameter type is: Action { content.Headers.ContentType = null; }); }); ``` **Note**: The `SetOnPreAddContent` method supports being called multiple times; the results of each call accumulate. --- # 4.24 Setting the Form Name Policy (Transformer) > Source: https://http.furion.net/en/docs/multipart-builder/setting-the-form-name-policy-transformer/ When sending `HTTP` form data, unlike directly sending `JSON` data in `application/json` format, you cannot directly use custom `JSON` serialization options to format property names. When setting an object as form data, the framework first converts the object to the `IDictionary` type and then adds it item by item as form fields. Therefore, this process does not follow the naming rules of `JSON` serialization. Because properties in the C# language are typically named using `PascalCase` naming, when interacting with some third-party services (such as `API`s written in `Java`), the other party may be case-sensitive about field names, causing the request to fail. For this reason, the framework provides the `SetFormNameTransformer` method for configuring the transformation rules for form field names. ```cs showLineNumbers {5} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddObject(new { Id = 1, Name = "Furion"}) .SetFormNameTransformer(FormNamingPolicy.CamelCase); // Use camelCase naming to transform form field names }); ``` The framework has built in the following five common naming-rule transformation approaches, and it also supports custom transformation logic: - **camelCase naming** (`FormNamingPolicy.CamelCase`): for example, transforms `TempCelsius` into `tempCelsius`. - **lowercase snake_case naming** (`FormNamingPolicy.SnakeCaseLower`): for example, transforms `TempCelsius` into `temp_celsius`. - **uppercase snake_case naming** (`FormNamingPolicy.SnakeCaseUpper`): for example, transforms `TempCelsius` into `TEMP_CELSIUS`. - **lowercase kebab-case naming** (`FormNamingPolicy.KebabCaseLower`): for example, transforms `TempCelsius` into `temp-celsius`. - **uppercase kebab-case naming** (`FormNamingPolicy.KebabCaseUpper`): for example, transforms `TempCelsius` into `TEMP-CELSIUS`. In addition, you can implement a specific format through a custom transformer delegate, for example uniformly adding a `_` prefix to all field names: ```cs showLineNumbers {5} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddObject(new { Id = 1, Name = "Furion"}) .SetFormNameTransformer(name => "_" + name); }); ``` --- # 4.25 Setting the Sort Rule for Multipart Form Content Items > Source: https://http.furion.net/en/docs/multipart-builder/setting-the-sort-rule-for-multipart-form-content-items/ Although the need to sort multipart form content items is uncommon, some systems with high security requirements often need to validate the submission order of form fields. The framework provides sorting support for this purpose: ```cs showLineNumbers {6} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddObject(new { name = "furion", id = 1 }); }) .SetFormItemsSorter(items => items.OrderBy(u => u.Name)); ``` Configure the sort rule for multipart form content items through the `.SetFormItemsSorter()` method. This method receives the original `MultipartFormDataItem` collection and returns a sorted enumerable collection. When it is `null`, no sorting is performed (the original insertion order is used). --- # 4.26 Adding HttpMultipartFormDataBuilder Extensions > Source: https://http.furion.net/en/docs/multipart-builder/extensions/ In addition to the `HttpMultipartFormDataBuilder` methods built into the system, you can also add custom extension methods to it to simplify code and reduce duplication. For example, you can add an `AddRawString` method for adding a raw `raw` string content to a multipart form. The concrete implementation is as follows: ```cs showLineNumbers {1,3,8} public static class HttpMultipartFormDataBuilderExtensions { public static HttpMultipartFormDataBuilder AddRawString(this HttpMultipartFormDataBuilder multipartFormDataBuilder, string? rawString, string name, Encoding? contentEncoding = null) { // Null check ArgumentException.ThrowIfNullOrWhiteSpace(name); return multipartFormDataBuilder.AddText($"\"{rawString}\"", name, contentEncoding); } } ``` Afterwards, you can easily use this method in a `HttpRequestBuilder` instance: ```cs showLineNumbers {4} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddRawString("Furion", "body"); }); ``` By leveraging the `C#` extension method feature, you can greatly enrich the functionality of `HttpMultipartFormDataBuilder`, reduce duplicate code, and improve code readability and maintainability. --- # 5.1 HTTP Declarative Requests > Source: https://http.furion.net/en/docs/declarative/http-declarative-requests/ The `HTTP` Declarative Requests mechanism dynamically builds implementation classes at runtime by implementing the `IHttpDeclarative` interface. This mechanism intelligently intercepts method calls that match specific rules and automatically generates the corresponding `HTTP` remote request code. This approach not only greatly reduces the burden on developers writing `HTTP` request code, but also makes the code structure more organized and easier to organize, maintain, and reuse. --- # 5.2 Interface Definition and Usage > Source: https://http.furion.net/en/docs/declarative/interface-definition-and-usage/ Before using `HTTP` Declarative Requests, you need to define an interface and make sure it implements the `IHttpDeclarative` interface: ```cs showLineNumbers {1} public interface IHttpService : IHttpDeclarative { } ``` Then, in the `Startup.cs` or `Program.cs` file, configure and register the `HttpRemote` service to enable the `HTTP` Declarative Requests feature: ```cs showLineNumbers {1,4,7,10,13,16} services.AddHttpRemote(builder => { // Register the IHttpService declarative interface using the generic approach builder.AddHttpDeclarative(); // Or use the type-based approach // builder.AddHttpDeclarative(typeof(IHttpService)); // To register multiple interfaces, use the following method (not the array syntax shown in this example) // builder.AddHttpDeclaratives(new[] { typeof(IHttpService), typeof(IHttpService) }); // Recommended: scan and batch-register from assemblies // builder.AddHttpDeclarativesFromAssemblies([Assembly.GetEntryAssembly()]); // If using the Furion framework, pass App.Assemblies directly // builder.AddHttpDeclarativesFromAssemblies(App.Assemblies); }); ``` When using the `IHttpService` declarative request in a service, you can inject it through the constructor: ```cs showLineNumbers {3,5} public class YourService { private readonly IHttpService _httpService; public YourService(IHttpService httpService) { _httpService = httpService; } } ``` If you are using `.NET 8` or later, you can use [primary constructors](https://learn.microsoft.com/zh-cn/dotnet/csharp/whats-new/tutorials/primary-constructors) injection to further simplify the code: ```cs showLineNumbers {1} public class YourService(IHttpService httpService) { // Use the httpService variable } ``` In some scenarios, you can also inject only in a specific method by adding the `[FromServices]` attribute in front of the parameter: ```cs showLineNumbers {3} public class YourService { public Task GetResource([FromServices] IHttpService httpService) { // Your business logic } } ``` In addition, if you want to dynamically resolve the declarative service, you can first inject `IHttpRemoteService` and then call its `For()` method to obtain an instance: ```cs showLineNumbers {1,5} public class YourService(IHttpRemoteService httpRemoteService) { public async Task InvokeAsync() { var httpService = httpRemoteService.For(); } } ``` ### Open Generic Interfaces `HTTP` declarative interfaces also support open generic definitions, for example: ```cs showLineNumbers {1} public interface IHttpService : IHttpDeclarative { } ``` Note that when using the assembly scanning approach (such as `builder.AddHttpDeclarativesFromAssemblies(assemblies)`), open generic interfaces are skipped by default, because it requires a concrete type at runtime (that is, a closed generic type). In this case, you should explicitly register the closed generic version: ```cs showLineNumbers {4} services.AddHttpRemote(builder => { // Register a closed generic type, such as IHttpService builder.AddHttpDeclarative>(); // Or use the type-based approach // builder.AddHttpDeclarative(typeof(IHttpService)); }); ``` When using it in business logic, you can obtain the specified closed type (such as `IHttpService`) directly through dependency injection, or call `IHttpRemoteService.For>()` to dynamically resolve the service instance. ### No Need to Implement the `IHttpDeclarative` Interface In some cases, you may want to generate a declarative proxy directly for an ordinary interface without requiring that interface to implement `IHttpDeclarative`. For example, define an ordinary `IMyApi` interface: ```cs showLineNumbers {1,3-4} public interface IMyApi { [Get("https://api.furion.net/users/{id}")] Task GetUserAsync(int id); } ``` In this case, specify `requireIHttpDeclarative: false` when registering: ```cs showLineNumbers {3} services.AddHttpRemote(builder => { builder.AddHttpDeclarative(typeof(IMyApi), requireIHttpDeclarative: false); }); ``` After registration, this interface is used in exactly the same way as an ordinary declarative interface: it can be injected through the constructor, injected with the `[FromServices]` attribute, or dynamically resolved with `IHttpRemoteService.For()`. > **Batch Registration by Scanning Assemblies** **Note**: This registration approach **does not support** the framework's built-in assembly scanning methods (such as `AddHttpDeclarativesFromAssemblies`), because the built-in scanning checks for the `IHttpDeclarative` interface by default. If you need to batch-register such interfaces, you can scan all public interfaces in the assemblies yourself, filter them according to custom constraints (for example, naming conventions, marker attributes, and so on), and then manually call `AddHttpDeclarative(declarativeType, false)` to complete the registration. An example follows: ```cs showLineNumbers {4,8-11} var assemblies = new[] { Assembly.GetExecutingAssembly() }; var apiInterfaces = assemblies .SelectMany(a => a.GetExportedTypes()) .Where(t => t.IsInterface && !t.IsGenericType && t.Name.EndsWith("Api")); // For example, interfaces ending with Api services.AddHttpRemote(builder => { foreach (var interfaceType in apiInterfaces) { builder.AddHttpDeclarative(interfaceType, requireIHttpDeclarative: false); } }); ``` --- # 5.3 Defining Request Methods > Source: https://http.furion.net/en/docs/declarative/defining-request-methods/ In the `IHttpService` declarative interface, you can define various `API` request methods. These methods must be marked with attributes derived from `HttpMethodAttribute` to indicate their corresponding `HTTP` request type. The system provides a variety of common `HTTP` request method attributes out of the box, while also supporting custom method attributes: ```cs showLineNumbers {4,8,12,16,20,24,28,32,36,40,45} public interface IHttpService : IHttpDeclarative { // Define an HTTP GET request [Get("https://furion.net/")] Task GetMethodAsync(); // Define an HTTP PUT request [Put("https://furion.net/")] Task PutMethodAsync(); // Define an HTTP POST request [Post("https://furion.net/")] Task PostMethodAsync(); // Define an HTTP DELETE request [Delete("https://furion.net/")] Task DeleteMethodAsync(); // Define an HTTP HEAD request [Head("https://furion.net/")] Task HeadMethodAsync(); // Define an HTTP OPTIONS request [Options("https://furion.net/")] Task OptionsMethodAsync(); // Define an HTTP TRACE request [Trace("https://furion.net/")] Task TraceMethodAsync(); // Define an HTTP PATCH request [Patch("https://furion.net/")] Task PatchMethodAsync(); // Define an HTTP QUERY request [Query("https://furion.net/")] Task PatchMethodAsync(); // Custom HTTP request method [HttpMethod("Connect", "https://furion.net/")] Task ConnectMethodAsync(); // Define a generic method [Get("https://furion.net/")] Task GenericMethodAsync(); } ``` > **Interface Method Naming Convention** For interface method naming, it is recommended to follow the asynchronous method naming convention, i.e., append the `Async` suffix to the method name, to clearly indicate that these methods perform asynchronous operations. > **Methods Not Marked with the `HttpMethodAttribute` Attribute** If a method in the interface is not marked with an attribute derived from `HttpMethodAttribute`, an `InvalidOperationException` is thrown when it is called, with the message "`No '[HttpMethod]' annotation was found in method 'System.Threading.Tasks.Task UnknownMethodAsync()' of type 'HttpAgent.Samples.IHttpService'.`". ```cs showLineNumbers {4} public interface IHttpService : IHttpDeclarative { // Missing the [HttpMethod] attribute, which will cause an exception Task UnknownMethodAsync(); } ``` ### Custom Request Methods In addition to directly using `[HttpMethod("Connect", "https://furion.net/")]` to add a custom `HTTP` request method, we can also create a concrete `ConnectAttribute` attribute class to improve code reusability and readability. This attribute class inherits from `HttpMethodAttribute` and is specifically used to represent `Connect` requests. ```cs showLineNumbers {1,2,4-5} [AttributeUsage(AttributeTargets.Method)] public sealed class ConnectAttribute : HttpMethodAttribute { public ConnectAttribute(string? requestUri = null) : base("Connect", requestUri) { } } ``` Now we can use the custom `[Connect]` attribute in the `IHttpService` interface to replace the previous `[HttpMethod("Connect", ...)]` attribute: ```cs showLineNumbers {4} public interface IHttpService : IHttpDeclarative { // Use the custom Connect attribute [Connect("https://furion.net/")] Task ConnectMethodAsync(); } ``` Such code is more concise and clear, while also improving the maintainability and reusability of the code. --- # 5.4 Defining Request Addresses > Source: https://http.furion.net/en/docs/declarative/defining-request-addresses/ In the constructors of `HttpMethodAttribute` and its derived attributes, you can configure the request address. The following shows how to use these attributes in the `IHttpService` interface to define different request addresses: ```cs showLineNumbers {4,8,12,16,20,24} public interface IHttpService : IHttpDeclarative { // Use a full URL address [Get("https://furion.net/")] Task GetFullUrlMethodAsync(); // Use a relative address (without a leading slash) [Get("api/get/user")] Task GetRelativeUrlMethod1Async(); // Use a relative address (with a leading slash) [Get("/api/get/user")] Task GetRelativeUrlMethod2Async(); // The request address is an empty string; the actual request is BaseAddress [Get("")] Task GetEmptyUrlMethodAsync(); // The request address is null; the actual request is BaseAddress [Get(null)] Task GetNullUrlMethodAsync(); // The request address is null; the actual request is BaseAddress [Get] Task GetNullUrlMethodAsync(); } ``` - When the provided request address is a complete `URL`, it is used directly as the final request address. - If the request address is a relative address (whether or not it includes a leading slash `/`), the framework attempts to combine it with the `BaseAddress` configured for `HttpClient` to generate the final request address. For example: ```cs showLineNumbers {3} services.AddHttpClient(string.Empty, client => { client.BaseAddress = new Uri("https://furion.net/"); }); ``` In the above configuration, if the request address is `"api/get/user"` or `"/api/get/user"`, the final request address will be `"https://furion.net/api/get/user"`. - If the request address is an empty string or `null`, the `BaseAddress` configured for `HttpClient` is used directly as the final request address. This means that if `BaseAddress` is `"https://furion.net/"`, the final request address will also be `"https://furion.net/"`. --- # 5.5 Synchronous and Asynchronous Methods > Source: https://http.furion.net/en/docs/declarative/synchronous-and-asynchronous-methods/ In the declarative request interface method definitions of `IHttpService`, we provide both the implementation of asynchronous methods and support for defining synchronous methods. For example: ```cs showLineNumbers {5,9} public interface IHttpService : IHttpDeclarative { // Asynchronous request method [Get("https://furion.net/")] Task GetMethodAsync(); // Synchronous request method [Get("https://furion.net/")] string GetMethod(); } ``` > **Tip** Although synchronous methods are more intuitive to use, to maximize hardware resource utilization and improve application throughput, **we strongly recommend using asynchronous methods**. Asynchronous methods not only effectively avoid issues such as deadlocks and resource contention, but also make your application more efficient and responsive when handling `I/O`-intensive tasks. --- # 5.6 Defining Return Value Types > Source: https://http.furion.net/en/docs/declarative/defining-return-value-types/ In `HTTP` declarative request interface methods, in addition to supporting common `HTTP` response types such as `string`, `byte[]`, `Stream`, `HttpRequestMessage`, `HttpResponseMessage`, `void`, `IAsyncEnumerable`, and `IActionResult` as well as their asynchronous versions (`Task/Task/ValueTask/ValueTask`), custom types and the framework built-in `HttpRemoteResult`, `HttpRequestBuilder`, and `VoidContent` types and their asynchronous versions are also supported. ```cs showLineNumbers {3,9,15,21,27,38,44,50,54,60,66,70} public interface IHttpService : IHttpDeclarative { // String type [Get("https://furion.net/")] Task GetStringAsync(); [Get("https://furion.net/")] string GetString(); // Byte array type [Get("https://furion.net/")] Task GetBytesAsync(); [Get("https://furion.net/")] byte[] GetBytes(); // Stream type [Get("https://furion.net/")] Task GetStreamAsync(); [Get("https://furion.net/")] Stream GetStream(); // HttpResponseMessage type [Get("https://furion.net/")] Task GetHttpResponseMessageAsync(); [Get("https://furion.net/")] HttpResponseMessage GetHttpResponseMessage(); // No return value [Get("https://furion.net/")] Task GetVoidAsync(); [Get("https://furion.net/")] void GetVoid(); [Get("https://furion.net/")] Task GetVoidContentAsync(); [Get("https://furion.net/")] VoidContent GetVoidContent(); // Framework built-in HttpRemoteResult type [Get("https://furion.net/")] Task> GetHttpRemoteResultAsync(); [Get("https://furion.net/")] HttpRemoteResult GetHttpRemoteResult(); // IActionResult type [Get("https://furion.net/")] Task GetYourModelAsync(); [Get("https://furion.net/")] IActionResult GetYourModel(); // IAsyncEnumerable type [Get("https://furion.net/")] IAsyncEnumerable GetAsyncEnumerable(); // Custom type [Get("https://furion.net/")] Task GetYourModelAsync(); [Get("https://furion.net/")] YourModel GetYourModel(); // ValueTask/ValueTask types [Post("https://furion.net/user/add")] ValueTask PostDataAsync(object data); [Get("https://furion.net/")] ValueTask GetValueTaskAsync(); // HttpRequestBuilder type, does not send a request (preflight request) [Get("https://furion.net/")] Task GetRequestBuilderAsync(); // HttpRequestMessage type, does not send a request (preflight request) [Get("https://furion.net/")] Task GetRequestMessageAsync(); } ``` > **About the `VoidContent` Type** Since the `void` keyword cannot be used as a generic type argument, the system provides the `VoidContent` type to represent the case of no return value. For example, `GetAsAsync` means not receiving the response content. > **Notes on Return Value Types** By default, when the return value type is not `string`, `byte[]`, `Stream`, `HttpResponseMessage`, `void`, `VoidContent`, `IAsyncEnumerable`, `IActionResult`, or `HttpRemoteResult`, other types are deserialized using `System.Text.Json`. If you need to change this behavior, you can learn in later chapters how to implement the `IHttpContentConverter` content converter interface for customization. --- # 5.7 Setting Trace Identifier > Source: https://http.furion.net/en/docs/declarative/setting-trace-identifier/ Assigns a unique identifier to a request to facilitate tracking and debugging. This identifier is set in the `X-Trace-ID` request header. `HTTP` declarative requests set the trace identifier through the `TraceIdentifierAttribute` attribute. The corresponding `HTTP` declarative extractor is implemented as the [`TraceIdentifierDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/TraceIdentifierDeclarativeExtractor.cs) type, which is responsible for parsing the `TraceIdentifierAttribute` attribute and building the trace identifier configuration required for the `HttpRequestBuilder` instance. ```cs showLineNumbers {2,9} // Applied on the interface definition, affecting all methods [TraceIdentifier("your-id")] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStringAsync(); // Applied on the method [TraceIdentifier("your-method-id")] [Get("https://furion.net/")] Task GetStringAsync(); } ``` > **Scope of the `TraceIdentifierAttribute` Attribute** The `TraceIdentifierAttribute` attribute applies to methods or interfaces. `TraceIdentifierAttribute` includes the following constructors and properties: - **Constructors**: - `new(traceIdentifier)`: Applies to a method or interface, setting the trace identifier. - **Properties**: - `Identifier`: The trace identifier (`string` type). --- # 5.8 Setting Timeout > Source: https://http.furion.net/en/docs/declarative/setting-timeout/ Sets the timeout duration for a single request. `HTTP` Declarative Requests set the timeout through the `TimeoutAttribute` attribute. The corresponding `HTTP` declarative extractor is implemented by the [`TimeoutDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/TimeoutDeclarativeExtractor.cs) type, which is responsible for parsing the `TimeoutAttribute` attribute and building the timeout configuration required for the `HttpRequestBuilder` instance. ```cs showLineNumbers {2,9} // Applied on the interface definition, affects all methods [Timeout(100_000)] // 100 seconds public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStringAsync(); // Applied on the method [Timeout(200_000)] // 200 seconds [Get("https://furion.net/")] Task GetStringAsync(); } ``` > **`HttpClient` Timeout Notes** When setting a timeout on `HttpClient`, ensure that a single request's timeout does not exceed the timeout configured on `HttpClient`. For example, if the `HttpClient` timeout is set to `10` minutes while a single request's timeout is set to `15` minutes, the single request will still trigger a timeout exception once it exceeds `10` minutes. Sample code: ```cs showLineNumbers {1,3} services.AddHttpClient(string.Empty, client => { client.Timeout = TimeSpan.FromMinutes(10); // The default timeout is 100 seconds and must be set explicitly }); ``` Therefore, **if a single request needs a longer timeout, make sure `HttpClient`'s timeout is set correspondingly longer.** > **`TimeoutAttribute` Scope** `TimeoutAttribute` applies to methods or interfaces. `TimeoutAttribute` contains the following constructors and properties: - **Constructors**: - `new(milliseconds)`: applies to a method or interface, sets the timeout. - **Properties**: - `Timeout`: the timeout (in milliseconds) (`double` type). --- # 5.9 Configuring Retry Strategy > Source: https://http.furion.net/en/docs/declarative/configuring-retry-strategy/ Configures the retry strategy for a single request. By default, once a retry strategy is configured, the retry mechanism is triggered automatically when an unsuppressed exception occurs during the request. `HTTP` Declarative Requests configure the retry strategy through the `RetryAttribute` attribute. The corresponding `HTTP` declarative extractor is implemented by the [`RetryDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/RetryDeclarativeExtractor.cs) type, which is responsible for parsing the `RetryAttribute` attribute and building the retry strategy configuration required for the `HttpRequestBuilder` instance. ```cs showLineNumbers {2,9,13,17,21,25} // Applied on the interface definition, affects all methods [Retry(3)] // Max 3 retries public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStringAsync(); // Applied on the method [Retry(10)] // Max 10 retries [Get("https://furion.net/")] Task GetStringAsync(); [Retry(10, 1000)] // Configure the retry interval [Get("https://furion.net/")] Task GetStringAsync(); [Retry(10, RetryStatusCodes = [401])] // Retry on specific HTTP status codes [Get("https://furion.net/")] Task GetStringAsync(); [Retry(10, RetryExceptionTypes = [typeof(InvalidOperationException)])] // Retry on specific exception types [Get("https://furion.net/")] Task GetStringAsync(); [Retry(RetryIndefinitely = true)] // Set unlimited retries until success [Get("https://furion.net/")] Task GetStringAsync(); } ``` > **How the `HttpClient` Timeout Affects Retries** The total time spent retrying is constrained by the `HttpClient` timeout. For example, with a timeout of `3` seconds, a maximum of `4` retries, and an interval of `1` second each, if the retries still do not succeed within `3` seconds, subsequent retries will be canceled. Therefore, to ensure retries are not interrupted, configure the timeout appropriately in tandem; otherwise, the request may be terminated prematurely or block indefinitely. > **`RetryAttribute` Scope** `RetryAttribute` applies to methods or interfaces. `RetryAttribute` contains the following constructors and properties: - **Constructors**: - `new(maxRetries)`: applies to a method or interface, sets the maximum number of retries (`0` means no retries). - `new(maxRetries, retryInterval)`: applies to a method or interface, sets the maximum number of retries (`0` means no retries) and the base retry interval (in milliseconds). - **Properties**: - `MaxRetries`: the maximum number of retries (`int` type). The default value is `0`, meaning no retries. If `RetryIntervals` is set, this value is automatically overridden to the array length. - `RetryInterval`: the base retry interval (in milliseconds) (`double` type). The default value is `1000` milliseconds. Only takes effect when `RetryIntervals` is not set. - `UseExponentialBackoff`: whether to use exponential backoff for retries (`bool` type). The default value is `false`. When set to `true`, each retry interval = `RetryInterval * 2^(retry-1)`. Only takes effect when `RetryIntervals` is not set. - `RetryIntervals`: a custom array of retry intervals (in milliseconds) (`double[]?` type). If this property is set, the number of retries equals the array length, and `MaxRetries` and `UseExponentialBackoff` are ignored. Each retry uses the interval at the corresponding index in the array, in order. - `RetryStatusCodes`: the set of `HTTP` status codes to retry (`int[]?` type). If empty, only failures caused by exceptions are retried. - `RetryExceptionTypes`: the set of exception types to retry (`Type[]?` type). If empty, all `Exception` types are retried (subject to `MaxRetries`). - `RetryIndefinitely`: whether to retry indefinitely until success (`bool` type). The default value is `false`. When set to `true`, `MaxRetries` and the length of `RetryIntervals` are ignored, and retries continue until success or a non-retryable exception occurs. --- # 5.10 Setting Path Segments > Source: https://http.furion.net/en/docs/declarative/setting-path-segments/ Adds or removes `URL` path segments. `HTTP` Declarative Requests set or remove path segments through the `PathSegmentAttribute` attribute. The corresponding `HTTP` declarative extractor is implemented by the [`PathSegmentDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/PathSegmentDeclarativeExtractor.cs) type, which is responsible for parsing the `PathSegmentAttribute` attribute and building the path segment configuration required for the `HttpRequestBuilder` instance. **1. Adding Path Segments** Using the `PathSegmentAttribute` attribute, you can conveniently add path segments on an interface, method, or parameter. ```cs showLineNumbers {2-3,7-8,15,19,23} // Applied on the interface definition, affects all methods [PathSegment("segment1")] [PathSegment("segment2")] public interface IHttpService : IHttpDeclarative { // Applied on the method [PathSegment("segment3")] [PathSegment("segment4")] [Get("https://furion.net/")] Task GetStringAsync(); // Applied on a parameter, supports multiple specifications [PathSegment("segment3")] [Get("https://furion.net/")] Task GetStringAsync([PathSegment] string segment3, [QueryParam][QueryParam] int lastSegment); // On a parameter, a default value can be set via the Segment property; it can also be set for the segment parameter, e.g. string? segment = "default" [Get("https://furion.net/")] Task GetStringAsync([PathSegment(Segment = "default")] string? segment); // Frozen parameter types are ignored [Get("https://furion.net/")] Task GetStringAsync([PathSegment] CancellationToken cancellationToken); } ``` If duplicate path segments exist, they will appear repeatedly in subsequent appends (e.g., `/docs/docs/users/docs/`). **2. Removing Path Segments** In the `PathSegmentAttribute` attribute, **setting `Remove = true`** means removing that path segment. It is effective when applied on an interface, method, or parameter. ```cs showLineNumbers {2,7,9} [PathSegment("segment1")] // Adds the segment1 path segment [PathSegment("segment2", Remove = true)] // Marks segment2 as pending removal public interface IHttpService : IHttpDeclarative { [PathSegment("segment2")] // Adds the segment2 path segment [PathSegment("segment3")] // Adds the segment3 path segment [PathSegment("segment3", Remove = true)] // Marks segment3 as pending removal [Get("https://furion.net/")] Task GetStringAsync([PathSegment(Remove = true)] string seg); // Dynamically marks seg as pending removal based on its value } ``` Before sending the `HTTP` request, the set of path segments marked for removal specified in the configuration will be removed. In other words, the removal operation is performed after all setting operations have been invoked. In the example above, although the `GetStringAsync` method attempts to add the `segment2` and `segment3` path segments via the `[PathSegment]` attribute, because the subsequent `[PathSegment("segment2", Remove = true)]` and `[PathSegment("segment3", Remove = true)]` attributes specify only the `Remove = true` property, these two keys are removed when the request `URL` is finally constructed. Only the `segment1` path segment remains in the request `URL`. `PathSegmentAttribute` contains the following constructors and properties: - **Constructors**: - `new()`: effective when applied to a parameter, adds a path segment whose value is the parameter value. - `new(segment)`: when applied to a method or interface, adds the specified path segment; when applied to a parameter whose value is `null`, adds a path segment whose value is the `segment` parameter. - **Properties**: - `Segment`: the path segment (`string` type). When the attribute is applied to a parameter whose value is `null`, it can be used as the default value. - `Remove`: whether to mark it as pending deletion (`bool` type). The default value is `false` (append). > **Frozen Parameter Types** In the system, `Action`, `Action`, `HttpCompletionOption`, and `CancellationToken` are treated as frozen parameter types that specifically serve particular operation executions. Therefore, `PathSegmentAttribute` is ignored when applied to these parameter types. > **Tip** `C#` supports attribute merging to make code more concise: ```cs showLineNumbers {1,4} [PathSegment("segment1"), PathSegment("segment2")] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/"), PathSegment("segment3"), PathSegment("segment4")] Task GetStringAsync(); } ``` --- # 5.11 Setting Query Parameters (URL Parameters) > Source: https://http.furion.net/en/docs/declarative/setting-query-parameters-url-parameters/ Add, modify, or remove `URL` query parameters. `HTTP` Declarative Requests set or remove query parameters via the `QueryParamAttribute` attribute. The corresponding `HTTP` declarative extractor is implemented as the [`QueryParamDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/QueryParamDeclarativeExtractor.cs) type, which is responsible for parsing the `QueryParamAttribute` attribute and building the query parameter configuration required by an `HttpRequestBuilder` instance. **1. Adding Query Parameters** Using the `QueryParamAttribute` attribute, you can conveniently add query parameters on an interface, method, or parameter. ```cs showLineNumbers {2-3,7-8,15,19,23,27,31,35,39} // Applied on the interface definition, affecting all methods [QueryParam("query1", "value1")] [QueryParam("query2", "value2")] public interface IHttpService : IHttpDeclarative { // Applied on the method [QueryParam("query3", "value3")] [QueryParam("query4", "value4")] [Get("https://furion.net/")] Task GetStringAsync(); // Applied on the parameter, supports the AliasAs property to specify an alias, and can be specified multiple times [QueryParam("query3", "value3")] [Get("https://furion.net/")] Task GetStringAsync([QueryParam] string query4, [QueryParam][QueryParam(AliasAs = "query5")] int lastQuery); // On the parameter, a default value can be set via the Value property, and the same can be set for the age parameter, e.g. int? age = 30 [Get("https://furion.net/")] Task GetStringAsync([QueryParam(Value = 30)] int? age); // Supports using an object as a query parameter and specifying a prefix [Get("https://furion.net/")] Task GetStringAsync([QueryParam(Prefix = "user")] object obj); // Supports defining an alias via [AliasAs] [Get("https://furion.net/")] Task GetStringAsync([QueryParam][AliasAs("query5")] int lastQuery); // Supports ignoring null-valued parameters; if the value of str1 is null, it is ignored [Get("https://furion.net/")] Task GetStringAsync([QueryParam(IgnoreNullValues = true)] string? str1, [QueryParam] string? str2); // Supports format formatting [Get("https://furion.net/")] Task GetStringAsync([QueryParam(Format = "yyyyMMdd")] DateTime date); // Frozen parameter types are ignored [Get("https://furion.net/")] Task GetStringAsync([QueryParam] CancellationToken cancellationToken); } ``` If duplicate query parameter keys exist, they are merged into multiple key-value pairs (e.g. `key1=value1&key1=value2`). By setting the `Replace = true` property, you can override the previous query parameters and the parameters from the original `URL` address. **By default, query parameters with a `null` value are added to the URL; to ignore these parameters, set `IgnoreNullValues = true`.** **2. Removing Query Parameters** In the `QueryParamAttribute` attribute, **specifying only the query parameter key without a value** means removing that parameter. This is effective when applied on an interface or method. ```cs showLineNumbers {2,7} [QueryParam("query1", "value1")] // Add the query1 parameter [QueryParam("query2")] // Mark query2 as to be removed public interface IHttpService : IHttpDeclarative { [QueryParam("query2", "value2")] // Add the query2 parameter [QueryParam("query3", "value3")] // Add the query3 parameter [QueryParam("query3")] // Mark query3 as to be removed [Get("https://furion.net/")] Task GetStringAsync(); } ``` Before sending the `HTTP` request, the set of query parameters marked for removal specified in the configuration will be removed. In other words, the removal operation is executed after all setting operations are called. In the example above, although the `GetStringAsync` method attempts to add the `query2` and `query3` parameters via the `[QueryParam]` attribute, the subsequent `[QueryParam("query2")]` and `[QueryParam("query3")]` attributes specify only the query parameter key without a value, so these two keys are removed when the request `URL` is finally built. Only the `query1` parameter is retained in the request `URL`. **3. `URL` Parameter Formatter** When setting query parameters for an `HTTP` request, the framework passes the parameter keys and values to `IUrlParameterFormatter` for formatting. The default implementation `UrlParameterFormatter` generates a `key=value` key-value pair for each value. However, certain types (such as `DateTime`) may require special handling, or you may want to change the output shape of the entire key-value pair (for example, outputting multiple values as an array format like `key[0]=val1&key[1]=val2`); in such cases you can implement a custom formatter. The following example shows how to override the `Format` method to format `DateTime` values as `yyyyMMdd`, while using the default handling for other types: ```csharp showLineNumbers {1,4,6-15} public class CustomUrlParameterFormatter : UrlParameterFormatter { /// public override IEnumerable>? Format(UrlFormattingContext context, string key, IEnumerable values) { foreach (var value in values) { if (value is DateTime dateTime) { yield return new(key, dateTime.ToString("yyyyMMdd")); // Format continue; } yield return new(key, FormatValue(context, value)); } } } ``` After completing the custom formatter, you can register it as the default `URL` parameter formatter when configuring `HttpRemoteOptions`: ```csharp showLineNumbers {2,4} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { options.UrlParameterFormatter = new CustomUrlParameterFormatter(); }); ``` In this way, when building `URL` query parameters, if a `DateTime` value is encountered, the framework automatically formats it as a `yyyyMMdd` string, thereby ensuring the output meets expectations. **4. `URL` Parameter Sorting** Although the need to sort `URL` query parameters is relatively rare, in some systems with higher security requirements it is often necessary to verify the order of parameters. The framework provides sorting support for this purpose, and the sorting target is the final collection of key-value pairs: ```cs showLineNumbers {3} HttpRequestBuilder.Get("https://furion.net/") .WithQueryParameters(new { name = "furion", id = 1}) .SetQueryParametersSorter(pairs => pairs.OrderBy(kv => kv.Key)); ``` Configure the query parameter sorting rule via the `.SetQueryParametersSorter()` method. This method receives a sequence of `KeyValuePair` and returns a new sorted sequence. When it is `null`, no sorting is performed (the original insertion order is preserved). `QueryParamAttribute` contains the following constructors and properties: - **Constructors**: - `new()`: Effective when applied to a parameter, indicates adding a query parameter, with the default key being the parameter name. - `new(name)`: When applied to a method or interface, it indicates removing the specified query parameter; when applied to a parameter, it indicates adding a query parameter with the key being the value of the `name` argument. - `new(name, value)`: Applies to interfaces, methods, or parameters, indicating adding a query parameter with the key being the value of the `name` argument, with lower priority than the `AliasAs` property. - **Properties**: - `Name`: The query parameter key (`string` type), with lower priority than the `AliasAs` property. - `Value`: The query parameter value (`object` type); when the attribute is applied to a parameter, it indicates the default value. - `AliasAs`: The query parameter key alias (`string` type), with higher priority than the `Name` property. - `Prefix`: The query parameter prefix (`string` type), effective only for object parameters. - `Replace`: Whether to replace existing query parameters (`bool` type); the default value is `false` (append). - `IgnoreNullValues`: Whether to ignore query parameters with a null (`null`) value (`bool` type); the default value is `false` (do not ignore). - `Format`: The format to use (`string?` type), effective only when `Value` implements `IFormattable`. > **Frozen Parameter Types** In the system, `Action`, `Action`, `HttpCompletionOption`, and `CancellationToken` are considered frozen parameter types; they are dedicated to specific operation execution. Therefore, the `QueryParamAttribute` attribute is ignored when applied to these parameter types. > **Tip** `C#` supports attribute merging, making the code more concise: ```cs showLineNumbers {1,4} [QueryParam("query1", "value1"), Query("query2", "value2")] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/"), Query("query3", "value3"), Query("query4", "value4")] Task GetStringAsync(); } ``` --- # 5.12 Setting Request Headers > Source: https://http.furion.net/en/docs/declarative/setting-request-headers/ Add, modify, or remove request headers. `HTTP` Declarative Requests set or remove request headers via the `HeaderAttribute` attribute. The corresponding `HTTP` declarative extractor is implemented as the [`HeaderDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/HeaderDeclarativeExtractor.cs) type, which is responsible for parsing the `HeaderAttribute` attribute and building the request header configuration required by an `HttpRequestBuilder` instance. **1. Adding Request Headers** Using the `HeaderAttribute` attribute, you can conveniently add request headers on an interface, method, or parameter. ```cs showLineNumbers {2-3,7-8,15,19,23,27,32,36} // Applied on the interface definition, affecting all methods [Header("header1", "value1")] [Header("header2", "value2")] public interface IHttpService : IHttpDeclarative { // Applied on the method [Header("header3", "value3")] [Header("header4", "value4")] [Get("https://furion.net/")] Task GetStringAsync(); // Applied on the parameter, supports the AliasAs property to specify an alias, and can be specified multiple times [Header("header3", "value3")] [Get("https://furion.net/")] Task GetStringAsync([Header] string header4, [Header][Header(AliasAs = "header5")] int lastHeader); // On the parameter, a default value can be set via the Value property, and the same can be set for the age parameter, e.g. int? age = 30 [Get("https://furion.net/")] Task GetStringAsync([Header(Value = 30)] int? age); // Supports defining an alias via [AliasAs] [Get("https://furion.net/")] Task GetStringAsync([Header][AliasAs("header5")] int lastHeader); // Supports configuration using a colon (:) [Get("https://furion.net/")] [Header("User-Agent: HttpAgent")] Task GetStringAsync(); // Supports format formatting [Get("https://furion.net/")] Task GetStringAsync([Header(Format = "yyyyMMdd")] DateTime date); // Frozen parameter types are ignored [Get("https://furion.net/")] Task GetStringAsync([Header] CancellationToken cancellationToken); } ``` If duplicate request headers exist, they are merged, with multiple values separated by a comma followed by a space (`, `). By setting the `Replace = true` property, you can override previous request header settings. **2. Removing Request Headers** In the `HeaderAttribute` attribute, **specifying only the request header key without a value** means removing that header. This is effective when applied on an interface or method. ```cs showLineNumbers {2,7} [Header("header1", "value1")] // Add the header1 header [Header("header2")] // Mark header2 as to be removed public interface IHttpService : IHttpDeclarative { [Header("header2", "value2")] // Add the header2 header [Header("header3", "value3")] // Add the header3 header [Header("header3")] // Mark header3 as to be removed [Get("https://furion.net/")] Task GetStringAsync(); } ``` Before sending the `HTTP` request, the set of request headers marked for removal specified in the configuration will be removed. In other words, the removal operation is executed after all setting operations are called. In the example above, although the `GetStringAsync` method attempts to add the `header2` and `header3` headers via the `[Header]` attribute, the subsequent `[Header("header2")]` and `[Header("header3")]` attributes specify only the request header key without a value, so these two keys are removed when the request headers are finally built. Only the `header1` header is retained in the request headers. `HeaderAttribute` contains the following constructors and properties: - **Constructors**: - `new()`: Effective when applied to a parameter, indicates adding a request header, with the default key being the parameter name. - `new(name)`: When applied to a method or interface, if the configured string does not contain a colon (`:`), it indicates removing the specified request header; if it contains a colon, the first colon is used as the separator, with the key on the left and the value on the right. When applied to a parameter, it indicates adding a request header with the key being the value of the `name` argument. - `new(name, value)`: Applies to interfaces, methods, or parameters, indicating adding a request header with the key being the value of the `name` argument, with lower priority than the `AliasAs` property. - **Properties**: - `Name`: The request header key (`string` type), with lower priority than the `AliasAs` property. - `Value`: The request header value (`object` type); when the attribute is applied to a parameter, it indicates the default value. - `AliasAs`: The request header key alias (`string` type), with higher priority than the `Name` property. - `Escape`: Whether to escape the request header value (`bool` type); the default value is `false` (do not escape). - `Replace`: Whether to replace existing request headers (`bool` type); the default value is `false` (append). - `Format`: The format to use (`string?` type), effective only when `Value` implements `IFormattable`. > **Frozen Parameter Types** In the system, `Action`, `Action`, `HttpCompletionOption`, and `CancellationToken` are considered frozen parameter types; they are dedicated to specific operation execution. Therefore, the `HeaderAttribute` attribute is ignored when applied to these parameter types. > **Tip** `C#` supports attribute merging, making the code more concise: ```cs showLineNumbers {1,4} [Header("header1", "value1"), Header("header2", "value2")] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/"), Header("header3", "value3"), Header("header4", "value4")] Task GetStringAsync(); } ``` > **Configuration Parameter Support** Request headers support configuration parameters, which are used to read configuration information for replacement operations. Configuration parameters use the `[[key]]` syntax. --- # 5.13 Setting Path Parameters (Template/Configuration Parameters) > Source: https://http.furion.net/en/docs/declarative/setting-path-parameters-templateconfiguration-parameters/ Replaces object template strings in the `URL` path. `HTTP` Declarative Requests configure path parameters through the `PathAttribute` attribute and the non-frozen parameters defined on methods. The corresponding `HTTP` declarative extractor is the [`PathDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/PathDeclarativeExtractor.cs) type, which is responsible for parsing these `PathAttribute` attributes and the non-frozen parameters defined on methods, and building the path parameter configuration required by the `HttpRequestBuilder` instance. ```cs showLineNumbers {2-3,7-8,12,15,18,23} // Applied on the interface definition, affecting all methods [Path("path1", "value1")] [Path("path2", "value2")] public interface IHttpService : IHttpDeclarative { // Applied on the method [Path("path3", "value3")] [Get("https://furion.net/{path1}/{path2}/{path3}")] Task GetStringAsync(); // Non-frozen parameters defined on the method are added to the path parameters by default and can be used directly in the URL [Get("https://furion.net/{path1}/{path2}/?id={id}&name={name}&address={address}&age={age}&name1={user.Name}&obj={obj}")] Task GetStringAsync(int id, string name, string[] address, int age, User user, object? obj); [Get("https://furion.net/{name?}")] // A trailing "?" means the value is replaced with an empty string when the key does not exist; can be combined with the [RemoveTrailingSlash] attribute Task GetStringAsync(string name); [Get("https://furion.net/{**path}")] // A leading "**" means the path separator "/" is not escaped Task GetStringAsync(string path); // Frozen parameter types are ignored [Get("https://furion.net/")] Task GetStringAsync(CancellationToken cancellationToken); } ``` If duplicate path parameter keys exist, the later-set key value overrides the earlier setting. **Template path syntax** In addition to directly using `{key}`, template paths also support accessing object properties and nested properties via `.`, and accessing collection elements via `[index]`. Additionally, when an object-typed value has no property matching the given name, the framework automatically attempts to treat it as a dictionary and retrieves the value using the path identifier as the key (equivalent to `dict["key"]`). - `{key}`: Directly replaces the corresponding value. - `{key.property}`: Accesses the `property` property of the `key` object, or, when `key` is a dictionary, accesses the value whose key is `"property"`. - `{key.property.nested}`: Multi-level property/key access. - `{list[0]}`: Accesses the element at index `0` in the `list` collection (arrays, `List`, etc.). - `{user.names[1]}`: First accesses the `names` property of the `user` object, then takes the element at index `1`. - `{dic.key}`: When `dic` is a dictionary (including `Dictionary`, `Hashtable`, etc.), `dic.key` is evaluated as `dic["key"]`. - `{obj.dictProp.someKey[0].another}`: Mixes dot and index access to drill down level by level. > **`JSON` nested access within dictionary values** When using a dictionary (`IDictionary`) as the data source, if the value of a key is itself a valid `JSON` string (such as an object or array), the framework automatically parses that `JSON` and continues to access the inner data via `.` and `[index]`. For example: if the dictionary contains `["user"] = "{\"name\":\"Monk\",\"tags\":[\"A\",\"B\"]}"`, then `{user.name}` is replaced with `Monk` and `{user.tags[0]}` is replaced with `A`. In this way, you only need to serialize complex objects into `JSON` and store them in the dictionary to perform deep value extraction with a unified placeholder syntax, greatly simplifying template concatenation logic. All the paths above support appending `?` at the end to indicate that the value is replaced with an empty string when it does not exist, and adding a `**` prefix to indicate that the path separator `/` is not escaped. > **Scope of the `PathAttribute` attribute** The `PathAttribute` attribute applies only to methods or interfaces, not to parameters. Because **non-frozen parameters defined on the method are added to the path parameters by default**, no manual marking is required. > **Notes on frozen parameter types** In the system, `Action`, `Action`, `HttpCompletionOption`, and `CancellationToken` are treated as frozen parameter types, dedicated to serving specific operation execution. Therefore, these parameter types are ignored as path parameters. `PathAttribute` includes the following constructors and properties: - **Constructors**: - `new(name, value)`: Applies to interfaces or methods, indicating the addition of a path parameter with the value of the parameter `name` as the key. - **Properties**: - `Name`: The path parameter key (`string` type). - `Value`: The path parameter value (`object` type). > **Tip** `C#` supports attribute combination to make the code more concise: ```cs showLineNumbers {1,4} [Path("path1", "value1"), Path("path2", "value2")] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/{path1}/{path2}/{path3}"), Path("path3", "value3")] Task GetStringAsync(); } ``` --- **Configuration parameters** In addition to setting path parameters through the `{key}` template syntax, the framework also provides configuration parameters for reading configuration information to perform replacements. Configuration parameters use the `[[key]]` syntax, for example: ```cs showLineNumbers {3} public interface IHttpService : IHttpDeclarative { [Get("https://furion.net?id=[[id]]&name=[[name]]")] Task GetStringAsync(); } ``` **Enabling configuration parameter support** To enable configuration parameter support in the `HttpRemote` service, configure it with the following steps: ```cs showLineNumbers {2,5} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { // Set the provider source used to replace configuration template parameters in the URL options.Configuration = builder.Configuration; // When using the Furion framework, you can set App.Configuration directly }); ``` **Using configuration parameters** Configuration parameters are read from your configuration file and replaced into the `URL`. For example, your configuration file might look like this: ```json showLineNumbers title="appsettings.json" { "id": 1, "name": "Furion" } ``` Configuration parameter keys support various format syntaxes to access values in the configuration file more flexibly: - `[[key]]`: Directly accesses the value corresponding to `key`. - `[[key:sub]]`: Accesses the value of the `sub` sub-item under `key`. - `[[key:sub:nest]]`: Accesses the value of the `nest` sub-item within the `sub` sub-item under `key`. - Fallback value lookup: - `[[notfound | bak]]`: If `notfound` does not exist, looks up `bak`. - `[[notfound | bak | other]]`: If neither `notfound` nor `bak` exists, looks up `other`. - `[[notfound | bak:sub | other:sub:nest]]`: Supports deeper fallback lookups. - Default values: - `[[notfound || default]]`: If `notfound` does not exist, uses `default` as the value. - `[[notfound | bak | other || default]]`: Combines fallback lookup and default value to ensure a value is always available. --- # 5.14 Setting Cookie > Source: https://http.furion.net/en/docs/declarative/setting-cookie/ Adds, modifies, or removes a `Cookie`. `HTTP` Declarative Requests set or remove a `Cookie` via the `CookieAttribute` attribute. The corresponding `HTTP` declarative extractor is implemented as the [`CookieDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/CookieDeclarativeExtractor.cs) type, which is responsible for parsing the `CookieAttribute` attribute and building the `Cookie` configuration required by the `HttpRequestBuilder` instance. **1. Adding a `Cookie`** Using the `CookieAttribute` attribute, you can conveniently add a `Cookie` on an interface, method, or parameter. ```cs showLineNumbers {2-3,7-8,15,19,23,27,31} // Applied on the interface definition, affecting all methods [Cookie("cookie1", "value1")] [Cookie("cookie2", "value2")] public interface IHttpService : IHttpDeclarative { // Applied on the method [Cookie("cookie3", "value3")] [Cookie("cookie4", "value4")] [Get("https://furion.net/")] Task GetStringAsync(); // Applied on the parameter; supports the AliasAs property to specify an alias, and can be specified multiple times [Cookie("cookie3", "value3")] [Get("https://furion.net/")] Task GetStringAsync([Cookie] string cookie4, [Cookie][Cookie(AliasAs = "cookie5")] int lastCookie); // On parameters, a default value can be set via the Value property; the same applies to the age parameter, e.g. int? age = 30 [Get("https://furion.net/")] Task GetStringAsync([Cookie(Value = 30)] int? age); // Supports [AliasAs] to define an alias [Get("https://furion.net/")] Task GetStringAsync([Cookie][AliasAs("cookie5")] int lastCookie); // Supports formatting via format [Get("https://furion.net/")] Task GetStringAsync([Cookie(Format = "yyyyMMdd")] DateTime date); // Frozen parameter types are ignored [Get("https://furion.net/")] Task GetStringAsync([Cookie] CancellationToken cancellationToken); } ``` If duplicate `Cookie` keys exist, the later-set key value overrides the earlier setting. **2. Removing a `Cookie`** In the `CookieAttribute` attribute, **specifying only the `Cookie` key without assigning a value** indicates removing that `Cookie`. It is effective when applied to interfaces or methods. ```cs showLineNumbers {2,7} [Cookie("cookie1", "value1")] // Add cookie1 [Cookie("cookie2")] // Mark cookie2 for removal public interface IHttpService : IHttpDeclarative { [Cookie("cookie2", "value2")] // Add cookie2 [Cookie("cookie3", "value3")] // Add cookie3 [Cookie("cookie3")] // Mark cookie3 for removal [Get("https://furion.net/")] Task GetStringAsync(); } ``` Before sending the `HTTP` request, the set of `Cookie`s marked for removal specified in the configuration will be removed. In other words, the removal operation is performed after all setting operations are invoked. In the example above, although the `GetStringAsync` method tries to add `cookie2` and `cookie3` via the `[Cookie]` attribute, because the subsequent `[Cookie("cookie2")]` and `[Cookie("cookie3")]` attributes only specify the `Cookie` key without assigning a value, these two keys are removed when the request header `Cookie` is finally built. Only the `cookie1` parameter remains in the request header `Cookie`. `CookieAttribute` includes the following constructors and properties: - **Constructors**: - `new()`: Effective on parameters, indicating the addition of a `Cookie` with the parameter name as the default key. - `new(name)`: When applied to a method or interface, indicates removing the specified `Cookie`; when applied to a parameter, indicates adding a `Cookie` with the value of the parameter `name` as the key. - `new(name, value)`: Applies to interfaces, methods, or parameters, indicating the addition of a `Cookie` with the value of the parameter `name` as the key; has lower priority than the `AliasAs` property. - **Properties**: - `Name`: The `Cookie` key (`string` type), with lower priority than the `AliasAs` property. - `Value`: The `Cookie` value (`object` type); when the attribute applies to a parameter, it represents the default value. - `AliasAs`: The `Cookie` key alias (`string` type), with higher priority than the `Name` property. - `Format`: The format to use (`string?` type), effective only when `Value` implements `IFormattable`. > **Notes on frozen parameter types** In the system, `Action`, `Action`, `HttpCompletionOption`, and `CancellationToken` are treated as frozen parameter types, dedicated to serving specific operation execution. Therefore, the `CookieAttribute` attribute is ignored when applied to these parameter types. > **Tip** `C#` supports attribute combination to make the code more concise: ```cs showLineNumbers {1,4} [Cookie("cookie1", "value1"), Cookie("cookie2", "value2")] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/"), Cookie("cookie3", "value3"), Cookie("cookie4", "value4")] Task GetStringAsync(); } ``` > **Configuration parameter support** `Cookie` values support configuration parameters for reading configuration information to perform replacements. Configuration parameters use the `[[key]]` syntax. --- # 5.15 Setting the HttpClient Instance Name (Multiple Base Addresses) > Source: https://http.furion.net/en/docs/declarative/setting-the-httpclient-instance-name-multiple-base-addresses/ The system uses `IHttpClientFactory` to create `HttpClient` instances by default, and sets the default client name to an empty string (`string.Empty`). You can set the client name used when creating the `HttpClient` instance via the `HttpClientNameAttribute` attribute. `HTTP` Declarative Requests set the `HttpClient` instance name via the `HttpClientNameAttribute` attribute. The corresponding `HTTP` declarative extractor is implemented as the [`HttpClientNameDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/HttpClientNameDeclarativeExtractor.cs) type, which is responsible for parsing the `HttpClientNameAttribute` attribute and building the `HttpClient` instance name configuration required by the `HttpRequestBuilder` instance. ```cs showLineNumbers {2,9} // Applied on the interface definition, affecting all methods [HttpClientName(string.Empty)] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStringAsync(); // Default client // Applied on the method [HttpClientName("weixin")] // Specified as the client named "weixin" [Get("https://furion.net/")] Task GetStringAsync(); } ``` You can also provide configuration for named `HttpClient` clients in the `Startup.cs` or `Program.cs` file: ```cs showLineNumbers {2,5} // Configure the default client (with an empty string name) services.AddHttpClient(string.Empty, client => { }); // Configure the client named "weixin" services.AddHttpClient("weixin", client => { }); ``` > **Scope of the `HttpClientNameAttribute` attribute** The `HttpClientNameAttribute` attribute applies to methods or interfaces. `HttpClientNameAttribute` includes the following constructors and properties: - **Constructors**: - `new(name)`: Applies to methods or interfaces, setting the `HttpClient` instance name. - **Properties**: - `Name`: The `HttpClient` instance name (`string` type). --- # 5.16 Setting Request Content (Body) > Source: https://http.furion.net/en/docs/declarative/setting-request-content-body/ Supports setting any type of request content. `HTTP` declarative requests configure request content via the `BodyAttribute` attribute. The corresponding `HTTP` declarative extractor is the [`BodyDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/BodyDeclarativeExtractor.cs) type, which is responsible for parsing a **single** `BodyAttribute` attribute and building the request content configuration required by the `HttpRequestBuilder` instance. ```cs showLineNumbers {5,9,13,17,21,25,29,32,36,40} public interface IHttpService : IHttpDeclarative { // Marks the parameter as the request content [Post("https://furion.net/")] Task PostStringAsync([Body] object body); // Automatically infers Content-Type // Supports setting Content-Type [Post("https://furion.net/")] Task PostStringAsync([Body("application/json")] object body); // or use [Body(MediaTypeNames.Application.Json)] // Supports setting Content-Type and character set [Post("https://furion.net/")] Task PostStringAsync([Body("application/json; charset=utf-8")] object body); // URL-encoded form [Post("https://furion.net/")] Task PostStringAsync([Body("application/x-www-form-urlencoded")] object body); // Uses StringContent to build the URL-encoded form [Post("https://furion.net/")] Task PostStringAsync([Body("application/x-www-form-urlencoded", UseStringContent = true)] object body); // Can be configured to skip URL encoding [Post("https://furion.net/")] Task PostStringAsync([Body("application/x-www-form-urlencoded", urlEncode = false)] object body); // Supports raw string content [Post("https://furion.net/")] Task PostStringAsync([Body(RawString = true)] string body); // Default content type text/plain [Post("https://furion.net/")] Task PostStringAsync([Body("application/json", RawString = true)] string body); // Supports configuring a file path (or internet address) [Post("https://furion.net/")] Task PostStringAsync([Body(AsFile = true)] string filePath); // Frozen parameter types will be ignored [Post("https://furion.net/")] Task PostStringAsync([Body] CancellationToken cancellationToken); } ``` > **Multiple `Body` Parameter Declaration Attributes** To conveniently annotate request content parameters quickly, the framework provides various commonly used `Body` attributes, such as `[JsonBody]`, `[HtmlBody]`, `[FormUrlEncodedBody]`, `[RawStringBody]`, `[TextBody]`, and `[XmlBody]`. > **Default Behavior When `Content-Type` Is Not Provided** When `Content-Type` is not specified, the framework determines `Content-Type` according to the following priority: 1. **Request content headers**: if `Content-Type` has been set via `WithHeader` or similar, it takes priority. 2. **Automatic content type inference**: if no content header is set, the framework automatically infers it based on the specific type of `RawContent` according to the following rules: - **`JsonContent`**: `application/json` - **`JsonNode` or `JsonElement`** - If it represents a `JSON` object or array, then `application/json` - Otherwise (e.g., a `JSON` scalar value), it falls back to `text/plain` - **`FormUrlEncodedContent`**: `application/x-www-form-urlencoded` - **`StringContent`**: `text/plain` - **`MultipartFormDataContent`**: `multipart/form-data` - **`MultipartContent`** (non-`FormData` subclasses): `multipart/mixed` - **`ByteArrayContent`, `StreamContent`, `ReadOnlyMemoryContent`**: `application/octet-stream` - **`byte[]`, `Stream`, `ReadOnlyMemory`**: `application/octet-stream` - **Other custom `HttpContent` subclasses** (no header set and none of the specific types above matched): `application/octet-stream` - **`MultipartFile`**: `application/octet-stream` - **`FileInfo`**: inferred from the file extension via `FileTypeMapper`, defaulting to `application/octet-stream` if it cannot be identified - **Other complex objects** (non-primitive, non-enum, non-collection): `application/json` (assumed to be serialized to `JSON`) 3. **Global default fallback value**: if none of the above rules match, the value configured in `HttpClientOptions.DefaultContentType` is used. This value defaults to `text/plain`; to change this fallback value, configure it with the following code: ```cs showLineNumbers {2,5} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { // Sets the default request content type (final fallback value) options.DefaultContentType = "application/json"; }); ``` Parameters marked with `BodyAttribute` are set through the underlying `httpRequestBuilder.SetContent` method, and any non-frozen parameter type is supported. > **Scope of the `BodyAttribute` Attribute** The `BodyAttribute` attribute only applies to parameters. > **Notes on Frozen Parameter Types** In the system, `Action`, `Action`, `HttpCompletionOption`, and `CancellationToken` are treated as frozen parameter types, which are reserved for specific operation execution. Therefore, the `BodyAttribute` attribute is ignored when applied to these parameter types. > **Multiple Parameters Marked with the `BodyAttribute` Attribute** Because request content can contain only one value, if a method has multiple parameters marked with the `BodyAttribute` attribute, an `InvalidOperationException` is thrown. The exception message is: `The input sequence contains more than one element.`. > **Notes on `URL`-Encoded Form Content** - **By default, `URL`-encoded forms are built via the [`FormUrlEncodedContent`](https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Net.Http/src/System/Net/Http/FormUrlEncodedContent.cs#L44) type, but this type does not support custom request content encoding; it uses `Encoding.Latin1` rather than `UTF-8` by default.** This may cause exceptions when submitting to certain endpoints. To resolve this issue, set the `UseStringContent` property to `true` to build the form data using `StringContent`, thereby allowing a custom encoding of `UTF-8`. ```cs showLineNumbers {4} public interface IHttpService : IHttpDeclarative { [Post("https://furion.net/")] Task PostStringAsync([Body("application/x-www-form-urlencoded", UseStringContent = true)] object body); // body also supports URL-encoded strings, e.g.: id=1&name=furion } ``` - Some servers require the character set (`charset`) to be declared explicitly, in which case the encoding can be specified via the `contentEncoding` parameter, for example using `UTF-8`: ```cs showLineNumbers {3} public interface IHttpService : IHttpDeclarative { [Post("https://furion.net/")] Task PostStringAsync([Body("application/x-www-form-urlencoded", "UTF-8")] object body); // body also supports URL-encoded strings, e.g.: id=1&name=furion } ``` When sending a remote request, this setting generates the following `Content-Type` request header: `application/x-www-form-urlencoded; charset=UTF-8`. `BodyAttribute` contains the following constructors and properties: - **Constructors**: - `new()`: applies to a parameter, treating the parameter as the request content. - `new(contentType)`: applies to a parameter, treating the parameter as the request content, and supports setting the content type. - `new(contentType, contentEncoding)`: applies to a parameter, treating the parameter as the request content, and supports setting the content type and encoding. - **Properties**: - `ContentType`: the content type (of type `string`). - `ContentEncoding`: the content encoding (of type `string`). - `UseStringContent`: whether to use `StringContent` to build [`FormUrlEncodedContent`](https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Net.Http/src/System/Net/Http/FormUrlEncodedContent.cs#L44), defaulting to `false`, and only effective when `ContentType` is `application/x-www-form-urlencoded`. - `UrlEncode`: whether to `URL`-encode the form data (of type `bool`), defaulting to `true`. - `RawString`: whether the content is a raw string (of type `bool`), defaulting to `false`, and only effective when the parameter is a string type and this property is `true`. - `AsFile`: treats the string as a file path (internet addresses supported) (of type `bool`), defaulting to `false`, and only effective when the parameter is a string type and this property is `true`. - `DisposeResourcesOnRequestCompletion`: whether to automatically release resources after the request completes (of type `bool`), defaulting to `false`. --- # 5.17 Setting Multipart Form Content (Complex Forms / File Upload) > Source: https://http.furion.net/en/docs/declarative/multipart/ Sets the request content type to `multipart/form-data` and sends multipart form content. `HTTP` declarative requests set multipart form content via the `MultipartAttribute` attribute. The corresponding `HTTP` declarative extractor is implemented as the [`MultipartDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/MultipartDeclarativeExtractor.cs) type, which is responsible for parsing the `MultipartAttribute` and `MultipartFormAttribute` attributes and building the multipart form content configuration required by the `HttpRequestBuilder` instance. ```cs showLineNumbers {6-12,17-20,24,27,33,38} public interface IHttpService : IHttpDeclarative { // Adds common form item content [Post("https://furion.net/")] Task PostStringAsync( [Multipart] int id, [Multipart] string name, [Multipart] object obj, [Multipart] Stream stream, [Multipart("bytes")] byte[] byteArray, // custom form name; the file name can also be specified via the FileName property [Multipart] StringContent content [Multipart] MultipartFile file); // Adds file content [Post("https://furion.net/")] Task PostStringAsync( [Multipart(AsFileFrom = FileSourceType.None)] string none, // does nothing [Multipart("files", AsFileFrom = FileSourceType.Path, ContentType = "image/jpeg")] string filePath, // adds from a local file path; if Content-Type is not provided, it is resolved automatically from the file extension [Multipart("files", AsFileFrom = FileSourceType.Base64String)] string base64String, // adds from a Base64 string file; if Content-Type is not provided, it is resolved automatically from the file extension [Multipart("files", AsFileFrom = FileSourceType.Remote)] string remote); // adds from an internet file address; if Content-Type is not provided, it is resolved automatically from the file extension // Adds object content; when AsFormItem is false, the object's properties are parsed and iterated, and its properties are set as independent form items [Post("https://furion.net/")] Task PostStringAsync([Multipart(AsFormItem = false)] object obj); // [MultipartObject] is recommended // Sets the boundary of the multipart form content [MultipartForm("--------------------")] [Post("https://furion.net/")] Task PostStringAsync([Multipart] int id); // Sets the form name naming policy (converter) [Post("https://furion.net/")] [MultipartForm(NamingPolicy = FormNamingPolicy.CamelCase)] Task PostStringAsync([MultipartObject] object obj); // Frozen parameter types will be ignored [Post("https://furion.net/")] Task PostStringAsync([Multipart] CancellationToken cancellationToken); } ``` ### Complex Forms Containing Files (or Binary Data) When handling complex forms that contain basic data along with files (or binary data), you can use the `[MultipartObject]` attribute to mark the corresponding complex type. For file fields, it is recommended to declare the file field using the `MultipartFile` type. An example interface definition is as follows: ```cs showLineNumbers {4} public interface IHttpService : IHttpDeclarative { [Post("https://furion.net/")] Task PostStringAsync([MultipartObject] FormClass data); } ``` The corresponding model class is defined as follows: ```cs showLineNumbers {5,7} public class FormClass // Supports defining aliases via the [AliasAs] attribute { public int Id { get; set; } public string Name { get; set; } public MultipartFile File { get; set; } // public IFormFile File { get; set; } // Note: needs to be configured according to the following steps } ``` **Note**: If you use `IFormFile` instead of `MultipartFile`, make sure that `FormFileContentProcessor` has been registered. You can complete the registration by calling `.AddHttpContentProcessors(() => [new FormFileContentProcessor()])` globally: In the `Startup.cs` or `Program.cs` file, configure and register the `HttpRemote` service to enable the `IFormFile` content processor feature: ```cs showLineNumbers {3} services.AddHttpRemote(builder => { builder.AddHttpContentProcessors(() => [ new FormFileContentProcessor() ]); }); ``` In this way, the framework automatically submits the primitive-type properties of the object as ordinary form items, and correctly encodes and transmits `MultipartFile`-typed properties as file upload content. > **Notes on `JSON` Serialization Configuration** **Note**: When a typed object is passed in, the framework first converts the object to the `IDictionary` type, and then adds it as form items one by one. Therefore, this process does not directly use the `JSON` serialization configuration. If you need to specify an alias for a property, define it via the `[AliasAs]` attribute or the `[MultipartForm(NamingPolicy)]` attribute. Parameters marked with `MultipartAttribute` are set through the underlying `httpRequestBuilder.SetMultipartContent` method, and support parameters of any non-frozen type. The following code example shows how to achieve the same configuration effect using `HttpRequestBuilder`: ```cs showLineNumbers {5-11,18-20,27,34-35,42} // Add common form item content HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddFormItem(1, "id"); multipart.AddFormItem("Furion", "name"); multipart.AddFormItem(new { id = 1, name = "Furion" }, "obj"); multipart.AddStream(stream, "stream"); multipart.AddByteArray(bytes, "bytes"); multipart.Add(stringContent, "content"); multipart.AddFile(Multipart.CreateFromPath("path")); }); // Add file content HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddFileAsStream(@"C:\Workspaces\httptest.jpg", "files", contentType: "image/jpeg"); multipart.AddFileFromBase64String("77u/5rWL6K+V5paH5Lu25YaF5a65", "files"); multipart.AddFileFromRemote("https://furion.net/img/furionlogo.png", "files"); }); // Add object content. When AsFormItem is false, the object is parsed and traversed, and its properties are set as independent form items HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddObject(new { id = 1, name = "furion" }); // When AsFormItem is false, it is equivalent to not setting a form name }); // Set the boundary of the multipart form content HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.SetBoundary("--------------------"); multipart.AddFormItem(1, "id"); }); // Add complex form content HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddObject(new FormClass { Id = 1, Name = "furion", File = MultipartFile.CreateFromPath("file path") }); }); ``` Comparing the two approaches for sending multipart form content above, the `HTTP` declarative request approach has a more organized code structure and is easier to organize, maintain, and reuse. > **Applicable Scope of the `MultipartAttribute` and `MultipartFormAttribute` Attributes** - The `MultipartAttribute` attribute only applies to parameters. - The `MultipartFormAttribute` attribute only applies to methods. > **Notes on Frozen Parameter Types** In the system, `Action`, `Action`, `HttpCompletionOption`, and `CancellationToken` are treated as frozen parameter types; they are dedicated to serving specific operation execution. Therefore, when the `MultipartAttribute` attribute is applied to these parameter types, it will be ignored. `MultipartAttribute` includes the following constructors and properties: - **Constructors**: - `new()`: applies to a parameter, using the parameter as the multipart form item content. - `new(name)`: applies to a parameter, using the parameter as the multipart form item content, and supports setting the form name. - **Properties**: - `Name`: the form name (`string` type). - `FileName`: the name of the file (`string` type). - `ContentType`: the content type (`string` type). - `ContentEncoding`: the content encoding (`string` type). - `AsFileFrom`: indicates the source for treating a string as a multipart form file (`FileSourceType` type). It is used to set the multipart form file content and only takes effect when the parameter is of string type. The `FileSourceType` enumeration includes the following options: - `None` (default): not used as the source of the file. - `Path`: used as a local file path. - `Base64String`: used as a `Base64` string file. - `Remote`: used as an internet file address. - `AsFormItem`: indicates whether it is treated as one item of the form (`bool` type). The default value is `true` (treated as an item), and it only takes effect when the parameter is of object type. When `false` (not treated as an item), the object is parsed and traversed, and its properties are set as independent form items. `MultipartFormAttribute` includes the following constructors and properties: - **Constructors**: - `new()`: applies to a method, configuring the multipart form content properties. - `new(boundary)`: applies to a method, configuring the multipart form content properties, and supports setting the boundary of the multipart form content. - **Properties**: - `Boundary`: the boundary of the multipart form content (`string` type). The default value is: `$"----{DateTime.Now.Ticks:x}"`. - `OmitContentType`: whether to remove the default multipart content `Content-Type` (`bool` type). The default value is `true`. - `NamingPolicy`: the form name naming policy (converter) (`FormNamingPolicy` type). The default value is `FormNamingPolicy.None`. --- # 5.18 Disabling HTTP Caching > Source: https://http.furion.net/en/docs/declarative/disabling-http-caching/ When sending an `HTTP GET` request, the server may cache the result of that request to improve performance. To cancel its caching behavior, you can add the `DisableCacheAttribute` attribute. `HTTP` declarative requests disable `HTTP` caching via the `DisableCacheAttribute` attribute. The corresponding `HTTP` declarative extractor is implemented as the [`DisableCacheDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/DisableCacheDeclarativeExtractor.cs) type, which is responsible for parsing the `DisableCacheAttribute` attribute and building the `HttpRequestBuilder` instance configuration required to disable `HTTP` caching. ```cs showLineNumbers {2,6,10} // Apply on the interface definition, affecting all methods [DisableCache] public interface IHttpService : IHttpDeclarative { // Apply on a method [DisableCache] [Get("https://furion.net/")] Task GetStringAsync(); [DisableCache(false)] // Enable caching (default) [Get("https://furion.net/")] Task GetStringAsync(); } ``` After adding this attribute, the `HTTP` request will automatically attach the following request headers before sending to ensure cache control: ```bash showLineNumbers Cache-Control: must-revalidate, no-cache, no-store Pragma: no-cache If-None-Match: "" ``` > **Applicable Scope of the `DisableCacheAttribute` Attribute** The `DisableCacheAttribute` attribute applies to methods or interfaces. `DisableCacheAttribute` includes the following constructors and properties: - **Constructors**: - `new()`: applies to a method or interface, disabling `HTTP` caching. - `new(disabled)`: applies to a method or interface, setting whether to disable `HTTP` caching. - **Properties**: - `Disabled`: whether to disable (`bool` type); the default value is `true` (disabled). --- # 5.19 Ensuring Request Success > Source: https://http.furion.net/en/docs/declarative/ensuring-request-success/ After adding the `EnsureSuccessStatusCodeAttribute` attribute, when the `HTTP` response status code is outside the `200-299` range (that is, when the `IsSuccessStatusCode` property is `false`), an exception will be thrown automatically. `HTTP` declarative requests ensure request success via the `EnsureSuccessStatusCodeAttribute` attribute. The corresponding `HTTP` declarative extractor is implemented as the [`EnsureSuccessStatusCodeDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/EnsureSuccessStatusCodeDeclarativeExtractor.cs) type, which is responsible for parsing the `EnsureSuccessStatusCodeAttribute` attribute and building the `HttpRequestBuilder` instance configuration required to ensure request success. ```cs showLineNumbers {2,6,10} // Apply on the interface definition, affecting all methods [EnsureSuccessStatusCode] public interface IHttpService : IHttpDeclarative { // Apply on a method [EnsureSuccessStatusCode] [Get("https://furion.net/")] Task GetStringAsync(); [EnsureSuccessStatusCode(false)] // Disable validation [Get("https://furion.net/")] Task GetStringAsync(); } ``` > **Applicable Scope of the `EnsureSuccessStatusCodeAttribute` Attribute** The `EnsureSuccessStatusCodeAttribute` attribute applies to methods or interfaces. `EnsureSuccessStatusCodeAttribute` includes the following constructors and properties: - **Constructors**: - `new()`: applies to a method or interface, ensuring request success. - `new(enabled)`: applies to a method or interface, setting whether to ensure request success. - **Properties**: - `Enabled`: whether to enable (`bool` type); the default value is `true` (enabled). --- # 5.20 Simulating a Browser Environment (Crawler Detection) > Source: https://http.furion.net/en/docs/declarative/simulating-a-browser-environment-crawler-detection/ When developing a crawler program, the target website may provide different page versions based on the user agent (`User-Agent`) or other factors, such as the `PC` version and the mobile version. In addition, some websites have anti-crawler mechanisms that can identify and block crawler program access. To address these problems, we can configure the request headers to simulate a real browser environment for making requests. `HTTP` declarative requests simulate a browser environment via the `SimulateBrowserAttribute` attribute. The corresponding `HTTP` declarative extractor is implemented as the [`SimulateBrowserDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/SimulateBrowserDeclarativeExtractor.cs) type, which is responsible for parsing the `SimulateBrowserAttribute` attribute and building the `HttpRequestBuilder` instance configuration required to simulate a browser environment. ```cs showLineNumbers {2,9} // Apply on the interface definition, affecting all methods [SimulateBrowser] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStringAsync(); // Apply on a method [SimulateBrowser(IsMobile = true)] // Simulate a mobile browser environment [Get("https://furion.net/")] Task GetStringAsync(); } ``` After adding this attribute, the `HTTP` request will automatically attach the following request headers before sending to ensure that the server can accurately identify and process the request: ```bash showLineNumbers {2,5} # PC browser user agent Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0 # Mobile browser user agent Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Mobile Safari/537.36 Edg/142.0.0.0 ``` > **Applicable Scope of the `SimulateBrowserAttribute` Attribute** The `SimulateBrowserAttribute` attribute applies to methods or interfaces. `SimulateBrowserAttribute` includes the following constructors and properties: - **Constructors**: - `new()`: applies to a method or interface, enabling browser environment simulation. - **Properties**: - `IsMobile`: whether it is mobile (`bool` type); the default value is `false` (that is, the desktop version). --- # 5.21 Enabling the Request Profiler > Source: https://http.furion.net/en/docs/declarative/enabling-the-request-profiler/ Modern browsers typically have built-in developer tools that can capture and visually present all request and response data when users visit websites. Similarly, we provide a set of profiling tools for the `HTTP` remote request module. `HTTP` Declarative Requests enable the request profiler through the `ProfilerAttribute` attribute. The corresponding `HTTP` declarative extractor is implemented as the [`ProfilerDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/ProfilerDeclarativeExtractor.cs) type, which is responsible for parsing the `ProfilerAttribute` attribute and building the request-profiler-enabling configuration required for an `HttpRequestBuilder` instance. ```cs showLineNumbers {2,6,10} // Applied at the interface level, affects all methods [Profiler] public interface IHttpService : IHttpDeclarative { // Applied at the method level [Profiler] [Get("https://furion.net/")] Task GetStringAsync(); [Profiler(false)] // Disable the request profiler [Get("https://furion.net/")] Task GetStringAsync(); } ``` Once enabled, when an `HTTP` remote request is executed, the console outputs the following detailed information: ```bash showLineNumbers {8} General: Request URL: https://furion.net/ Request Method: GET Status Code: 200 OK HTTP Version: 1.1 HTTP Content: Content Type: Declarative: System.Threading.Tasks.Task GetStringAsync() | HttpAgent.Samples.IHttpService HttpClient Name: Request Duration (ms): 24.00 Response Headers: Server: nginx/1.22.1 Date: Sun, 24 Nov 2024 16:48:50 GMT Connection: keep-alive Vary: Accept-Encoding ETag: "67426a3f-f366" Cache-Control: max-age=315360000 Accept-Ranges: bytes Content-Type: text/html Content-Length: 62310 Last-Modified: Sat, 23 Nov 2024 23:50:23 GMT Expires: Thu, 31 Dec 2037 23:55:55 GMT ``` > **Notes on `Blazor WebAssembly` Projects** In `Blazor WebAssembly` applications, the request profiler output is displayed in the developer tools console of the client (i.e., the browser). Make sure to check this console during development to obtain the relevant profiling information. In addition to enabling the profiler for individual requests, you can also register it globally to enable it in `HttpClient`: ```cs showLineNumbers {3,7,10,13-14,17-18,21-22} // Enable for the default client services.AddHttpClient(string.Empty) .AddProfilerDelegatingHandler(); // You can also provide conditional disabling, e.g., disable in production services.AddHttpClient(string.Empty) .AddProfilerDelegatingHandler(disableIn: () => builder.Environment.EnvironmentName == "Production"); services.AddHttpClient(string.Empty) .AddProfilerDelegatingHandler(disableInProduction: true); // Enable for a specific client //services.AddHttpClient("weixin") // .AddProfilerDelegatingHandler(); // You can also enable it for all client configurations in one step services.ConfigureHttpClientDefaults(clientBuilder => clientBuilder.AddProfilerDelegatingHandler()); // Or use the IHttpRemoteBuilder extension method for one-step configuration services.AddHttpRemote() .ConfigureHttpClientDefaults(clientBuilder => clientBuilder.AddProfilerDelegatingHandler()); ``` By enabling the request profiler, developers can observe and debug `HTTP` requests more intuitively and conveniently, thereby improving development efficiency and debugging accuracy. > **Scope of the `ProfilerAttribute` Attribute** The `ProfilerAttribute` attribute applies to methods or interfaces. `ProfilerAttribute` contains the following constructors and properties: - **Constructors**: - `new()`: Applies to methods or interfaces and enables the request profiler. - `new(enabled)`: Applies to methods or interfaces and sets whether to enable the request profiler. - **Properties**: - `Enabled`: Whether to enable (type `bool`), defaulting to `true` (enabled). > **Disable in Production** To ensure optimal performance and security in production, it is recommended to **disable** the request profiler in production environments. In addition, printing request content may cause the `Stream` object to be read repeatedly or become unreadable, because the stream is read into memory in advance and its `Position` is moved to the end as a result. **Supplementary note:** By default, the request profiler only displays up to `5KB` of the request or response content data. --- # 5.22 Setting the Client's Preferred Language and Region > Source: https://http.furion.net/en/docs/declarative/setting-the-clients-preferred-language-and-region/ Globalization is the development trend for internet application products; therefore, products targeting a global audience should support internationalization. When sending `HTTP` requests, you can specify the client's preferred natural language and region by adding the `AcceptLanguageAttribute` attribute. `HTTP` Declarative Requests set the client's preferred language and region through the `AcceptLanguageAttribute` attribute. The corresponding `HTTP` declarative extractor is implemented as the [`AcceptLanguageDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/AcceptLanguageDeclarativeExtractor.cs) type, which is responsible for parsing the `AcceptLanguageAttribute` attribute and building the client's preferred language and region configuration required for an `HttpRequestBuilder` instance. ```cs showLineNumbers {2,6,10} // Applied at the interface level, affects all methods [AcceptLanguage("en-US")] public interface IHttpService : IHttpDeclarative { // Applied at the method level [AcceptLanguage("zh-CN,en;q=0.5")] [Get("https://furion.net/")] Task GetStringAsync(); [AcceptLanguage("fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5")] [Get("https://furion.net/")] Task GetStringAsync(); } ``` > **Scope of the `AcceptLanguageAttribute` Attribute** The `AcceptLanguageAttribute` attribute applies to methods or interfaces. `AcceptLanguageAttribute` contains the following constructors and properties: - **Constructors**: - `new(language)`: Applies to methods or interfaces and configures the client's preferred language and region. - **Properties**: - `Language`: The client's preferred language and region (type `string`). --- # 5.23 Setting HttpRequestMessage Properties > Source: https://http.furion.net/en/docs/declarative/setting-httprequestmessage-properties/ In certain scenarios, we may need to add extra properties to the `HttpRequestMessage` request rather than going through request headers. `HTTP` Declarative Requests set `HttpRequestMessage` request properties through the `PropertyAttribute` attribute. The corresponding `HTTP` declarative extractor is implemented as the [`PropertyDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/PropertyDeclarativeExtractor.cs) type, which is responsible for parsing the `PropertyAttribute` attribute and building the `HttpRequestMessage` request property configuration required for an `HttpRequestBuilder` instance. Using the `PropertyAttribute` attribute, you can conveniently add `HttpRequestMessage` request properties on interfaces, methods, or parameters. ```cs showLineNumbers {2-4,8-9,14,16,20,24,28,32} // Applied at the interface level, affects all methods [Property("property1", "value1")] [Property("property2", "value2")] [Property("property0")] // Value is null public interface IHttpService : IHttpDeclarative { // Applied at the method level [Property("property3", "value3")] [Property("property4", "value4")] [Get("https://furion.net/")] Task GetStringAsync(); // Applied at the parameter level; the AliasAs property can specify an alias, and multiple attributes are allowed [Property("property3", "value3")] [Get("https://furion.net/")] Task GetStringAsync([Property] string property4, [Property][Property(AliasAs = "property5")] int lastProperty); // At the parameter level, you can set a default value through the Value property; similarly for the age parameter, e.g., int? age = 30 [Get("https://furion.net/")] Task GetStringAsync([Property(Value = 30)] int? age); // [AliasAs] is supported to define an alias [Get("https://furion.net/")] Task GetStringAsync([Property][AliasAs("property5")] int lastProperty); // Add object content; when AsItem is false, the object is parsed and traversed, and its properties are set as individual HttpRequestMessage request property items [Get("https://furion.net/")] Task GetStringAsync([Property(AsItem = false)] object obj); // Frozen parameter types are ignored [Get("https://furion.net/")] Task GetStringAsync([Property] CancellationToken cancellationToken); } ``` These properties are added to the `Options` property of the `HttpRequestMessage` object ([reference documentation](https://learn.microsoft.com/zh-cn/dotnet/api/system.net.http.httprequestmessage.options)). To retrieve these values, you can do the following: ```cs showLineNumbers httpRequestMessage.Options.TryGetValue(new HttpRequestOptionsKey("key1"), out var value); ``` If a property key is duplicated, the value set later overrides the earlier setting. > **Tip** This feature is often integrated into custom `DelegatingHandler` and `IHttpRequestEventHandler` components. `PropertyAttribute` contains the following constructors and properties: - **Constructors**: - `new()`: Effective when applied to parameters; adds an `HttpRequestMessage` request property with the parameter name as the default key. - `new(name)`: When applied to methods or interfaces, adds an `HttpRequestMessage` request property operation with a value of `null`; when applied to parameters, adds an `HttpRequestMessage` request property with the key being the value of the `name` parameter. - `new(name, value)`: Applies to interfaces, methods, or parameters; adds an `HttpRequestMessage` request property with the key being the value of the `name` parameter, with lower priority than the `AliasAs` property. - **Properties**: - `Name`: The `HttpRequestMessage` request property key (type `string`), with lower priority than the `AliasAs` property. - `Value`: The value of the `HttpRequestMessage` request property (type `object`); when the attribute is applied to a parameter, it represents the default value. - `AliasAs`: The alias of the `HttpRequestMessage` request property key (type `string`), with higher priority than the `Name` property. - `AsItem`: Indicates whether to treat the value as a single item of the `HttpRequestMessage` request property (type `bool`), defaulting to `true` (as an item), and only takes effect when the parameter is an object type. When `false` (not as an item), the object is parsed and traversed, and its properties are set as individual `HttpRequestMessage` request property items. > **Notes on Frozen Parameter Types** In the system, `Action`, `Action`, `HttpCompletionOption`, and `CancellationToken` are treated as frozen parameter types that are dedicated to specific operation execution. Therefore, the `PropertyAttribute` attribute is ignored when applied to these parameter types. > **Did You Know** `C#` supports attribute merging, which makes the code more concise: ```cs showLineNumbers {1,4} [Property("property1", "value1"), Property("property2", "value2")] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/"), Property("property3", "value3"), Property("property4", "value4")] Task GetStringAsync(); } ``` --- # 5.24 Enabling Standard Request Headers > Source: https://http.furion.net/en/docs/declarative/enabling-standard-request-headers/ To improve the compatibility of network requests sent by applications through the `HTTP` client and avoid being blocked by `WAF` (`Web` application firewalls), the framework provides a one-step configuration method that makes it easy to quickly set standard request headers uniformly. `HTTP` Declarative Requests enable standard request headers through the `StandardRequestHeadersAttribute` attribute. The corresponding `HTTP` declarative extractor is implemented as the [`StandardRequestHeadersDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/StandardRequestHeadersDeclarativeExtractor.cs) type, which is responsible for parsing the `StandardRequestHeadersAttribute` attribute and building the configuration required for an `HttpRequestBuilder` instance. ```cs showLineNumbers {2,6,10} // Applied at the interface level, affects all methods [StandardRequestHeaders] public interface IHttpService : IHttpDeclarative { // Applied at the method level [StandardRequestHeaders] [Get("https://furion.net/")] Task GetStringAsync(); [StandardRequestHeaders(false)] // Turn off standard request headers [Get("https://furion.net/")] Task GetStringAsync(); } ``` In addition to enabling standard header configuration for individual requests, you can also register it globally to enable it in `HttpClient`: ```cs showLineNumbers {4} // Enable for the default client services.AddHttpClient(string.Empty, client => { client.UseStandardRequestHeaders(); }); services.AddHttpRemote(); ``` After standard request headers are enabled, requests automatically add the following headers: - **`Accept`**: `application/json`, `text/plain;q=0.9`, `*/*;q=0.8` (explicit media type priority to avoid being blocked by `WAF`) - **`Connection`**: Enables persistent connections (`Keep-Alive`) to reduce the overhead of establishing and closing `TCP` connections > **Scope of the `StandardRequestHeadersAttribute` Attribute** The `StandardRequestHeadersAttribute` attribute applies to methods or interfaces. `StandardRequestHeadersAttribute` contains the following constructors and properties: - **Constructors**: - `new()`: Applies to methods or interfaces and enables standard request headers. - `new(enabled)`: Applies to methods or interfaces and sets whether to enable standard request headers. - **Properties**: - `Enabled`: Whether to enable (type `bool`), defaulting to `true` (enabled). --- # 5.25 Setting the Automatic Host Header > Source: https://http.furion.net/en/docs/declarative/setting-the-automatic-host-header/ The `Host` header is a required header in the `HTTP/1.1` protocol. The `Host` header is used to specify the hostname and port number of the target server for a request, ensuring that the server can correctly distinguish between different domain names on the same `IP` address and handle them accordingly. The framework provides a simple way to configure this: `HTTP` Declarative Requests set the automatic `Host` header through the `AutoSetHostHeaderAttribute` attribute. The corresponding `HTTP` declarative extractor is implemented as the [`AutoSetHostHeaderDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/AutoSetHostHeaderDeclarativeExtractor.cs) type, which is responsible for parsing the `AutoSetHostHeaderAttribute` attribute and building the automatic `Host` header configuration required by an `HttpRequestBuilder` instance. ```cs showLineNumbers {2,6,10} // Apply at the interface level to affect all methods [AutoSetHostHeader] // Enable public interface IHttpService : IHttpDeclarative { // Apply at the method level [AutoSetHostHeader] [Get("https://furion.net/")] Task GetStringAsync(); [AutoSetHostHeader(false)] // Disable the automatic Host header [Get("https://furion.net/")] Task GetStringAsync(); } ``` Once enabled, a `Host: furion.net` header is automatically added when sending `HTTP` remote requests. > **Tip** When integrating with `API` interfaces provided by legacy programs, it is recommended to enable this configuration to improve compatibility. > **`Host` Issues Caused by `HttpClient` Automatic Redirection** When sending `HTTP` remote requests, if the target server returns a redirect response (such as `301 Moved Permanently` or `302 Found`), the framework follows redirects automatically by default. However, when the automatic `Host` header is enabled, you may encounter a problem where the `Host` header cannot be updated. In this case, you can disable the `AllowAutoRedirect` option so that the framework handles redirects correctly: ```cs showLineNumbers {3,5} // Configure the default client services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { AllowAutoRedirect = false }); ``` In addition, if you need to set the maximum number of redirects for the framework's built-in redirect behavior, you can use the following approach: ```cs showLineNumbers {2,4} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { MaximumAutomaticRedirections = 20; }); ``` This ensures that the redirect behavior works as expected while avoiding the problem of an incorrectly set `Host` header. > **`AutoSetHostHeaderAttribute` Attribute Scope** The `AutoSetHostHeaderAttribute` attribute applies to methods or interfaces. `AutoSetHostHeaderAttribute` includes the following constructors and properties: - **Constructors**: - `new()`: Applies to methods or interfaces, setting the automatic `Host` header. - `new(enabled)`: Applies to methods or interfaces, setting whether to set the automatic `Host` header. - **Properties**: - `Enabled`: Whether enabled (type `bool`), defaulting to `true` (enabled). --- # 5.26 Setting the Request Base Address > Source: https://http.furion.net/en/docs/declarative/setting-the-request-base-address/ When you need to integrate with multiple third-party `API`s, you usually register and configure the `BaseAddress` of multiple `HttpClient` instances globally. For example: ```cs showLineNumbers {4,10} // Configure the base address of the default client services.AddHttpClient(string.Empty, client => { client.BaseAddress = new Uri("https://furion.net/"); }); // Configure the base address of the GitHub client services.AddHttpClient("github", client => { client.BaseAddress = new Uri("https://github.com/"); }); ``` You can then specify the client to use via the `[HttpClientName(client name)]` attribute. In addition to global configuration, the framework also supports setting the base address locally in Declarative Requests, allowing it to be specified dynamically when building the request. `HTTP` Declarative Requests set the request base address through the `BaseAddressAttribute` attribute. The corresponding `HTTP` declarative extractor is implemented as the [`BaseAddressDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/BaseAddressDeclarativeExtractor.cs) type, which is responsible for parsing the `BaseAddressAttribute` attribute and building the request base address configuration required by an `HttpRequestBuilder` instance. ```cs showLineNumbers {2,6,9,14} // Apply at the interface level to affect all methods [BaseAddress("https://furion.net")] public interface IHttpService : IHttpDeclarative { [Get("/api/test")] Task GetStringAsync(); // Apply at the method level [BaseAddress("https://baiqian.com")] [Get("/api/test/2")] Task GetStringAsync(); // Can be used as a prefix [BaseAddress("/furion")] [Get("/api/test/2")] Task GetStringAsync(); } ``` Once enabled, the request base address is automatically set when sending `HTTP` remote requests. > **Important Note** Make sure the request base address you set is an absolute path, that is, it starts with `http://` or `https://`. **Processing Logic**: - If the request address is an absolute address, that address is used directly to send the request. - If the request address is a relative address: - When no local `BaseAddress` is set, it is combined with the `BaseAddress` of the global `HttpClient` instance to form the final request address. - When a local `BaseAddress` is set: - If the local `BaseAddress` is a relative address, it is first prepended to the request address and then combined with the global `BaseAddress`. - If the local `BaseAddress` is an absolute address, that absolute address is directly combined with the request address to form the final request address (the global `BaseAddress` is ignored in this case). > **`BaseAddressAttribute` Attribute Scope** The `BaseAddressAttribute` attribute applies to methods or interfaces. `BaseAddressAttribute` includes the following constructors and properties: - **Constructors**: - `new(baseAddress)`: Applies to methods or interfaces, setting the request base address. - **Properties**: - `BaseAddress`: The request base address (type `string`). > **Configuration Parameter Support** The request base address supports configuration parameters, which are used to read configuration information for substitution. Configuration parameters use the `[[key]]` syntax. --- # 5.27 Setting the Referrer Address (Hotlink Protection) > Source: https://http.furion.net/en/docs/declarative/setting-the-referrer-address-hotlink-protection/ When accessing certain third-party servers, the server may validate the `Referer` source address in the request headers. For example, when downloading images, triggering the hotlink protection mechanism may cause the retrieved images to not match expectations. In this case, you can set the `Referer` request header to simulate the source page and bypass hotlink protection detection. `HTTP` Declarative Requests set the request referrer address through the `RefererAttribute` attribute. The corresponding `HTTP` declarative extractor is implemented as the [`RefererDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/RefererDeclarativeExtractor.cs) type, which is responsible for parsing the `RefererAttribute` attribute and building the request referrer address configuration required by an `HttpRequestBuilder` instance. ```cs showLineNumbers {2,6,9} // Apply at the interface level to affect all methods [Referer("https://furion.net")] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/logo.png")] Task GetStringAsync(); // Apply at the method level [Referer("https://baiqian.com")] [Get("https://furion.net/logo2.png")] Task GetStringAsync(); } ``` Once enabled, the request referrer address is automatically set when sending `HTTP` remote requests. To simplify configuration, the framework provides a built-in template string `"{BASE_ADDRESS}"` that automatically extracts the base address of the request address as the `Referer`: ```cs showLineNumbers {1} [Referer("{BASE_ADDRESS}")] // Automatically replaces {BASE_ADDRESS} with https://furion.net/ when sending public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/logo.png")] Task GetStringAsync(); } ``` > **`RefererAttribute` Attribute Scope** The `RefererAttribute` attribute applies to methods or interfaces. `RefererAttribute` includes the following constructors and properties: - **Constructors**: - `new(referer)`: Applies to methods or interfaces, setting the request referrer address. - **Properties**: - `Referer`: The request referrer address (type `string`). --- # 5.28 Configuring the HTTP Version > Source: https://http.furion.net/en/docs/declarative/configuring-the-http-version/ When initiating `HTTP` remote requests, the default `HTTP` protocol version is `1.1`. However, when accessing certain third-party servers, these servers may validate the `HTTP` version (for example, requiring version `2.0`). `HTTP` Declarative Requests set the `HTTP` version through the `HttpVersionAttribute` attribute. The corresponding `HTTP` declarative extractor is implemented as the [`HttpVersionDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/HttpVersionDeclarativeExtractor.cs) type, which is responsible for parsing the `HttpVersionAttribute` attribute and building the `HTTP` version configuration required by an `HttpRequestBuilder` instance. ```cs showLineNumbers {2,6,9} // Apply at the interface level to affect all methods [HttpVersion("1.2")] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/logo.png")] Task GetStringAsync(); // Apply at the method level [HttpVersion("2.0")] [Get("https://furion.net/logo2.png")] Task GetStringAsync(); } ``` Once enabled, the `HTTP` version is automatically set when sending `HTTP` remote requests. In addition to configuring it through the `[HttpVersion]` attribute, the system also supports a global configuration approach, as shown in the following example: ```cs showLineNumbers {2,4,8,10} // Configure the default client services.AddHttpClient(string.Empty, client => { client.DefaultRequestVersion = HttpVersion.Version10; }); // Configure a specific client services.AddHttpClient("weixin", client => { client.DefaultRequestVersion = HttpVersion.Version10; }); ``` > **`HttpVersionAttribute` Attribute Scope** The `HttpVersionAttribute` attribute applies to methods or interfaces. `HttpVersionAttribute` includes the following constructors and properties: - **Constructors**: - `new(version)`: Applies to methods or interfaces, setting the `HTTP` version. - **Properties**: - `Version`: The `HTTP` version (type `string`). --- # 5.29 Exception Suppression Mechanism (Silent Handling) > Source: https://http.furion.net/en/docs/declarative/exception-suppression-mechanism-silent-handling/ When initiating an `HTTP` remote request, the following exceptions may be encountered: - The target host is unreachable - The request is canceled - The request times out - Other network exceptions By default, these exceptions interrupt program execution. Although developers typically use `try/catch` for exception handling, in certain scenarios we would rather silently return `null` when an exception occurs without interrupting the flow. To this end, the framework provides flexible exception suppression. `HTTP` declarative requests suppress exceptions through the `SuppressExceptionsAttribute` attribute. The corresponding `HTTP` declarative extractor is implemented as the [`SuppressExceptionsDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/SuppressExceptionsDeclarativeExtractor.cs) type, which is responsible for parsing the `SuppressExceptionsAttribute` attribute and building the exception suppression configuration required by an `HttpRequestBuilder` instance. ```cs showLineNumbers {2,6,9,13} // Applied on the interface definition, affecting all methods [SuppressExceptions] // Suppress all exceptions public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/logo.png")] Task GetStringAsync(); // Applied on the method [SuppressExceptions(typeof(TimeoutException), typeof(TaskCanceledException))] // Suppress timeout and cancellation exceptions [Get("https://furion.net/logo2.png")] Task GetStringAsync(); [SuppressExceptions(false)] // Disable exception suppression (restore the default configuration) [Get("https://furion.net/logo2.png")] Task GetStringAsync(); } ``` After it is enabled, the exception suppression mechanism is automatically set when an `HTTP` remote request is sent. > **Scope of the `SuppressExceptionsAttribute` Attribute** The `SuppressExceptionsAttribute` attribute applies to methods or interfaces. `SuppressExceptionsAttribute` includes the following constructors and properties: - **Constructors**: - `new()`: Applies to a method or interface; suppresses all exceptions. - `new(enable)`: Applies to a method or interface; indicates whether to enable exception suppression. - `new(types)`: Applies to a method or interface; suppresses exceptions of the specified types. - **Properties**: - `Types`: The collection of exception suppression types (of type `type[]`; every element in the array must be of type `System.Exception` or a derived type). > **Important Notes** When enabling exception suppression, note the following: 1. **Override rule** When `SuppressExceptions()` or related configuration is invoked multiple times, **only the last call takes effect**. 2. **Priority between status code checking and exception suppression** Even if `EnsureSuccessStatusCode()` is configured, a suppressed exception still returns `null` and does not trigger the status code checking logic. 3. **Priority of exception suppression** Exception suppression has higher priority than status code checking. If both status code checking and exception suppression are enabled, exception suppression takes effect first. 4. **Request interceptors remain available** If exceptions are captured via `SetOnRequestFailed(ex, res)` or other request handling mechanisms, the interceptor or callback method is still invoked even when the exception is suppressed. 5. **Exception type selection advice** Carefully choose the exception types to suppress based on the specific business scenario, and avoid masking potential problems by over-suppressing exceptions. 6. **Automatic suppression logging** When an exception is successfully suppressed, the framework automatically outputs a `Warning`-level log (for example `"An exception occurred but was suppressed by SuppressExceptionPipelineHandler."`) to facilitate troubleshooting. --- # 5.30 Enabling Parameter Validation > Source: https://http.furion.net/en/docs/declarative/validation/ When invoking `HTTP` declarative interface methods, the framework supports validating the legality of the passed parameter data. `HTTP` declarative requests enable parameter validation through attributes derived from `ValidationAttribute`. The corresponding `HTTP` declarative extractor is implemented as the [`ValidationDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/ValidationDeclarativeExtractor.cs) type, which is responsible for parsing attributes derived from `ValidationAttribute` and validating the legality of the passed parameter data. - **Validating individual values and object data** ```cs showLineNumbers {9,14-17,21,29-31} public interface IHttpService : IHttpDeclarative { // No parameter validation is required [Get("https://furion.net/")] Task GetStringAsync(string str, object obj); // Validate parameter legality; supports validating validation attributes inside the object model [Get("https://furion.net/")] Task GetStringAsync([Length(10, 20)] string str, [Required] ValidationModel obj); // Supports adding multiple validation rules to a parameter [Get("https://furion.net/")] Task GetStringAsync( [Required] [MinLength(2)] [MaxLength(5)] string str, [Range(0, 10)] int age); // Frozen parameter types will be ignored [Get("https://furion.net/")] Task GetStringAsync([Required] CancellationToken cancellationToken); } // Object property validation public class ValidationModel { public int Id { get; set; } [Required] [MinLength(3)] public string? Name { get; set; } } ``` - **Validating object data that implements the `IValidatableObject` interface** ```cs showLineNumbers {4,8,16-22} public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStringAsync(ValidationObject obj); } // Implement IValidatableObject for complex validation public class ValidationObject : IValidatableObject { public int Id { get; set; } [Required] [MinLength(3)] public string? Name { get; set; } public IEnumerable Validate(ValidationContext validationContext) { if (Id < 0) { yield return new ValidationResult("Id must be greater than or equal to 0.", [nameof(Id)]); } } } ``` - **Validating custom `ValidationAttribute` attributes** ```cs showLineNumbers {4,8,15-23} public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStringAsync([StringEqual("Furion")] string str); } // Custom validation attribute public class StringEqualAttribute : ValidationAttribute { public StringEqualAttribute(string value) => Value = value; public string Value { get; } /// protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) { if (value?.ToString() != Value) { return new ValidationResult($"Value is not equal to {Value}."); } return ValidationResult.Success; } } ``` Parameters marked with attributes derived from `ValidationAttribute` and parameters implementing the `IValidatableObject` interface are validated through the underlying `Validator.ValidateValue` and `Validator.ValidateObject` methods, and support any non-frozen parameter type. > **Scope of Attributes Derived from `ValidationAttribute`** Attributes derived from `ValidationAttribute` apply only to parameters. > **Notes on Frozen Parameter Types** In the system, `Action`, `Action`, `HttpCompletionOption`, and `CancellationToken` are treated as frozen parameter types; they serve specific operation executions. Therefore, attributes derived from `ValidationAttribute` are ignored when applied to these parameter types. > **Tip** `C#` supports attribute merging, making the code more concise: ```cs showLineNumbers {1,4} public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStringAsync([Required, MinLength(2), MaxLength(5)] string str, [Range(0, 10)] int age) } ``` - **Disabling parameter validation** The framework provides the `SuppressValidationAttribute` attribute, which can be used to disable parameter validation for `HTTP` declarative requests. When applied to an interface, it disables parameter validation for all methods under that interface; when applied to a specific method, it disables parameter validation only for that method. ```cs showLineNumbers {3} public interface IHttpService : IHttpDeclarative { [SuppressValidation] // Disable parameter validation only for the GetStringAsync method [Get("https://furion.net/")] Task GetStringAsync([Length(10, 20)] string str, [Required] ValidationModel obj); } ``` ```cs showLineNumbers {1,8} [SuppressValidation] // Disable parameter validation for all methods defined on the interface public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStringAsync([Length(10, 20)] string str, [Required] ValidationModel obj); [Get("https://furion.net/")] [SuppressValidation(false)] // Enable parameter validation for the GetStringAsync method Task GetStringAsync([Length(10, 20)] string str, [Required] ValidationModel obj); } ``` --- # 5.31 Enabling the JSON Response Deserialization Wrapper > Source: https://http.furion.net/en/docs/declarative/enabling-the-json-response-deserialization-wrapper/ When performing `HTTP` remote communication with third-party `API`s, a `JSON` response with a unified structure is usually returned, such as the `ApiResult` type, where the actual data is stored in the `Data` property: ```cs showLineNumbers {1,4} public class ApiResult { public bool Success { get; set; } public T? Data { get; set; } // Actual returned data } ``` `HTTP` declarative requests enable the `JSON` response deserialization wrapper through the `JsonResponseWrapperAttribute` attribute. The corresponding `HTTP` declarative extractor is implemented as the [`JsonResponseWrapperDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/JsonResponseWrapperDeclarativeExtractor.cs) type, which is responsible for parsing the `JsonResponseWrapperAttribute` attribute and building the `JSON` response deserialization wrapper configuration required by an `HttpRequestBuilder` instance. When the `JSON` response deserialization wrapper feature is not enabled, the `ApiResult` type must be explicitly specified on every call: ```cs showLineNumbers {4,7} public interface IHttpService : IHttpDeclarative { [Get("https://furion.net")] Task> GetStringAsync(); [Get("https://furion.net/")] Task> GetJsonModelAsync(); } ``` ### Ways to Enable #### 1. One-Time Enablement To simplify the calling process, you can configure the `JSON` response deserialization wrapper so that it automatically extracts the content of the `Data` property: ```cs showLineNumbers {2-3,5} // Configure the default HTTP client services.AddHttpClient(string.Empty) .ConfigureOptions(options => { options.JsonResponseWrapper = new JsonResponseWrapper(typeof(ApiResult<>), nameof(ApiResult<>.Data)); }); ``` After configuration is complete, enable the feature via `[JsonResponseWrapper]`; afterwards you only need to specify the target data type, without repeatedly declaring `ApiResult`: ```cs showLineNumbers {2,7,10,12,14,16} // Applied on the interface definition, affecting all methods [JsonResponseWrapper] public interface IHttpService : IHttpDeclarative { // Applied automatically by default [Get("https://furion.net/")] Task GetStringAsync(); // Applied on the method [JsonResponseWrapper] // Can be explicitly enabled (not required) [Get("https://furion.net/")] Task GetStringAsync(); [JsonResponseWrapper(false)] // Disable the JSON response deserialization wrapper; the complete response type must be passed in [Get("https://furion.net/")] Task> GetJsonModelAsync(); } ``` At runtime, the framework automatically creates an `ApiResult` instance and returns the value of its `Data` property. #### 2. Global Enablement (Enabled by Default for All Requests) You can also globally enable the `JSON` response deserialization wrapper feature by simply setting `UseJsonResponseWrapper` to `true`: ```cs showLineNumbers {2-3,6} // Configure the default HTTP client services.AddHttpClient(string.Empty) .ConfigureOptions(options => { options.JsonResponseWrapper = new JsonResponseWrapper(typeof(ApiResult<>), nameof(ApiResult<>.Data)); options.UseJsonResponseWrapper = true; }); ``` After global enablement, all requests use the wrapper feature by default: ```cs showLineNumbers {1} // [JsonResponseWrapper] // No need to explicitly set [JsonResponseWrapper] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStringAsync(); } ``` #### 3. One-time Disabling (Overriding Global Settings) If you need to disable this feature for a specific request, set the `[JsonResponseWrapper(false)]` attribute. ### Custom Result Handling (`ResultHandler`) Sometimes, in addition to extracting `Data`, you may need to perform additional validation or conversion on the response. This can be implemented through the `ResultHandler` callback: ```cs showLineNumbers {7,12,16,19} // Configure the default HTTP client services.AddHttpClient(string.Empty) .ConfigureOptions(options => { options.JsonResponseWrapper = new JsonResponseWrapper(typeof(ApiResult<>), nameof(ApiResult<>.Data)) { ResultHandler = context => { if (context.Instance is { } instance) { // Access the wrapper type instance and get any of its properties var success = context.GetPropertyValue(nameof(ApiResult<>.Success)); } // For example, ensure the request succeeds context.ResponseMessage.EnsureSuccessStatusCode(); // Return the final target result (i.e. the value of Data) return context.Result; } }; }); ``` With `ResultHandler`, you can execute any custom logic (such as validation, conversion, or exception handling) before returning the final data, making request processing more flexible. The `context` parameter is of type `JsonResponseWrapperContext` and contains the following properties and methods: - **Properties**: - `Instance`: The concrete instance of the wrapper type (such as `ApiResult`, of type `object?`). - `Result`: The target result (i.e. the value of `Data`, of type `object?`). - `ResponseMessage`: The response message (of type `HttpResponseMessage`). - **Methods**: - `GetPropertyValue(propertyName)`: Gets the specified property value from the concrete type of the wrapper type (i.e. `Instance`). > **Scope of the `JsonResponseWrapperAttribute` Attribute** The `JsonResponseWrapperAttribute` attribute applies to methods or interfaces. `JsonResponseWrapperAttribute` contains the following constructors and properties: - **Constructors**: - `new()`: Applies to methods or interfaces, enabling the `JSON` response deserialization wrapper. - `new(enabled)`: Applies to methods or interfaces, setting whether the `JSON` response deserialization wrapper is enabled. - **Properties**: - `Enabled`: Whether it is enabled (`bool` type), defaulting to `true` (enabled). --- # 5.32 Response JSON Double Serialization Handling > Source: https://http.furion.net/en/docs/declarative/response-json-double-serialization-handling/ When communicating remotely with third-party `API`s over `HTTP`, in very rare cases the `JSON` data returned by the server may be unintentionally double-serialized (sometimes deliberately). For example, instead of returning `"{\"id\":1,\"name\":\"furion\"}"`, double serialization turns it into `"\"{\\\"id\\\":10, \\\"name\\\":\\\"furion\\\"}\""`. For such cases, the framework provides unwrapping support. `HTTP` declarative requests enable unwrapping of the `JSON` response content string through the `JsonResponseStringUnwrapAttribute` attribute. The corresponding `HTTP` declarative extractor implementation is the [`JsonResponseStringUnwrapDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/JsonResponseStringUnwrapDeclarativeExtractor.cs) type, which is responsible for parsing the `JsonResponseStringUnwrapAttribute` attribute and building the configuration required by the `HttpRequestBuilder` instance to enable unwrapping of the `JSON` response content string. ```cs showLineNumbers {2,7,10,12,14,16} // Applied at the interface definition, affecting all methods [JsonResponseStringUnwrap] public interface IHttpService : IHttpDeclarative { // Applied automatically by default [Get("https://furion.net/")] Task GetAsync(); // Applied at the method level [JsonResponseStringUnwrap] // Can be explicitly enabled (not required) [Get("https://furion.net/")] Task GetAsync(); [JsonResponseStringUnwrap(false)] // Disable unwrapping of the JSON response content string [Get("https://furion.net/")] Task> GetAsync(); } ``` If you need to disable this feature for a specific request, set the `[JsonResponseStringUnwrap(false)]` attribute. > **Scope of the `JsonResponseStringUnwrapAttribute` Attribute** The `JsonResponseStringUnwrapAttribute` attribute applies to methods or interfaces. `JsonResponseStringUnwrapAttribute` contains the following constructors and properties: - **Constructors**: - `new()`: Applies to methods or interfaces, enabling unwrapping of the `JSON` response content string. - `new(enabled)`: Applies to methods or interfaces, setting whether unwrapping of the `JSON` response content string is enabled. - **Properties**: - `Enabled`: Whether it is enabled (`bool` type), defaulting to `true` (enabled). --- # 5.33 Setting the Request Event Handler > Source: https://http.furion.net/en/docs/declarative/setting-the-request-event-handler/ The `IHttpRequestEventHandler` interface lets you define preprocessing operations for `HTTP` requests. By implementing this interface, you can create a custom request event handler, such as the `CustomRequestEventHandler` class: ```cs showLineNumbers {1} public class CustomRequestEventHandler : IHttpRequestEventHandler { // Operation before sending the HTTP request public void OnPreSendRequest(HttpRequestMessage httpRequestMessage) {} // Operation after receiving the HTTP response public Task OnPostReceiveResponseAsync(HttpResponseMessage httpResponseMessage, CancellationToken cancellationToken) {} // Operation when an exception occurs while sending the HTTP request public void OnRequestFailed(Exception exception, HttpResponseMessage? httpResponseMessage = null) {} } ``` When sending an `HTTP` request, you can set the request event handler by adding the `RequestEventHandlerAttribute` attribute. `HTTP` declarative requests set the request event handler through the `RequestEventHandlerAttribute` attribute. The corresponding `HTTP` declarative extractor implementation is the [`RequestEventHandlerDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/RequestEventHandlerDeclarativeExtractor.cs) type, which is responsible for parsing the `RequestEventHandlerAttribute` attribute and building the request event handler configuration required by the `HttpRequestBuilder` instance. ```cs showLineNumbers {2,7,10} // Applied at the interface definition, affecting all methods [RequestEventHandler(typeof(CustomRequestEventHandler))] public interface IHttpService : IHttpDeclarative { // Automatically applies the interface declaration [Get("https://furion.net/")] Task GetStringAsync(); // Applied at the method level [RequestEventHandler(typeof(CustomRequestEventHandler))] [Get("https://furion.net/")] Task GetStringAsync(); } ``` > **Scope of the `RequestEventHandlerAttribute` Attribute** The `RequestEventHandlerAttribute` attribute applies to methods or interfaces. `RequestEventHandlerAttribute` contains the following constructors and properties: - **Constructors**: - `new(handlerType)`: Applies to methods or interfaces, configuring the request event handler. - **Properties**: - `HandlerType`: The request event handler (of type `Type`). --- # 5.34 Disabling Automatic Access Token Management > Source: https://http.furion.net/en/docs/declarative/disabling-automatic-access-token-management/ The framework has a built-in `Access Token` automatic management feature. You only need to implement the `IHttpAccessTokenProvider` interface and write the logic for obtaining the `Access Token` in the `GetAsync` method. Normally, obtaining an `Access Token` requires a separate `HTTP` request, and if you use `IHttpRemoteService` directly inside `GetAsync` to send the request, it will trigger the automatic management mechanism and fall into an infinite recursive loop. In this case, you need to explicitly disable `Access Token` automatic management for the current request by marking it with the `[SuppressTokenManagement]` attribute to avoid circular calls. ```cs showLineNumbers {1,6} public class CustomHttpAccessTokenProvider(IHttpBaseService httpBaseService): IHttpAccessTokenProvider { /// public async Task GetAsync(CancellationToken cancellationToken) { var serverToken = await httpBaseService.LoginAsync(new { username = "furion", password = "your-password"}); return new HttpAccessToken(serverToken.Token, serverToken.ExpiresAt) }; } ``` `HTTP` declarative requests disable the automatic `Access Token` management of the framework through the `SuppressTokenManagementAttribute` attribute. The corresponding `HTTP` declarative extractor implementation is the [`SuppressTokenManagementDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/DeclarativeExtractor.cs) type, which is responsible for parsing the `SuppressTokenManagementAttribute` attribute and building the configuration required by the `HttpRequestBuilder` instance to disable the automatic `Access Token` management of the framework. ```cs showLineNumbers {4} public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] [SuppressTokenManagement] // Skip Token management to avoid recursive calls Task LoginAsync(object auth); [Get("https://furion.net/")] Task> GetAsync(); // No marking required } ``` If you need to disable this feature for a specific request, set the `[JsonResponseStringUnwrap(false)]` attribute. > **Scope of the `SuppressTokenManagementAttribute` Attribute** The `SuppressTokenManagementAttribute` attribute applies only to methods. `SuppressTokenManagementAttribute` contains the following constructors and properties: None. --- # 5.35 Removing the Trailing / from the URL Address > Source: https://http.furion.net/en/docs/declarative/removing-the-trailing--from-the-url-address/ Some servers are sensitive to a trailing `/` in the path (such as `/api/` versus `/api`), which may cause a `301` redirect or route matching failure. When this feature is enabled, the framework automatically removes the trailing `/` from the path when constructing the final request address. `HTTP` declarative requests remove the trailing `/` from the `URL` address through the `RemoveTrailingSlashAttribute` attribute. The corresponding `HTTP` declarative extractor implementation is the [`RemoveTrailingSlashDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/RemoveTrailingSlashDeclarativeExtractor.cs) type, which is responsible for parsing the `RemoveTrailingSlashAttribute` attribute and building the configuration required by the `HttpRequestBuilder` instance to remove the trailing `/` from the `URL` address. ```cs showLineNumbers {2,6,10} // Applied at the interface definition, affecting all methods [RemoveTrailingSlash] public interface IHttpService : IHttpDeclarative { // Applied at the method level [RemoveTrailingSlash] [Get("https://furion.net/")] Task GetStringAsync(); [RemoveTrailingSlash(false)] // Disable this feature [Get("https://furion.net/")] Task GetStringAsync(); } ``` The request address becomes `https://furion.net`. > **Scope of the `RemoveTrailingSlashAttribute` Attribute** The `RemoveTrailingSlashAttribute` attribute applies to methods or interfaces. `RemoveTrailingSlashAttribute` contains the following constructors and properties: - **Constructors**: - `new()`: Applies to methods or interfaces, removing the trailing `/` from the `URL` address. - `new(enabled)`: Applies to methods or interfaces, setting whether the trailing `/` is removed from the `URL` address. - **Properties**: - `Enabled`: Whether it is enabled (`bool` type), defaulting to `true` (enabled). --- # 5.36 Setting the Request Interface Quota Key > Source: https://http.furion.net/en/docs/declarative/setting-the-request-interface-quota-key/ Specifies a quota key for the current request, used to associate it with the quota limit rules configured in `HttpClientOptions`. `HTTP` declarative requests set the request interface quota key through the `QuotaKeyAttribute` attribute. The corresponding `HTTP` declarative extractor implementation is the [`QuotaKeyDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/QuotaKeyDeclarativeExtractor.cs) type, which is responsible for parsing the `QuotaKeyAttribute` attribute and building the request interface quota key configuration required by the `HttpRequestBuilder` instance. ```cs showLineNumbers {2,9} // Applied at the interface definition, affecting all methods [QuotaKey("weixin")] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStringAsync(); // Applied at the method level [QuotaKey("qq")] [Get("https://furion.net/")] Task GetStringAsync(); } ``` > **Notes on the `QuotaKey` Configuration Key** - If no quota key is specified, or if the specified key does not exist in `QuotaLimits`, no quota check is performed and the request is sent normally. - The quota key can be any custom string; using a name related to the interface path is recommended for easier identification and management. > **Scope of the `QuotaKeyAttribute` Attribute** The `QuotaKeyAttribute` attribute applies to methods or interfaces. `QuotaKeyAttribute` contains the following constructors and properties: - **Constructors**: - `new(key)`: Applies to methods or interfaces, setting the request interface quota key. - **Properties**: - `Key`: The quota key (of type `string`). --- # 5.37 Enabling ETag Response Caching > Source: https://http.furion.net/en/docs/declarative/enabling-etag-response-caching/ `ETag` (entity tag) is a mechanism in the `HTTP` protocol used to identify the version of a resource. The server returns the resource's `ETag` value in the response headers (for example, `"abc123"`), and the client can carry that value via the `If-None-Match` header in subsequent requests. If the resource has not changed, the server returns `304 Not Modified` without retransmitting the content; otherwise it returns the new content along with a new `ETag`. After enabling `ETag` caching, the framework handles this process automatically: the first request caches the response and the `ETag`, and subsequent requests automatically attach `If-None-Match`; when a `304` status code is received, the cached content is reused directly, reducing data transfer and improving request efficiency. In weak network environments or mobile data-metering scenarios, this mechanism can significantly reduce bandwidth consumption while speeding up response times. `HTTP` declarative requests enable `ETag` response caching through the `UseETagAttribute` attribute. The corresponding `HTTP` declarative extractor is implemented as the [`UseETagDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/UseETagDeclarativeExtractor.cs) type, which is responsible for parsing the `UseETagAttribute` attribute and building the `ETag` response caching configuration needed for the `HttpRequestBuilder` instance. ```cs showLineNumbers {2,6,10} // Applied on the interface definition, affecting all methods [UseETag] public interface IHttpService : IHttpDeclarative { // Applied on a method [UseETag] [Get("https://furion.net/")] Task GetStringAsync(); [UseETag(false)] // Disable this feature [Get("https://furion.net/")] Task GetStringAsync(); } ``` > **`ETag` Response Caching Notes** - Only takes effect for `GET` and `HEAD` requests. - If the request explicitly calls `DisableCache()`, the `ETag` feature is automatically skipped. - The cache is stored in memory by default, and can be replaced with a distributed cache (such as `Redis`) by implementing the `IHttpETagCache` interface. - The default in-memory cache does not limit the number of cache entries or the size of a single response; a large number of unique `URL`s or large responses may cause memory to grow continuously. To avoid this, it is recommended to implement the `IHttpETagCache` interface and replace the default implementation, for example: ```cs showLineNumbers services.Replace(ServiceDescriptor.Singleton()); ``` - If the global request profiler (`AddProfilerDelegatingHandler`) is also enabled and you find that the response content is not printed, explicitly call the `Profiler()` method on the request to resolve it. > **`UseETagAttribute` Attribute Scope** The `UseETagAttribute` attribute applies to methods or interfaces. `UseETagAttribute` contains the following constructors and properties: - **Constructors**: - `new()`: applies to methods or interfaces, enabling `ETag` response caching. - `new(enabled)`: applies to methods or interfaces, setting whether to enable `ETag` response caching. - **Properties**: - `Enabled`: whether to enable (type `bool`), with a default value of `true` (enabled). --- # 5.38 Inheritance and Reuse > Source: https://http.furion.net/en/docs/declarative/inheritance-and-reuse/ `HTTP` declarative requests support object-oriented features such as encapsulation and inheritance. Common interfaces can be defined in parent interfaces and then inherited by derived interfaces to achieve reuse. For example: ```cs showLineNumbers {1,7} public interface IHttpBaseService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStringAsync(); } public interface IHttp1 : IHttpBaseService { [Get("https://baiqian.com/")] Task GetWebsiteAsync(); } ``` Additionally, inheritance from ordinary interfaces that do not implement the `IHttpDeclarative` interface is also supported, for example: ```cs showLineNumbers {1,7} public interface IHttpNormal { [Get("https://baiqian.com/")] Task GetBaiduAsync(); } public interface IHttp2 : IHttpNormal, IHttpBaseService { // ... } ``` With encapsulation and inheritance, code can be organized and reused more efficiently. --- # 5.39 Frozen Parameter Types > Source: https://http.furion.net/en/docs/declarative/frozen-parameter-types/ In the previous chapters, we mentioned frozen parameter types several times, and now we can finally discuss them in depth. In the system, `Action`, `Action`, `Action`, `HttpCompletionOption`, and `CancellationToken` are defined as frozen parameter types. They specifically serve `HTTP` declarative request interfaces to provide additional configuration and operation capabilities. These frozen parameter types can greatly extend the functionality of `HTTP` declarative request interfaces, enabling them to cover a wider range of use cases. - **`Action`** This parameter allows developers to apply additional configuration to the `HttpRequestMessage` sent by `HttpClient` when calling an `HTTP` declarative interface. For example, adding custom `HTTP` headers, setting authentication information, and so on. The corresponding `HTTP` declarative extractor is implemented as the [`HttpRequestMessageDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/HttpRequestMessageDeclarativeExtractor.cs) type, which is responsible for parsing a **single** `Action` parameter and providing operations before the request is sent. ```cs showLineNumbers {4} public interface IHttpService : IHttpDeclarative { [Post("https://furion.net/")] Task PostStringAsync([QueryParam] int id, [Body("application/json")] object body, Action? configure = null) } ``` ```cs showLineNumbers {5-8} // Default call await httpService.PostStringAsync(1, new { id = 1, name = "Furion" }); // Provide more HttpRequestMessage configuration await httpService.PostStringAsync(1, new { id = 1, name = "Furion" }, requestMessage => { requestMessage.Headers.TryAddWithoutValidation("header1", "value1"); // For example, add a request header named "header1" }); ``` - **`Action`** This parameter allows developers to apply additional configuration to the `HttpRequestBuilder` when calling an `HTTP` declarative interface. For example, adding custom `HTTP` headers, setting authentication information, and so on. The corresponding `HTTP` declarative extractor is implemented as the [`HttpRequestBuilderDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/HttpRequestBuilderDeclarativeExtractor.cs) type, which is responsible for parsing a **single** `Action` parameter and providing additional configuration for building the `HttpRequestBuilder` instance. ```cs showLineNumbers {4} public interface IHttpService : IHttpDeclarative { [Post("https://furion.net/")] Task PostStringAsync([QueryParam] int id, [Body("application/json")] object body, Action? configure = null) } ``` ```cs showLineNumbers {5-8} // Default call await httpService.PostStringAsync(1, new { id = 1, name = "Furion" }); // Provide more HttpRequestBuilder configuration await httpService.PostStringAsync(1, new { id = 1, name = "Furion" }, builder => { builder.AddBearerAuthentication("your-token"); // For example, add Bearer authorization }); ``` - **`Action`** This parameter is used to configure the settings of multipart form data. Through it, developers can add files, set file types, and so on. The corresponding `HTTP` declarative extractor is implemented as the [`HttpMultipartFormDataBuilderDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/HttpMultipartFormDataBuilderDeclarativeExtractor.cs) type, which is responsible for parsing a **single** `Action` parameter and providing additional configuration for building the `HttpMultipartFormDataBuilder` multipart form instance. ```cs showLineNumbers {4} public interface IHttpService : IHttpDeclarative { [Post("https://furion.net/")] Task PostStringAsync([Multipart] string name, Action? configure = null); } ``` ```cs showLineNumbers {5-10} // Default call await httpService.PostStringAsync("Furion"); // Provide more multipart form content configuration await httpService.PostStringAsync("Furion", multipart => { multipart.AddFileAsStream(@"C:\Workspaces\httptest.jpg", "files", contentType: "image/jpeg"); multipart.AddFileFromBase64String("77u/5rWL6K+V5paH5Lu25YaF5a65", "files"); multipart.AddFileFromRemote("https://furion.net/img/furionlogo.png", "files"); }); ``` - **`HttpCompletionOption`** This parameter is used to specify how the `HTTP` response is read. For example, whether to wait until the entire response content has been read before returning, or to return after reading only the response headers. ```cs showLineNumbers {4} public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStreamAsync([QueryParam] string version, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead); } ``` ```cs showLineNumbers {5} // Default call await httpService.GetStreamAsync("v4"); // Customize the response reading method await httpService.GetStreamAsync("v5", HttpCompletionOption.ResponseHeadersRead); ``` - **`CancellationToken`** This parameter allows developers to provide cancellation configuration when sending an `HTTP` request. Through it, the request can be set to be canceled under specific conditions. ```cs showLineNumbers public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStreamAsync(CancellationToken cancellationToken = default); } ``` ```cs showLineNumbers {5-6,8} // Default call (cannot be canceled) await httpService.GetStreamAsync("v4"); // Set the request to be canceled after 100 milliseconds using var cancellationTokenSource = new CancellationTokenSource(); cancellationTokenSource.CancelAfter(100); await httpService.GetStreamAsync("v5", cancellationTokenSource.Token); // Assume this request takes longer than 100 milliseconds ``` It is worth noting that these frozen parameter types can be combined, and are usually (recommended to be) placed at the end of the method parameter list as optional configuration. **However, within the same method parameter definition, frozen parameters of the same type must be unique**, otherwise an `InvalidOperationException` is thrown. ```cs showLineNumbers {6-9,14-15} public interface IHttpService : IHttpDeclarative { // Combined usage is supported [Post("https://furion.net/")] Task PostStringAsync([QueryParam] int id, [Body("application/json")] object body, Action? multipartConfigure = null, Action? configure = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default); // Action type parameters are not unique, an exception will be thrown ❎ [Post("https://furion.net/")] Task PostStringAsync([QueryParam] int id, [Body("application/json")] object body, Action? configure = null, Action? configure1 = null); } ``` > **Frozen Parameter Type Execution Order** To ensure that these frozen parameter types execute in the expected order, they all implement the `IFrozenHttpDeclarativeExtractor` interface, which contains an `Order` property used to indicate the execution order. Their execution order is: `Action` -> `Action` -> `Action` -> `HttpCompletionOption` -> `CancellationToken`. Through these frozen parameter types, `HTTP` declarative request interfaces not only greatly reduce the burden on developers writing HTTP request code, but also make the code structure clearer and easier to maintain and reuse. --- # 5.40 Getting the Request Builder or Request Message (Pre-flight Request) > Source: https://http.furion.net/en/docs/declarative/getting-the-request-builder-or-request-message-pre-flight-request/ In some cases, you may want to **only obtain the `HTTP` request object itself without actually sending the request**. For example: verifying in unit tests that the generated request matches expectations, or further manually modifying the request builder after obtaining it, or passing the request message to another system for execution. To this end, when the return type of a method that sends an `HTTP` remote request is `HttpRequestBuilder` or `HttpRequestMessage`, the framework builds and returns that object directly, **skipping the actual network transmission**. The example is as follows: ```cs showLineNumbers {5,9} public interface IHttpService : IHttpDeclarative { // HttpRequestBuilder type, no request is sent (pre-flight request) [Get("https://furion.net/")] Task GetRequestBuilderAsync(); // HttpRequestMessage type, no request is sent (pre-flight request) [Get("https://furion.net/")] Task GetRequestMessageAsync(); } ``` When called, the object is obtained directly: ```cs showLineNumbers {2,4,7-8} // Get the builder; you can continue chained configuration and then send manually var builder = await httpService.GetRequestBuilderAsync(); // No request is sent builder.WithHeader("X-Custom", "value"); var httpResponseMessage = await httpRemoteService.SendAsync(builder); // Initiate the network request // Get HttpRequestMessage for assertions or external passing var httpRequestMessage = await httpService.GetRequestMessageAsync(); // No request is sent Assert.Equal("https://furion.net/", httpRequestMessage.RequestUri?.ToString()); ``` ### Use Cases - **Pre-flight Check**: Before formally sending, check whether the generated request object matches expectations. After confirming that the `URL`, request headers, `Token` injection, and so on are all correct, send it manually or continue processing. - **Unit Testing**: Without simulating a network environment, directly verify whether the generated `HttpRequestMessage` contains the correct parameters, headers, and authentication information. - **Request Object Passing**: Pass the constructed `HttpRequestMessage` to other services, libraries, or processes for execution, achieving separation between request construction and request execution. - **Hybrid Programming**: First complete most of the configuration through the builder or declarative style (parameter mapping, `Token` injection, etc.), then obtain the builder for minor dynamic modifications before sending manually, balancing the simplicity of the declarative style with the flexibility of the imperative style. > **Note** - Methods that return `HttpRequestBuilder` or `HttpRequestMessage` **do not initiate network requests**; the framework only completes the construction in memory. - If a method returns another type (such as `string`, `HttpResponseMessage`, etc.), the framework sends the request normally and returns the corresponding result. - This feature is complementary to [frozen parameter types](/en/docs/declarative/frozen-parameter-types/) (such as `Action`), which inject configuration before sending but cannot prevent sending. --- # 5.41 Custom HTTP Declarative Extractor > Source: https://http.furion.net/en/docs/declarative/custom-http-declarative-extractor/ In the `5.1 Declarative Requests` section, we learned that every attribute or parameter type corresponds to an `HTTP` declarative extractor. The following are the system-provided attribute extractors and their corresponding implementations: - `BaseAddressAttribute` attribute extractor: [`BaseAddressDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/BaseAddressDeclarativeExtractor.cs) - `ValidationAttribute` attribute extractor: [`ValidationDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/ValidationDeclarativeExtractor.cs) - `AutoSetHostHeaderAttribute` attribute extractor: [`AutoSetHostHeaderDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/AutoSetHostHeaderDeclarativeExtractor.cs) - `StandardRequestHeadersAttribute` attribute extractor: [`StandardRequestHeadersDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/StandardRequestHeadersDeclarativeExtractor.cs) - `HttpClientNameAttribute` attribute extractor: [`HttpClientNameDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/HttpClientNameDeclarativeExtractor.cs) - `TraceIdentifierAttribute` attribute extractor: [`TraceIdentifierDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/TraceIdentifierDeclarativeExtractor.cs) - `ProfilerAttribute` attribute extractor: [`ProfilerDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/ProfilerDeclarativeExtractor.cs) - `SimulateBrowserAttribute` attribute extractor: [`SimulateBrowserDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/SimulateBrowserDeclarativeExtractor.cs) - `AcceptLanguageAttribute` attribute extractor: [`AcceptLanguageDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/AcceptLanguageDeclarativeExtractor.cs) - `DisableCacheAttribute` attribute extractor: [`DisableDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/DisableDeclarativeExtractor.cs) - `EnsureSuccessStatusCodeAttribute` attribute extractor: [`EnsureSuccessStatusCodeDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/EnsureSuccessStatusCodeDeclarativeExtractor.cs) - `RetryAttribute` attribute extractor: [`RetryDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/RetryDeclarativeExtractor.cs) - `TimeoutAttribute` attribute extractor: [`TimeoutDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/TimeoutDeclarativeExtractor.cs) - `PathSegmentAttribute` attribute extractor: [`PathSegmentDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/PathSegmentDeclarativeExtractor.cs) - `QueryParamAttribute` attribute extractor: [`QueryParamDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/QueryParamDeclarativeExtractor.cs) - `QuotaKeyAttribute` attribute extractor: [`QuotaKeyDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/QuotaKeyDeclarativeExtractor.cs) - `PathAttribute` attribute extractor: [`PathDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/PathDeclarativeExtractor.cs) - `CookieAttribute` attribute extractor: [`CookieDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/CookieDeclarativeExtractor.cs) - `RefererAttribute` attribute extractor: [`RefererDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/RefererDeclarativeExtractor.cs) - `HeaderAttribute` attribute extractor: [`HeaderDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/HeaderDeclarativeExtractor.cs) - `PropertyAttribute` attribute extractor: [`PropertyDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/PropertyDeclarativeExtractor.cs) - `HttpVersionAttribute` attribute extractor: [`HttpVersionDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/HttpVersionDeclarativeExtractor.cs) - `SuppressExceptionsAttribute` attribute extractor: [`SuppressExceptionsDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/SuppressExceptionsDeclarativeExtractor.cs) - `RemoveTrailingSlashAttribute` attribute extractor: [`RemoveTrailingSlashDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/RemoveTrailingSlashDeclarativeExtractor.cs) - `RequestEventHandlerAttribute` attribute extractor: [`RequestEventHandlerDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/RequestEventHandlerDeclarativeExtractor.cs) - `JsonResponseWrapperAttribute` attribute extractor: [`JsonResponseWrapperDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/JsonResponseWrapperDeclarativeExtractor.cs) - `JsonResponseStringUnwrapAttribute` attribute extractor: [`JsonResponseStringUnwrapDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/JsonResponseStringUnwrapDeclarativeExtractor.cs) - `SuppressTokenManagementAttribute` attribute extractor: [`SuppressTokenManagementDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/SuppressTokenManagementDeclarativeExtractor.cs) - `UseETagAttribute` attribute extractor: [`UseETagDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/UseETagDeclarativeExtractor.cs) - `BodyAttribute` attribute extractor: [`BodyDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/BodyDeclarativeExtractor.cs) - `MultipartAttribute` and `MultipartFormAttribute` attribute extractors: [`MultipartDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/MultipartDeclarativeExtractor.cs) - **`Action`** parameter extractor: [`HttpRequestMessageDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/HttpRequestMessageDeclarativeExtractor.cs) - **`Action`** parameter extractor: [`HttpMultipartFormDataBuilderDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/HttpMultipartFormDataBuilderDeclarativeExtractor.cs) - **`Action`** parameter extractor: [`HttpRequestBuilderDeclarativeExtractor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/HttpRequestBuilderDeclarativeExtractor.cs) By customizing an `HTTP` declarative extractor, you can provide additional functionality for `HTTP` declarative interfaces. The following is an example of a custom `AcceptAttribute` attribute and its extractor: **1. Defining the `AcceptAttribute` attribute** Set the `AcceptAttribute` attribute's scope to methods or interfaces. ```cs showLineNumbers {1-2} [AttributeUsage(AttributeTargets.Method | AttributeTargets.Interface)] public sealed class AcceptAttribute : Attribute { public AcceptAttribute(string accept) { ArgumentException.ThrowIfNullOrWhiteSpace(accept); Accept = accept; } public string Accept { get; set; } } ``` **2. Implementing the `AcceptDeclarativeExtractor` extractor** Parse the `AcceptAttribute` attribute and set it on the `HttpRequestBuilder` instance. ```cs showLineNumbers {1,4,7,13} public sealed class AcceptDeclarativeExtractor : IHttpDeclarativeExtractor { // Implement the Extract method public void Extract(HttpRequestBuilder httpRequestBuilder, HttpDeclarativeParsingContext context) { // Get the AcceptAttribute attribute defined on the method or interface if (!context.IsMethodDefined(out var acceptAttribute, true)) { return; } // Set the Accept header httpRequestBuilder.WithHeader("Accept", acceptAttribute.Accept, replace: true); } } ``` The `context` parameter of the extractor's `Extract` method is of type `HttpDeclarativeParsingContext`, which contains the following properties and methods: - **Properties**: - `Method`: The invoked method (of type `MethodInfo`). - `Args`: The array of argument values of the invoked method (of type `object[]`). - `MethodMetadata`: The metadata of the invoked method (of type `HttpDeclarativeMethodMetadata`). - `Parameters`: The dictionary of parameter keys and values of the invoked method (of type `IReadOnlyDictionary`). - `UnFrozenParameters`: The dictionary of **non-frozen** typed parameter keys and values of the invoked method (of type `IReadOnlyDictionary`). - **Methods**: - `IsFrozenParameter(parameter)`: Determines whether a parameter is a frozen parameter type. - `IsMethodDefined(out var attribute, inherit)`: Checks whether the invoked method defines the specified attribute. - `GetMethodDefinedCustomAttributes(inherit, methodScanFirst)`: Gets all instances of the specified attribute defined on the invoked method. **3. Registering the custom extractor in configuration** In the `Startup.cs` or `Program.cs` file, configure and register the `HttpRemote` service to enable the custom `HTTP` declarative extractor functionality. ```cs showLineNumbers {1,3} services.AddHttpRemote(builder => { builder.AddHttpDeclarativeExtractors(() => [ new AcceptDeclarativeExtractor() ]); }); ``` **4. Using the custom attribute in an `HTTP` declarative interface** ```cs showLineNumbers {1,5} [Accept("text/html")] public interface IHttpService : IHttpDeclarative { // Apply on the method [Accept("text/xml")] [Get("https://furion.net/")] Task GetStringAsync(); } ``` > **Tip** When a custom attribute is allowed to be used on parameters, make sure to exclude frozen-type parameters via the `HttpDeclarativeParsingContext.IsFrozenParameter(parameter)` method. The example code is as follows: ```cs showLineNumbers {2,6} // Return non-frozen typed parameter key-value pairs via the UnFrozenParameters property context.UnFrozenParameters; // Manually determine via the HttpDeclarativeParsingContext.IsFrozenParameter static method var parameters = context.Parameters.Where(param => !HttpDeclarativeParsingContext.IsFrozenParameter(param.Key) && // Filter out frozen-type parameters param.Key.IsDefined(typeof(YourAttribute), true)).ToArray(); ``` This code snippet shows how to filter out an array of parameters that do not contain frozen parameters and are marked with a specific attribute. Through the steps above, you have successfully created a custom `HTTP` declarative extractor. This not only enhances the functionality of `HTTP` declarative interfaces but also makes the code more concise and easier to maintain. You can continue to extend and customize other `HTTP` declarative extractors according to your needs. For more custom `HTTP` declarative extractors, refer to the framework's built-in `HTTP` declarative extractor code implementations. --- # 5.42 Custom HTTP Declarative Extractor (Authorization) > Source: https://http.furion.net/en/docs/declarative/custom-http-declarative-extractor-authorization/ The following is an example that shows how to implement automatic authorization and anonymous access by customizing the `AuthenticationAttribute` and `AllowAnonymousAttribute` attributes and adding the corresponding extractors. **1. Defining the `AuthenticationAttribute` attribute** Apply the `AuthenticationAttribute` attribute to methods or interfaces. ```cs showLineNumbers {1-2} [AttributeUsage(AttributeTargets.Method | AttributeTargets.Interface)] public class AuthenticationAttribute : Attribute; ``` **2. Implementing the `AuthenticationDeclarativeExtractor` and `AllowAnonymousDeclarativeExtractor` extractors** ```cs showLineNumbers {4,10,13,16,24,30,33} /// /// [Authentication] attribute extractor /// public class AuthenticationDeclarativeExtractor : IHttpDeclarativeExtractor { /// public void Extract(HttpRequestBuilder httpRequestBuilder, HttpDeclarativeParsingContext context) { // Skip if the [AllowAnonymous] attribute is applied if (context.IsMethodDefined(out _, true)) return; // Check whether authorization information has already been set if (httpRequestBuilder.AuthenticationHeader is not null) return; // Add the authorization header (any authorization logic can be implemented here, such as getting a token from a parameter, etc.) httpRequestBuilder.AddBearerAuthentication( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"); } } /// /// [AllowAnonymous] attribute extractor /// public class AllowAnonymousDeclarativeExtractor : IHttpDeclarativeExtractor { /// public void Extract(HttpRequestBuilder httpRequestBuilder, HttpDeclarativeParsingContext context) { // Skip if the [AllowAnonymous] attribute is not applied if (!context.IsMethodDefined(out _, true)) return; // Remove the authorization header httpRequestBuilder.RemoveHeaders("Authorization"); } } ``` **3. Registering the custom extractors in configuration** In the `Startup.cs` or `Program.cs` file, configure and register the `HttpRemote` service to enable the custom `HTTP` declarative extractor functionality. ```cs showLineNumbers {1,4,7} services.AddHttpRemote(builder => { // Add custom HTTP declarative extractors builder.AddHttpDeclarativeExtractors(() => [ new AuthenticationDeclarativeExtractor(), new AllowAnonymousDeclarativeExtractor() ]); // Scan assemblies to add HTTP declarative extractors in bulk (recommended) // builder.AddHttpDeclarativeExtractorsFromAssemblies([ assembly1, assembly2, ... ]); // When using the Furion framework, you can directly set App.Assemblies }); ``` **4. Using the custom attributes in an `HTTP` declarative interface** ```cs showLineNumbers {1,7} [Authentication] // Add global authorization public interface IAuthService : IHttpDeclarative { [Get("https://furion.net/")] Task GetDataAsync(); // Accessing this interface requires authorization [AllowAnonymous] // Anonymous access [Get("https://furion.net/")] Task LoginAsync(string username, string password); } ``` When the `GetDataAsync` method is called, the authorization header is automatically added (implementing authorization). When the `LoginAsync` method is called, the authorization request header is automatically removed (implementing anonymous access). As this example shows, custom `HTTP` declarative extractors provide great flexibility for implementing complex authorization logic. --- # 5.43 The HttpDeclarativeBuilder Builder (Dynamic Building) > Source: https://http.furion.net/en/docs/declarative/declarative-builder/ The `HttpDeclarativeBuilder` builder is provided by the framework specifically for dynamically building the various settings required by `HTTP` declarative requests. The constructor of `HttpDeclarativeBuilder` is private, so it cannot be instantiated directly with the `new` keyword; however, the framework provides multiple static overloads of `HttpRequestBuilder.Declarative` to create `HttpDeclarativeBuilder` instances. ```cs showLineNumbers HttpRequestBuilder.Declarative(methodInfo, args); ``` The code above demonstrates how to dynamically build an `HTTP` declarative request builder using a `MethodInfo` typed parameter and an argument array. This mechanism enables us to implement `HTTP` declarative request functionality for methods of any type. The following is a concrete example: ```cs showLineNumbers {1,3,6} public class NormalClass { [Get("https://furion.net"/)] public Task GetStringAsync() { throw new NotImplementedException(); // No implementation needed } } ``` Through the following steps, we can dynamically build an `HTTP` declarative request based on the `GetStringAsync` method of `NormalClass`: ```cs showLineNumbers {2,5} // Get the GetStringAsync method of the NormalClass type var getStringMethod = typeof(NormalClass).GetMethod(nameof(NormalClass.GetStringAsync), BindingFlags.Instance | BindingFlags.Public); // Send the HTTP request var str = await httpRemoteService.SendAsAsync(HttpRequestBuilder.Declarative(getStringMethod, [])); ``` In this way, we implement the dynamic building of `HTTP` declarative requests for methods of any type. In addition, `HTTP` declarative requests support a variety of methods, including but not limited to: ```cs showLineNumbers {1-2,4-5} httpRemoteService.Declarative(method, args); await httpRemoteService.DeclarativeAsync(method, args); httpRemoteService.SendAs(httpDeclarativeBuilder); await httpRemoteService.SendAsAsync(httpDeclarativeBuilder); ``` --- # 6.1 IHttpContentProcessor Content Processor > Source: https://http.furion.net/en/docs/advanced-guide/ihttpcontentprocessor-content-processor/ `IHttpContentProcessor` is used to build an `HttpContent` instance based on the raw request content and type set by the user, and to set it as the `Content` property of the `HttpRequestMessage` object. As shown in the following diagram: ![httpagent](/images/httpagent.jpg) [**View the High-Resolution Architecture Diagram**](https://github.com/monksoul/HttpAgent/blob/master/drawio/HttpAgent.drawio) --- # 6.2 Built-in Content Processors > Source: https://http.furion.net/en/docs/advanced-guide/built-in-content-processors/ - **`StringContentProcessor` Content Processor** When the raw request content satisfies the following conditions, [`StringContentProcessor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Processors/StringContentProcessor.cs) is used to build a `StringContent` instance: - The raw request content is `StringContent` or `JsonContent`. - The content type includes `application/json`, `application/json-patch+json`, `application/xml`, `application/xml-patch+xml`, `text/xml`, `text/html`, `text/plain`, and `application/soap+xml`, and it still applies even when a `charset` character set is appended to these types (for example `application/json; charset=utf-8`). > **About the Default `JSON` Serialization Configuration** The `StringContentProcessor` content processor uses the `JsonSerializerOptions.Web` configuration by default, which provides a set of default serialization settings suitable for Web scenarios. For detailed information about this configuration, refer to the official documentation: [`Web` defaults for `JsonSerializerOptions`](https://learn.microsoft.com/zh-cn/dotnet/standard/serialization/system-text-json/configure-options#web-defaults-for-jsonserializeroptions). To customize these `JSON` serialization options, you can adjust them in the following ways: ```cs showLineNumbers {2-3,6,10-11,14} // Global configuration (applies to all clients) services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { // Customize JSON serialization behavior, e.g. ignore null values options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); // Client-level configuration (higher priority) services.AddHttpClient("client-name") .ConfigureOptions(options => { // Customize JSON serialization behavior, e.g. ignore null values options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); ``` In the code above, through the `ConfigureOptions` method, you can flexibly adjust the various settings of `JsonSerializerOptions` to meet specific serialization requirements. --- - **`StreamContentProcessor` Content Processor** When the raw request content satisfies the following conditions, [`StreamContentProcessor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Processors/StreamContentProcessor.cs) is used to build a `StreamContent` instance: - The raw request content is `StreamContent` or `Stream`. --- - **`ByteArrayContentProcessor` Content Processor** When the raw request content satisfies the following conditions, [`ByteArrayContentProcessor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Processors/ByteArrayContentProcessor.cs) is used to build a `ByteArrayContent` instance: - The raw request content is `ByteArrayContent` or `byte[]`, and is not `FormUrlEncodedContent` or `StringContent`. --- - **`FormUrlEncodedContentProcessor` Content Processor** When the raw request content satisfies the following conditions, [`FormUrlEncodedContentProcessor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Processors/FormUrlEncodedContentProcessor.cs) is used to build a `FormUrlEncodedContent` instance: - The raw request content is `FormUrlEncodedContent` or a URL-encoded string. - The content type is `application/x-www-form-urlencoded`, and it still applies even when a `charset` character set is appended to the type (for example `application/x-www-form-urlencoded; charset=utf-8`). --- - **`StringContentForFormUrlEncodedContentProcessor` Content Processor** When the raw request content satisfies the following conditions, [`StringContentForFormUrlEncodedContentProcessor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Processors/StringContentForFormUrlEncodedContentProcessor.cs) is used to build a `StringContent` instance with content type `application/x-www-form-urlencoded`: - The raw request content is `FormUrlEncodedContent` or a URL-encoded string. - The content type is `application/x-www-form-urlencoded`, and it still applies even when a `charset` character set is appended to the type (for example `application/x-www-form-urlencoded; charset=utf-8`). - **The `useStringContent` parameter or the `UseStringContent` property is `true`.** `StringContentForFormUrlEncodedContentProcessor` derives from `FormUrlEncodedContentProcessor`. --- - **`ReadOnlyMemoryContentProcessor` Content Processor** When the raw request content satisfies the following conditions, [`ReadOnlyMemoryContentProcessor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Processors/ReadOnlyMemoryContentProcessor.cs) is used to build a `ReadOnlyMemoryContent` instance: - The raw request content is `ReadOnlyMemoryContent` or `ReadOnlyMemory`. --- - **`MultipartFormDataContentProcessor` Content Processor** When the raw request content satisfies the following conditions, [`MultipartFormDataContentProcessor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Processors/MultipartFormDataContentProcessor.cs) is used to build a `MultipartFormDataContent` instance: - The raw request content is `MultipartFormDataContent`. - The content type is `multipart/form-data`, and it still applies even when a `charset` character set is appended to the type (for example `multipart/form-data; charset=utf-8`). --- - **`MessagePackContentProcessor` Content Processor** When the raw request content satisfies the following conditions, [`MessagePackContentProcessor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Processors/MessagePackContentProcessor.cs) is used to build a `ByteArrayContent` instance: - The content type is `application/msgpack`, and it still applies even when a `charset` character set is appended to the type (for example `application/msgpack; charset=utf-8`). --- - **`JsonLinesContentProcessor` Content Processor** When the raw request content satisfies the following conditions, [`JsonLinesContentProcessor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Processors/JsonLinesContentProcessor.cs) is used to build a `StringContent` instance: - The content type includes `application/x-ndjson`, `application/x-jsonlines`, `application/jsonlines`, and `application/jsonl`, and it still applies even when a `charset` character set is appended to the type (for example `application/x-ndjson; charset=utf-8`). - **`FileInfoContentProcessor` Content Processor** When the raw request content satisfies the following conditions, [`FileInfoContentProcessor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Processors/FileInfoContentProcessor.cs) is used to build a `StreamContent` instance: - The raw request content is `FileInfo`. --- - **`FormFileContentProcessor` Content Processor** When the raw request content satisfies the following conditions, [`FormFileContentProcessor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent.AspNetCore/src/Processors/FormFileContentProcessor.cs) is used to build a `StreamContent` instance: - The raw request content is `IFormFile`. --- --- # 6.3 IHttpContentProcessorFactory Content Processor Factory > Source: https://http.furion.net/en/docs/advanced-guide/ihttpcontentprocessorfactory-content-processor-factory/ The `IHttpContentProcessorFactory` content processor factory is responsible for determining the appropriate `IHttpContentProcessor` content processor based on the content and type of the raw request, and calling its `Process` method to generate an `HttpContent` instance. The factory service is configured as a singleton to ensure its uniqueness and stability throughout the application lifecycle. If no suitable `IHttpContentProcessor` content processor is found, an `InvalidOperationException` is thrown, with the following exception message: ```bash showLineNumbers No processor found that can handle the content type `application/pdf` and the provided raw content of type `System.Span`1[T]`. Please ensure that the correct content type is specified and that a suitable processor is registered. ``` The following is an example combining the `IHttpContentProcessorFactory` content processor factory with `HttpClient`, showing how to easily build the appropriate `HttpContent` content via its `Build` method. As explained in the previous sections, when the content type is `application/json`, the `StringContentProcessor` processor is used and a `StringContent` instance is generated. ```cs showLineNumbers {1,8} public class YourService(IHttpContentProcessorFactory httpContentProcessorFactory) // .NET8+ supports primary constructor injection { public async Task GetStringAsync() { var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, "https://furion.net/"); // Call the Build method to build an HttpContent instance; the concrete instance type is StringContent var httpContent = httpContentProcessorFactory.Build(new HttpContentProcessorContext(new { id = 1, name = "Furion" }, "application/json")); httpRequestMessage.Content = httpContent; using var httpClient = new HttpClient(); var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage); return await httpResponseMessage.Content.ReadAsStringAsync(); } } ``` > **About Looking Up the `IHttpContentProcessor` Content Processor** The `IHttpContentProcessorFactory` factory searches for a suitable `IHttpContentProcessor` starting from the most recently added content processor. Once the `CanProcess` method of a content processor returns `true`, it means a matching content processor has been found, and that processor is then used to build the `HttpContent`. --- # 6.4 Custom Content Processor (e.g. Serialization) > Source: https://http.furion.net/en/docs/advanced-guide/custom-content-processor-eg-serialization/ In specific scenarios, when the framework's built-in `IHttpContentProcessor` content processors cannot meet your needs, you can solve the problem by implementing a custom `IHttpContentProcessor` content processor. If you want to replace the framework's default `System.Text.Json` serialization provider, for example by using `Newtonsoft.Json` to add specific serialization configuration options for the `application/json` content type, you can implement the `IHttpContentProcessor` interface to meet this custom requirement. > **Framework Recommendation** However, please note that unless there is a compelling reason, it is generally recommended to use `System.Text.Json`, because it is tightly integrated with `.NET Core` and offers excellent performance. To customize these `JSON` serialization options, you can adjust them in the following ways: ```cs showLineNumbers {2-3,6,10-11,14} // Global configuration (applies to all clients) services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { // Customize JSON serialization behavior, e.g. ignore null values options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); // Client-level configuration (higher priority) services.AddHttpClient("client-name") .ConfigureOptions(options => { // Customize JSON serialization behavior, e.g. ignore null values options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); ``` ```cs showLineNumbers {1,4,15} public class CustomStringContentProcessor : HttpContentProcessorBase { public override bool CanProcess(HttpContentProcessorContext context) => context.ContentType == "application/json"; public override HttpContent? Process(HttpContentProcessorContext context) { if (TryProcess(context, out var httpContent)) { return httpContent; } var content = context.RawContent!.GetType().IsBasicType() || context.RawContent is JsonElement or JsonNode ? context.RawContent.ToString() : context.RawContent.ToJsonString(ResolveJsonSerializerOptions(context.HttpClientName)); var stringContent = new StringContent(content!, context.Encoding, new MediaTypeHeaderValue(context.ContentType) { CharSet = context.Encoding?.WebName ?? "utf-8" }); return stringContent; } } ``` Next, you can apply the custom content processor in the following two ways: - **Per-request setting**: ```cs showLineNumbers {2} HttpRequestBuilder.Post("https://furion.net/") .AddHttpContentProcessors(() => [ new CustomStringContentProcessor() ]) .SetJsonContent(new { id = 1, name = "Furion" }); ``` - **Global configuration**: In the `Startup.cs` or `Program.cs` file, configure and register the `HttpRemote` service to enable the custom content processor feature: ```cs showLineNumbers {1,3} services.AddHttpRemote(builder => { builder.AddHttpContentProcessors(() => [ new CustomStringContentProcessor() ]); }); ``` > **About the `HttpContentProcessorBase` Base Class** The `HttpContentProcessorBase` base class provides a built-in `ServiceProvider` property, which allows you to easily resolve and obtain services registered via dependency injection (`DI`). > **Extended Knowledge About `IHttpContentProcessor`** To learn more about custom `IHttpContentProcessor` content processors, visit the [official `HttpAgent` repository](https://github.com/monksoul/HttpAgent/tree/master/src/HttpAgent/src/Processors) for reference. --- # 6.5 Adding MessagePack Support > Source: https://http.furion.net/en/docs/advanced-guide/adding-messagepack-support/ `MessagePack` is a compact, efficient binary serialization format designed for data exchange across multiple languages. Compared with `JSON`, `MessagePack` offers higher performance and a smaller data footprint. Although it is a binary format, `MessagePack` was designed with cross-language convenience in mind, and it is now widely used in many programming languages such as `Python`, `Ruby`, `JavaScript`, `C++`, and `C#`. To enable `MessagePack` support in your project, follow these steps: 1. **Install the `MessagePack` package**: ```bash showLineNumbers dotnet add package MessagePack ``` 2. **Add the `MessagePackContentProcessor` content processor**: ```cs showLineNumbers {1,5,17} public class MessagePackContentProcessor : HttpContentProcessorBase { /// public override bool CanProcess(HttpContentProcessorContext context) => context.ContentType == "application/msgpack"; /// public override HttpContent? Process(HttpContentProcessorContext context) { // Attempt to resolve the HttpContent type if (TryProcess(context, out var httpContent)) { return httpContent; } // Convert the raw request content to a byte array var content = context.RawContent as byte[] ?? MessagePackSerializer.Serialize(context.RawContent); // Initialize a ByteArrayContent instance var byteArrayContent = new ByteArrayContent(content); byteArrayContent.Headers.ContentType = new MediaTypeHeaderValue(context.ContentType) { CharSet = context.Encoding?.WebName }; return byteArrayContent; } } ``` 3. **Apply the `MessagePackContentProcessor` content processor**: - **Per-request configuration**: ```cs showLineNumbers {2-3} HttpRequestBuilder.Post("https://furion.net/") .AddHttpContentProcessors(() => [ new MessagePackContentProcessor() ]) .SetContent(new MessagePackModel { Id = 1, Name = "Furion" }, "application/msgpack"); ``` > **Note** To use `MessagePack` serialization, your model class must add the `MessagePackObject` attribute, and its properties must add the `MessagePack.Key` attribute. For detailed documentation, refer to the [MessagePack-CSharp official repository](https://github.com/MessagePack-CSharp/MessagePack-CSharp). ```cs showLineNumbers {1,4,7} [MessagePackObject] public class MessagePackModel { [MessagePack.Key(0)] public int Id { get; set; } [MessagePack.Key(1)] public string? Name { get; set; } } ``` - **Global configuration**: In the `Startup.cs` or `Program.cs` file, configure and register the `HttpRemote` service to enable the `MessagePackContentProcessor` content processor: ```cs showLineNumbers {1,3} services.AddHttpRemote(builder => { builder.AddHttpContentProcessors(() => [ new MessagePackContentProcessor() ]); }); ``` This way you can send data in the `application/msgpack` format via `HTTP` remote requests in your project. > **Tip** The system includes a built-in `MessagePackContentProcessor` content processor by default, so you only need to install the `MessagePack` package in your project to enable it. However, note that the built-in [`MessagePackContentProcessor`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Processors/MessagePackContentProcessor.cs) is created via reflection, which may incur some performance overhead. If you have extremely high performance requirements, consider using the custom implementation shown above; otherwise, you can simply use the framework's built-in processor. --- # 6.6 Adding Protobuf Support > Source: https://http.furion.net/en/docs/advanced-guide/adding-protobuf-support/ `Protobuf` (`Protocol Buffers`) is a language-neutral, platform-neutral, extensible serialization format for structured data developed by Google, used for communication protocols, data storage, and more. To enable `Protobuf` support in your project, follow these steps: 1. **Install the `protobuf-net` package**: ```bash showLineNumbers dotnet add protobuf-net ``` 2. **Add the `ProtobufContentProcessor` content processor**: ```cs showLineNumbers {1,5,24-26} public class ProtobufContentProcessor : HttpContentProcessorBase { /// public override bool CanProcess(HttpContentProcessorContext context) => context.ContentType == "application/x-protobuf"; /// public override HttpContent? Process(HttpContentProcessorContext context) { // Attempt to resolve the HttpContent type if (TryProcess(context, out var httpContent)) { return httpContent; } byte[] content; if (context.RawContent is byte[] bytes) { content = bytes; } else { // Convert the raw request content to a byte array using var ms = new MemoryStream(); Serializer.Serialize(ms, context.RawContent); content = ms.ToArray(); } // Initialize a ByteArrayContent instance var byteArrayContent = new ByteArrayContent(content); byteArrayContent.Headers.ContentType = new MediaTypeHeaderValue(context.ContentType) { CharSet = context.Encoding?.WebName }; return byteArrayContent; } } ``` 3. **Apply the `ProtobufContentProcessor` content processor**: - **Per-request configuration**: ```cs showLineNumbers {2-3} HttpRequestBuilder.Post("https://furion.net/") .AddHttpContentProcessors(() => [ new ProtobufContentProcessor() ]) .SetContent(new MyProtobufMessage { Id = 1, Name = "Furion" }, "application/x-protobuf"); ``` > **Note** To use `protobuf-net` for serialization, your model class must add the `ProtoContract` attribute, and its properties must add the `ProtoMember` attribute. For detailed documentation, refer to the [protobuf-net official repository](https://github.com/protobuf-net/protobuf-net). However, types are usually defined and generated via `.proto` files: 1. Define the `my_message.proto` file as follows: ```cs showLineNumbers {1,3,6-9} syntax = "proto3"; // Specify using the proto3 syntax package mynamespace; // Optional: defines the package name (corresponding to the C# namespace) // Defines the MyProtobufMessage message type message MyProtobufMessage { int32 id = 1; // Integer field with tag number 1 string name = 2; // String field with tag number 2 } ``` 2. Use the [`Google.Protobuf`](https://www.nuget.org/packages/Google.Protobuf) command-line tool to generate the corresponding `C#` class, as follows: ```bash protoc -I=./ --csharp_out=./Generated ./my_message.proto ``` The generated C# class may look like the following (with the `ProtoContract` and `ProtoMember` attributes already added): ```cs showLineNumbers {1,4,7} [ProtoContract] public class MyProtobufMessage { [ProtoMember(1)] public int Id { get; set; } [ProtoMember(2)] public string Name { get; set; } } ``` - **Global configuration**: In the `Startup.cs` or `Program.cs` file, configure and register the `HttpRemote` service to enable the `ProtobufContentProcessor` content processor: ```cs showLineNumbers {1,3} services.AddHttpRemote(builder => { builder.AddHttpContentProcessors(() => [ new ProtobufContentProcessor() ]); }); ``` This way you can send data in the `application/x-protobuf` format via `HTTP` remote requests in your project. --- # 6.7 IHttpContentConverter Content Converter > Source: https://http.furion.net/en/docs/advanced-guide/ihttpcontentconverter-content-converter/ `IHttpContentConverter` is used to convert the `HttpResponseMessage` object returned by an `HTTP` remote request into the target type, as shown in the following diagram: ![httpagent](/images/httpagent.jpg) [**View the high-resolution architecture diagram**](https://github.com/monksoul/HttpAgent/blob/master/drawio/HttpAgent.drawio) --- # 6.8 Built-in Content Converters > Source: https://http.furion.net/en/docs/advanced-guide/built-in-content-converters/ - **`StringContentConverter` content converter** When the target receiving type is a string, [`StringContentConverter`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Converters/StringContentConverter.cs) is used to convert the `HttpResponseMessage` object into a string. Internally, this conversion calls the `ReadAsStringAsync` method provided by `HttpResponseMessage.Content`. --- - **`StreamContentConverter` content converter** When the target receiving type is `Stream`, [`StreamContentConverter`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Converters/StreamContentConverter.cs) is used to convert the `HttpResponseMessage` object into a `Stream`. Internally, this conversion calls the `ReadAsStreamAsync` method provided by `HttpResponseMessage.Content`. --- - **`ByteArrayContentConverter` content converter** When the target receiving type is a byte array, [`ByteArrayContentConverter`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Converters/ByteArrayContentConverter.cs) is used to convert the `HttpResponseMessage` object into a byte array. Internally, this conversion calls the `ReadAsByteArrayAsync` method provided by `HttpResponseMessage.Content`. --- - **`HttpResponseMessageConverter` content converter** When the target receiving type is `HttpResponseMessage`, [`HttpResponseMessageConverter`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Converters/HttpResponseMessageConverter.cs) is used to return the `HttpResponseMessage` object directly. --- - **`VoidContentConverter` content converter** When the target receiving type is `void` or `VoidContent`, [`VoidContentConverter`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Converters/VoidContentConverter.cs) is used to return a null value (no return value). --- - **`IActionResultContentConverter` content converter** When the target receiving type is `IActionResult`, [`IActionResultContentConverter`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent.AspNetCore/src/Converters/IActionResultContentConverter.cs) is used to convert the `HttpResponseMessage` object into `IActionResultContentConverter`. --- - **`AsyncEnumerableContentConverter` content converter** When the target receiving type is `IAsyncEnumerable`, [`AsyncEnumerableContentConverter`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Converters/AsyncEnumerableContentConverter.cs) is used to convert the `HttpResponseMessage` object into `IAsyncEnumerable`. Internally, this conversion calls the `ReadFromJsonAsAsyncEnumerable` method provided by `HttpResponseMessage.Content`. --- - **`HttpRemoteResult` content converter** When the target receiving type is `HttpRemoteResult`, [`HttpRemoteResultContentConverter`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Converters/HttpRemoteResultContentConverter.cs) is used to convert the `HttpResponseMessage` object into `HttpRemoteResult`. Internally, this conversion calls the `ReadAsync` method provided by `IHttpContentConverterFactory`. --- - **`ObjectContentConverter` content converter** When the target receiving type is not a specific type — for example custom types, primitive data types (such as `int`, `bool`, and so on), and various other types — [`ObjectContentConverter`](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Converters/ObjectContentConverter.cs) is used to convert the `HttpResponseMessage` object into the target receiving type. Internally, this conversion calls the `ReadFromJsonAsync` method provided by `HttpResponseMessage.Content`. --- # 6.9 IHttpContentConverterFactory Content Converter Factory > Source: https://http.furion.net/en/docs/advanced-guide/ihttpcontentconverterfactory-content-converter-factory/ The `IHttpContentConverterFactory` content converter factory is responsible for determining the appropriate `IHttpContentConverter` content converter based on the target receiving type, and calling its `Read` method to convert the `HttpResponseMessage` object into the target receiving type. This factory service is configured as a singleton to ensure its uniqueness and stability throughout the application's lifecycle. **If no matching `IHttpContentConverter` content converter is found, the system falls back to using the `IObjectContentConverterFactory` object content converter factory for the conversion, which internally returns an `ObjectContentConverter()` instance to perform the conversion.** The following example shows how to use the `IHttpContentConverterFactory` content converter factory together with `HttpClient`, demonstrating how its `Read` method can easily convert an `HttpResponseMessage` object into a target type instance. ```cs showLineNumbers {1,11-12} public class YourService(IHttpContentConverterFactory httpContentConverterFactory) // .NET8+ supports primary constructor injection { public async Task GetStringAsync() { var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, "https://furion.net/"); using var httpClient = new HttpClient(); var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage); // Call the Read method to convert the HttpResponseMessage object into a target type instance var context = new HttpContentConverterContext(httpResponseMessage); return await httpContentConverterFactory.GetConverter(context).ReadAsync(context); } } ``` > **About finding the `IHttpContentConverter` content converter** The `IHttpContentConverterFactory` factory searches for an `IHttpContentConverter` whose target receiving type matches, starting from the most recently added content converter. Once a content converter's generic type matches the target receiving type, a match is found, and that converter is then used to read the target type instance. --- # 6.10 IObjectContentConverterFactory Object Content Converter Factory > Source: https://http.furion.net/en/docs/advanced-guide/iobjectcontentconverterfactory-object-content-converter-factory/ When the `IHttpContentConverterFactory` content converter factory cannot find a matching `IHttpContentConverter` content processor, the system falls back to using the `IObjectContentConverterFactory` object content converter factory for conversion, which internally performs conversion by returning an `ObjectContentConverter()` instance. This factory service is configured as a singleton to ensure its uniqueness and stability throughout the application lifecycle. The following example combines the `IObjectContentConverterFactory` content converter factory with `HttpClient`, showing how to easily convert an `HttpResponseMessage` object into a target type instance through its `Read` method. ```cs showLineNumbers {1,12-13} public class YourService(IObjectContentConverterFactory objectContentConverterFactory) // .NET8+ supports primary constructor injection { public async Task GetStringAsync() { var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, "https://furion.net/getuser/100"); using var httpClient = new HttpClient(); var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage); // Call the GetConverter(new HttpContentConverterContext(httpResponseMessage)) method to obtain an object content converter instance // Then call the Read method to convert the HttpResponseMessage object into a target type instance var context = new HttpContentConverterContext(httpResponseMessage); return await objectContentConverterFactory.GetConverter(context).ReadAsync(context); } } ``` > **Tip** If you need to manually convert an `HttpResponseMessage` object into the target receiving type, it is recommended to use the `IHttpContentConverterFactory` content converter factory, because it internally calls `IObjectContentConverterFactory` by default. --- # 6.11 Custom Object Content Converter (e.g. Serialization) > Source: https://http.furion.net/en/docs/advanced-guide/custom-object-content-converter-eg-serialization/ The default content converter factory of `IObjectContentConverterFactory` returns an `ObjectContentConverter` instance, which uses the `ReadFromJsonAsync` method of `HttpResponseMessage.Content`, combined with the `System.Text.Json` serialization library, to convert the `HttpRequestMessage` object into the target receiving type. However, this conversion approach may encounter deserialization failures for certain special types (such as `DataTable`). To address this situation, you can customize `ObjectContentConverter` to select or replace it with a `JSON` serialization tool that better suits your needs (such as `Newtonsoft.Json`). > **Framework Recommendation** However, please note that unless there is a good reason, it is generally recommended to use `System.Text.Json`, because it is tightly integrated with `.NET Core` and offers excellent performance. If you need to customize these `JSON` serialization options, you can adjust them in the following ways: ```cs showLineNumbers {2-3,6,10-11,14} // Global configuration (applies to all clients) services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { // Customize JSON serialization behavior, e.g. ignore null values options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); // Client-level configuration (higher priority) services.AddHttpClient("client-name") .ConfigureOptions(options => { // Customize JSON serialization behavior, e.g. ignore null values options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); ``` **When customizing, you must provide both generic and non-generic versions:** ```cs showLineNumbers {2,5,11-12,15-16,19,24,28,32} // Non-generic version public class CustomObjectContentConverter : ObjectContentConverter { /// public override async Task ReadAsync(Type resultType, HttpContentConverterContext context, CancellationToken cancellationToken = default) { // Get the HttpResponseMessage instance var httpResponseMessage = context.ResponseMessage; // Resolve the JSON serialization context information corresponding to the HttpClient client var jsonSerializationContext = HttpRemoteUtility.ResolveJsonSerializationContext(resultType, httpResponseMessage, ServiceProvider); // Get the JSON deserialized value (if you need to use Newtonsoft.Json for serialization or deserialization, replace the following code with the corresponding method call from the Newtonsoft.Json library) ✅✅✅ var deserializedValue = await httpResponseMessage.Content.ReadFromJsonAsync(jsonSerializationContext.ResultType, jsonSerializationContext.JsonSerializerOptions, cancellationToken); // Get the converted target type value return jsonSerializationContext.GetResultValue(deserializedValue, httpResponseMessage); } } // Generic version public class CustomObjectContentConverter : CustomObjectContentConverter, IHttpContentConverter { /// public virtual TResult? Read(HttpContentConverterContext context, CancellationToken cancellationToken = default) => (TResult?)base.Read(typeof(TResult), context, cancellationToken); /// public virtual async Task ReadAsync(HttpContentConverterContext context, CancellationToken cancellationToken = default) => (TResult?)await base.ReadAsync(typeof(TResult), context, cancellationToken); } ``` Next, create a custom `IObjectContentConverterFactory` implementation: ```cs showLineNumbers {1,4,12,16,24} public sealed class CustomObjectContentConverterFactory : IObjectContentConverterFactory { /// public IHttpContentConverter GetConverter(HttpContentConverterContext context) { // Check whether the HTTP response content type is an XML media type if (context.ResponseMessage.IsXmlContent()) { return new XmlObjectContentConverter(); } return new CustomObjectContentConverter(); } /// public IHttpContentConverter GetConverter(Type resultType, HttpContentConverterContext context) { // Check whether the HTTP response content type is an XML media type if (context.ResponseMessage.IsXmlContent()) { return new XmlObjectContentConverter(); } return new CustomObjectContentConverter(); } } ``` Finally, in the `Startup.cs` or `Program.cs` file, configure and register the `HttpRemote` service to replace the default object content converter factory functionality: ```cs showLineNumbers {1,3} services.AddHttpRemote(builder => { builder.UseObjectContentConverterFactory(); }); ``` In this way, through the custom `ObjectContentConverter` and the `IObjectContentConverterFactory` factory, you can ensure that the specified `JSON` options are used during deserialization, avoiding potential deserialization issues. > **`ObjectContentConverter` and `ObjectContentConverter` Base Class Notes** The `ObjectContentConverter` and `ObjectContentConverter` base classes have a built-in `ServiceProvider` property, which allows you to easily resolve and obtain services registered through dependency injection (`DI`). --- # 6.12 Custom Content Converter > Source: https://http.furion.net/en/docs/advanced-guide/custom-content-converter/ In certain scenarios, when the framework's built-in `IHttpContentConverter` content converter cannot meet your needs, you can solve this by customizing the `IHttpContentConverter` content converter. For example, to add a content converter for the `Span` type, you can implement custom requirements by implementing the `IHttpContentConverter` interface. ```cs showLineNumbers {1,5,10-11} public class SpanCharContentConverter : HttpContentConverterBase> { /// public override byte[]? Read(HttpContentConverterContext context, CancellationToken cancellationToken = default) => AsyncUtility.RunSync(() => ReadAsync(context, cancellationToken)); /// public override async Task?> ReadAsync(HttpContentConverterContext context, CancellationToken cancellationToken = default) { var str = await context.ResponseMessage.Content.ReadAsStringAsync(cancellationToken); return str.AsSpan(); } } ``` Next, you can apply the custom content converter in the following two ways: - **Per-request configuration**: ```cs showLineNumbers {2} HttpRequestBuilder.Post("https://furion.net/") .AddHttpContentConverters(() => [ new SpanCharContentConverter() ]); ``` - **Global configuration**: In the `Startup.cs` or `Program.cs` file, configure and register the `HttpRemote` service to enable the custom content converter functionality: ```cs showLineNumbers {1,3} services.AddHttpRemote(builder => { builder.AddHttpContentConverters(() => [ new SpanCharContentConverter() ]); }); ``` The following example shows how to use the `SpanCharContentConverter` content converter when sending an `HTTP` remote request: Using the `IHttpRemoteService` approach: ```cs showLineNumbers {2-3,6} // Add for a single request var span = await httpRemoteService.SendAsAsync>(HttpRequestBuilder.Post("https://furion.net/") .AddHttpContentConverters(() => [ new SpanCharContentConverter() ])); // Global configuration var span = await httpRemoteService.GetAsAsync>("https://furion.net/"); ``` Using the `IHttpContentConverterFactory` approach: ```cs showLineNumbers {1,11-12} public class YourService(IHttpContentConverterFactory httpContentConverterFactory) // .NET8+ supports primary constructor injection { public async Task?> GetSpanAsync() { var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, "https://furion.net/"); using var httpClient = new HttpClient(); var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage); // Call the ReadAsync method to convert the HttpResponseMessage object into a target type instance var context = new HttpContentConverterContext(httpResponseMessage); return await httpContentConverterFactory.GetConverter>(context).ReadAsync(context); } } ``` > **`HttpContentConverterBase` Base Class Notes** The `HttpContentConverterBase` base class has a built-in `ServiceProvider` property, which allows you to easily resolve and obtain services registered through dependency injection (`DI`). --- # 6.13 Custom Generic Content Converter > Source: https://http.furion.net/en/docs/advanced-guide/custom-generic-content-converter/ In addition to concrete types, the framework also supports the conversion of generic content. For example, define the following generic converter: ```cs showLineNumbers {1,6,12} public class YourGenericClassContentConverter : HttpContentConverterBase> { /// public override YourGenericClass? Read(HttpContentConverterContext context, CancellationToken cancellationToken = default) { // Implement the synchronous conversion logic } /// public override Task?> ReadAsync(HttpContentConverterContext context, CancellationToken cancellationToken = default) { // Implement the asynchronous conversion logic } } ``` Next, in the `Startup.cs` or `Program.cs` file, configure and register the `HttpRemote` service to enable the custom generic content converter functionality: ```cs showLineNumbers {3} services.AddHttpRemote(builder => { builder.AddGenericHttpContentConverters(() => [ new(typeof(YourGenericClass<>), typeArgs => (IHttpContentConverter)Activator.CreateInstance(typeof(YourGenericClassContentConverter<>).MakeGenericType(typeArgs[0]))!) ]); }); ``` The following example shows how to use the `YourGenericClassContentConverter` content converter when sending an `HTTP` remote request: Using the `IHttpRemoteService` approach: ```cs showLineNumbers {2-3,6} var str = await httpRemoteService.GetAsAsync>("https://furion.net/"); ``` Using the `IHttpContentConverterFactory` approach: ```cs showLineNumbers {1,11-12} public class YourService(IHttpContentConverterFactory httpContentConverterFactory) // .NET8+ supports primary constructor injection { public async Task?> GetStringAsync() { var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, "https://furion.net/"); using var httpClient = new HttpClient(); var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage); // Call the ReadAsync method to convert the HttpResponseMessage object into a target type instance var context = new HttpContentConverterContext(httpResponseMessage); return await httpContentConverterFactory.GetConverter>(context).ReadAsync(context); } } ``` > **Extending Your Knowledge of `IHttpContentConverter`** To learn more about customizing the `IHttpContentConverter` content processor, please visit the [`HttpAgent` official repository](https://github.com/monksoul/HttpAgent/tree/master/src/HttpAgent/src/Converters) for reference. --- # 6.14 IHttpRemoteService Service > Source: https://http.furion.net/en/docs/advanced-guide/ihttpremoteservice-service/ `IHttpRemoteService` is an entry-point service for sending `HTTP` remote requests, and it forms the core of the `HTTP` remote request module. In short, when you need to send an `HTTP` remote request, you should use the injected `IHttpRemoteService` service. This service is registered as a singleton by default, so it can be safely used in services of any lifetime. Before using the `IHttpRemoteService` service, you need to register and configure the `HttpRemote` service in the `Startup.cs` or `Program.cs` file. ```cs showLineNumbers {2,5} // Register in Startup.cs: services.AddHttpRemote(); // In Program.cs, register as follows: // builder.Services.AddHttpRemote(); ``` > **Resolving the `AddHttpRemote` ambiguity error** If you encounter an ambiguity error on the `AddHttpRemote` method, you can resolve it by passing an empty delegate argument to it, as shown below: ```cs showLineNumbers services.AddHttpRemote(builder => {}); ``` Then, inject the `IHttpRemoteService` service in your service, controller, or any class that supports dependency injection. ```cs showLineNumbers {3,5} public class YourService { private readonly IHttpRemoteService _httpRemoteService; public YourService(IHttpRemoteService httpRemoteService) { _httpRemoteService = httpRemoteService; } } ``` If you are using `.NET 8` or later, you can simplify the code by injecting via [primary constructor](https://learn.microsoft.com/zh-cn/dotnet/csharp/whats-new/tutorials/primary-constructors): ```cs showLineNumbers {1} public class YourService(IHttpRemoteService httpRemoteService) { // Use the httpRemoteService variable } ``` Alternatively, you can inject it on demand within a specific method: ```cs showLineNumbers {3} public class YourService { public Task GetResource([FromServices] IHttpRemoteService httpRemoteService) { // Your code logic } } ``` > **Usage notes in environments without dependency injection** In `.NET Core`, it is recommended to build application projects using dependency injection with inversion of control. Therefore, it is advisable to build your application using dependency injection wherever possible. However, in certain special scenarios (such as in static classes), dependency injection may not be directly usable. In this case, you can obtain the service as follows: ```cs showLineNumbers var httpRemoteService = App.GetRequiredService(); ``` **Please note that this approach should be used as a supplement to dependency injection, not a replacement. Where possible, you should still prefer dependency injection to build and manage services in your application.** --- # 6.15 HttpRemoteBuilder Builder > Source: https://http.furion.net/en/docs/advanced-guide/httpremotebuilder-builder/ `HttpRemoteBuilder` is a builder used to configure and construct all the settings required by the `IHttpRemoteService` service. At application startup, these configurations are usually specified by calling the `services.AddHttpRemote` method. The following shows all the configuration capabilities provided by `HttpRemoteBuilder`: ```cs showLineNumbers {1,4,7,9,12-13,16-17,20,23,26,29,32-33,36-37} services.AddHttpRemote(builder => { // Add custom content processors builder.AddHttpContentProcessors(() => [ new CustomStringContentProcessor() ]); // Add custom content converters builder.AddHttpContentConverters(() => [ new SpanCharContentConverter() ]); // Add custom generic content converters builder.AddGenericHttpContentConverters(() => [ new(typeof(IAsyncEnumerable<>), typeArgs => (IHttpContentConverter)Activator.CreateInstance(typeof(AsyncEnumerableContentConverter<>).MakeGenericType(typeArgs[0]))!) ]); // Set the custom object content converter factory builder.UseObjectContentConverterFactory(); builder.UseObjectContentConverterFactory(typeof(CustomObjectContentConverterFactory)); // Add HTTP declarative services builder.AddHttpDeclarative(); builder.AddHttpDeclarative(typeof(IHttpService)); // Via the requireIHttpDeclarative parameter, supports registering declarative proxies that do not need to implement the IHttpDeclarative interface // Batch-add HTTP declarative services builder.AddHttpDeclaratives([typeof(IHttpService), typeof(IHttpService2), ...]); // Scan assemblies to batch-add HTTP declarative services builder.AddHttpDeclarativesFromAssemblies([ assembly1, assembly2, ... ]); // If using the Furion framework, you can directly set App.Assemblies // Add custom HTTP declarative extractors builder.AddHttpDeclarativeExtractors(() => [ new AcceptDeclarativeExtractor() ]); // Scan assemblies to batch-add HTTP declarative extractors builder.AddHttpDeclarativeExtractorsFromAssemblies([ assembly1, assembly2, ... ]); // If using the Furion framework, you can directly set App.Assemblies // Add HTTP request pipeline handler services builder.AddPipelineHandler(); builder.AddPipelineHandler(typeof(CustomHttpRequestPipelineHandler)); // Set the custom logging service, which can be implemented by inheriting HttpRemoteLoggerBase builder.UseLogger(); builder.UseLogger(typeof(CustomHttpRemoteLogger)); }); ``` --- # 6.16 HttpRemoteOptions Configuration Options > Source: https://http.furion.net/en/docs/advanced-guide/httpremoteoptions-configuration-options/ When adding the `HTTP` remote request service using the `services.AddHttpRemote()` method, an `IHttpRemoteBuilder` instance is returned. Through this instance, you can access and configure `HttpRemoteOptions`, which include properties such as the default request content type and `JSON` serialization settings: ```cs showLineNumbers {2,5,8,11,14,17,20,23,26,29,32,35} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { // Configure the default request content type options.DefaultContentType = "text/plain"; // "application/json" is recommended // Set the default save directory for file downloads options.DefaultFileDownloadDirectory = @"C:\Workspaces\"; // Set the request profiler log level, Warning by default options.ProfilerLogLevel = LogLevel.Warning; // Set whether requests should follow redirect responses, true by default options.AllowAutoRedirect = true; // Set the maximum number of redirects a request follows, 50 by default options.MaximumAutomaticRedirections = 50; // Set the fallback request base address, effective when HttpClient's BaseAddress is not configured and the request address is a relative address options.FallbackBaseAddress = new Uri("https://localhost:5000"); // Customize JSON serialization options options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; // Set the provider source used to replace configured template parameters in URL addresses options.Configuration = builder.Configuration; // If using the Furion framework, you can directly set App.Configuration // Set the URL parameter formatter options.UrlParameterFormatter = new UrlParameterFormatter(); // The fallback log output delegate when the logging service or console output is unavailable options.FallbackLogger = Console.WriteLine; // Can be replaced with Debug.WriteLine // Set the unified HttpRequestBuilder configurator options.RequestBuilderConfigurator = null; // null by default }); ``` The `ConfigureOptions` method allows more customized configuration of the `HTTP` remote request service, such as adjusting `JSON` serialization behavior. In addition, `ConfigureOptions` also provides an overload that supports service resolution. An example is shown below: ```cs showLineNumbers {2,5} services.AddHttpRemote(builder => {}) .ConfigureOptions((options, serviceProvider) => { // Resolve the required service var yourService = serviceProvider.GetRequiredService(); // Other configuration code }); ``` --- # 6.17 Unified Configuration of the HttpClient Client > Source: https://http.furion.net/en/docs/advanced-guide/unified-configuration-of-the-httpclient-client/ In application project development, it is often necessary to uniformly configure all `HttpClient` client instances. To this end, the framework provides the `ConfigureHttpClientDefaults` method, which supports one-click configuration: ```cs showLineNumbers {1,3,8,10} services.ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler()); }); // Or use the IHttpRemoteBuilder extension method for one-click configuration services.AddHttpRemote() .ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler()); }); ``` In this way, you can easily set a default `HttpMessageHandler` for all `HttpClient` instances, ensuring configuration consistency and maintainability. --- # 6.18 Built-in Properties and Methods > Source: https://http.furion.net/en/docs/advanced-guide/http-remote-service-members/ The `IHttpRemoteService` service type contains multiple properties as well as a rich variety of methods. ### Built-in Properties ```cs showLineNumbers {2,5} // Get the HTTP remote request options, the return value type is HttpRemoteOptions var remoteOptions = httpRemoteService.RemoteOptions; // Get the IServiceProvider interface instance var serviceProvider = httpRemoteService.ServiceProvider; ``` ### Built-in Methods - **Core methods**: ```cs showLineNumbers {1,7,13,19,25,31,37} // Returns an HttpResponseMessage object httpRemoteService.Send(httpRequestBuilder, cancellationToken); httpRemoteService.Send(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); await httpRemoteService.SendAsync(httpRequestBuilder, cancellationToken); await httpRemoteService.SendAsync(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); // Returns the target T type httpRemoteService.SendAs(httpRequestBuilder, cancellationToken); httpRemoteService.SendAs(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); await httpRemoteService.SendAsAsync(httpRequestBuilder, cancellationToken); await httpRemoteService.SendAsAsync(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); // Returns HttpRemoteResult httpRemoteService.Send(httpRequestBuilder, cancellationToken); httpRemoteService.Send(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); await httpRemoteService.SendAsync(httpRequestBuilder, cancellationToken); await httpRemoteService.SendAsync(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); // Returns an object type, whose actual type is resultType httpRemoteService.SendAs(resultType, httpRequestBuilder, cancellationToken); httpRemoteService.SendAs(resultType, httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); await httpRemoteService.SendAsAsync(resultType, httpRequestBuilder, cancellationToken); await httpRemoteService.SendAsAsync(resultType, httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); // Returns a string type httpRemoteService.SendAsString(httpRequestBuilder, cancellationToken); httpRemoteService.SendAsString(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); await httpRemoteService.SendAsStringAsync(httpRequestBuilder, cancellationToken); await httpRemoteService.SendAsStringAsync(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); // Returns a byte array type httpRemoteService.SendAsByteArray(httpRequestBuilder, cancellationToken); httpRemoteService.SendAsByteArray(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); await httpRemoteService.SendAsByteArrayAsync(httpRequestBuilder, cancellationToken); await httpRemoteService.SendAsByteArrayAsync(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); // Returns a Stream type httpRemoteService.SendAsStream(httpRequestBuilder, cancellationToken); httpRemoteService.SendAsStream(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); await httpRemoteService.SendAsStreamAsync(httpRequestBuilder, cancellationToken); await httpRemoteService.SendAsStreamAsync(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); ``` - **Request verb methods**: ```cs showLineNumbers {1,39,77,115,153,191,229,267,305} // ============ GET ============ // Returns an HttpResponseMessage object httpRemoteService.Get(requestUri, configure, cancellationToken); httpRemoteService.Get(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.GetAsync(requestUri, configure, cancellationToken); await httpRemoteService.GetAsync(requestUri, completionOption, configure, cancellationToken); // Returns the target T type httpRemoteService.GetAs(requestUri, configure, cancellationToken); httpRemoteService.GetAs(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.GetAsAsync(requestUri, configure, cancellationToken); await httpRemoteService.GetAsAsync(requestUri, completionOption, configure, cancellationToken); // Returns HttpRemoteResult httpRemoteService.Get(requestUri, configure, cancellationToken); httpRemoteService.Get(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.GetAsync(requestUri, configure, cancellationToken); await httpRemoteService.GetAsync(requestUri, completionOption, configure, cancellationToken); // Returns a string type httpRemoteService.GetAsString(requestUri, configure, cancellationToken); httpRemoteService.GetAsString(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.GetAsStringAsync(requestUri, configure, cancellationToken); await httpRemoteService.GetAsStringAsync(requestUri, completionOption, configure, cancellationToken); // Returns a byte array type httpRemoteService.GetAsByteArray(requestUri, configure, cancellationToken); httpRemoteService.GetAsByteArray(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.GetAsByteArrayAsync(requestUri, configure, cancellationToken); await httpRemoteService.GetAsByteArrayAsync(requestUri, completionOption, configure, cancellationToken); // Returns a Stream type httpRemoteService.GetAsStream(requestUri, configure, cancellationToken); httpRemoteService.GetAsStream(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.GetAsStreamAsync(requestUri, configure, cancellationToken); await httpRemoteService.GetAsStreamAsync(requestUri, completionOption, configure, cancellationToken); // ============ PUT ============ // Returns an HttpResponseMessage object httpRemoteService.Put(requestUri, configure, cancellationToken); httpRemoteService.Put(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PutAsync(requestUri, configure, cancellationToken); await httpRemoteService.PutAsync(requestUri, completionOption, configure, cancellationToken); // Returns the target T type httpRemoteService.PutAs(requestUri, configure, cancellationToken); httpRemoteService.PutAs(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PutAsAsync(requestUri, configure, cancellationToken); await httpRemoteService.PutAsAsync(requestUri, completionOption, configure, cancellationToken); // Returns HttpRemoteResult httpRemoteService.Put(requestUri, configure, cancellationToken); httpRemoteService.Put(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PutAsync(requestUri, configure, cancellationToken); await httpRemoteService.PutAsync(requestUri, completionOption, configure, cancellationToken); // Returns a string type httpRemoteService.PutAsString(requestUri, configure, cancellationToken); httpRemoteService.PutAsString(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PutAsStringAsync(requestUri, configure, cancellationToken); await httpRemoteService.PutAsStringAsync(requestUri, completionOption, configure, cancellationToken); // Returns a byte array type httpRemoteService.PutAsByteArray(requestUri, configure, cancellationToken); httpRemoteService.PutAsByteArray(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PutAsByteArrayAsync(requestUri, configure, cancellationToken); await httpRemoteService.PutAsByteArrayAsync(requestUri, completionOption, configure, cancellationToken); // Returns a Stream type httpRemoteService.PutAsStream(requestUri, configure, cancellationToken); httpRemoteService.PutAsStream(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PutAsStreamAsync(requestUri, configure, cancellationToken); await httpRemoteService.PutAsStreamAsync(requestUri, completionOption, configure, cancellationToken); // ============ POST ============ // Returns an HttpResponseMessage object httpRemoteService.Post(requestUri, configure, cancellationToken); httpRemoteService.Post(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PostAsync(requestUri, configure, cancellationToken); await httpRemoteService.PostAsync(requestUri, completionOption, configure, cancellationToken); // Returns the target T type httpRemoteService.PostAs(requestUri, configure, cancellationToken); httpRemoteService.PostAs(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PostAsAsync(requestUri, configure, cancellationToken); await httpRemoteService.PostAsAsync(requestUri, completionOption, configure, cancellationToken); // Returns HttpRemoteResult httpRemoteService.Post(requestUri, configure, cancellationToken); httpRemoteService.Post(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PostAsync(requestUri, configure, cancellationToken); await httpRemoteService.PostAsync(requestUri, completionOption, configure, cancellationToken); // Returns a string type httpRemoteService.PostAsString(requestUri, configure, cancellationToken); httpRemoteService.PostAsString(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PostAsStringAsync(requestUri, configure, cancellationToken); await httpRemoteService.PostAsStringAsync(requestUri, completionOption, configure, cancellationToken); // Returns a byte array type httpRemoteService.PostAsByteArray(requestUri, configure, cancellationToken); httpRemoteService.PostAsByteArray(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PostAsByteArrayAsync(requestUri, configure, cancellationToken); await httpRemoteService.PostAsByteArrayAsync(requestUri, completionOption, configure, cancellationToken); // Returns a Stream type httpRemoteService.PostAsStream(requestUri, configure, cancellationToken); httpRemoteService.PostAsStream(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PostAsStreamAsync(requestUri, configure, cancellationToken); await httpRemoteService.PostAsStreamAsync(requestUri, completionOption, configure, cancellationToken); // ============ DELETE ============ // Returns an HttpResponseMessage object httpRemoteService.Delete(requestUri, configure, cancellationToken); httpRemoteService.Delete(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.DeleteAsync(requestUri, configure, cancellationToken); await httpRemoteService.DeleteAsync(requestUri, completionOption, configure, cancellationToken); // Returns the target T type httpRemoteService.DeleteAs(requestUri, configure, cancellationToken); httpRemoteService.DeleteAs(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.DeleteAsAsync(requestUri, configure, cancellationToken); await httpRemoteService.DeleteAsAsync(requestUri, completionOption, configure, cancellationToken); // Returns HttpRemoteResult httpRemoteService.Delete(requestUri, configure, cancellationToken); httpRemoteService.Delete(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.DeleteAsync(requestUri, configure, cancellationToken); await httpRemoteService.DeleteAsync(requestUri, completionOption, configure, cancellationToken); // Returns a string type httpRemoteService.DeleteAsString(requestUri, configure, cancellationToken); httpRemoteService.DeleteAsString(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.DeleteAsStringAsync(requestUri, configure, cancellationToken); await httpRemoteService.DeleteAsStringAsync(requestUri, completionOption, configure, cancellationToken); // Returns a byte array type httpRemoteService.DeleteAsByteArray(requestUri, configure, cancellationToken); httpRemoteService.DeleteAsByteArray(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.DeleteAsByteArrayAsync(requestUri, configure, cancellationToken); await httpRemoteService.DeleteAsByteArrayAsync(requestUri, completionOption, configure, cancellationToken); // Returns a Stream type httpRemoteService.DeleteAsStream(requestUri, configure, cancellationToken); httpRemoteService.DeleteAsStream(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.DeleteAsStreamAsync(requestUri, configure, cancellationToken); await httpRemoteService.DeleteAsStreamAsync(requestUri, completionOption, configure, cancellationToken); // ============ HEAD ============ // Returns an HttpResponseMessage object httpRemoteService.Head(requestUri, configure, cancellationToken); httpRemoteService.Head(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.HeadAsync(requestUri, configure, cancellationToken); await httpRemoteService.HeadAsync(requestUri, completionOption, configure, cancellationToken); // Returns the target T type httpRemoteService.HeadAs(requestUri, configure, cancellationToken); httpRemoteService.HeadAs(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.HeadAsAsync(requestUri, configure, cancellationToken); await httpRemoteService.HeadAsAsync(requestUri, completionOption, configure, cancellationToken); // Returns HttpRemoteResult httpRemoteService.Head(requestUri, configure, cancellationToken); httpRemoteService.Head(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.HeadAsync(requestUri, configure, cancellationToken); await httpRemoteService.HeadAsync(requestUri, completionOption, configure, cancellationToken); // Returns a string type httpRemoteService.HeadAsString(requestUri, configure, cancellationToken); httpRemoteService.HeadAsString(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.HeadAsStringAsync(requestUri, configure, cancellationToken); await httpRemoteService.HeadAsStringAsync(requestUri, completionOption, configure, cancellationToken); // Returns a byte array type httpRemoteService.HeadAsByteArray(requestUri, configure, cancellationToken); httpRemoteService.HeadAsByteArray(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.HeadAsByteArrayAsync(requestUri, configure, cancellationToken); await httpRemoteService.HeadAsByteArrayAsync(requestUri, completionOption, configure, cancellationToken); // Returns a Stream type httpRemoteService.HeadAsStream(requestUri, configure, cancellationToken); httpRemoteService.HeadAsStream(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.HeadAsStreamAsync(requestUri, configure, cancellationToken); await httpRemoteService.HeadAsStreamAsync(requestUri, completionOption, configure, cancellationToken); // ============ OPTIONS ============ // Returns an HttpResponseMessage object httpRemoteService.Options(requestUri, configure, cancellationToken); httpRemoteService.Options(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.OptionsAsync(requestUri, configure, cancellationToken); await httpRemoteService.OptionsAsync(requestUri, completionOption, configure, cancellationToken); // Returns the target T type httpRemoteService.OptionsAs(requestUri, configure, cancellationToken); httpRemoteService.OptionsAs(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.OptionsAsAsync(requestUri, configure, cancellationToken); await httpRemoteService.OptionsAsAsync(requestUri, completionOption, configure, cancellationToken); // Returns HttpRemoteResult httpRemoteService.Options(requestUri, configure, cancellationToken); httpRemoteService.Options(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.OptionsAsync(requestUri, configure, cancellationToken); await httpRemoteService.OptionsAsync(requestUri, completionOption, configure, cancellationToken); // Returns a string type httpRemoteService.OptionsAsString(requestUri, configure, cancellationToken); httpRemoteService.OptionsAsString(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.OptionsAsStringAsync(requestUri, configure, cancellationToken); await httpRemoteService.OptionsAsStringAsync(requestUri, completionOption, configure, cancellationToken); // Returns a byte array type httpRemoteService.OptionsAsByteArray(requestUri, configure, cancellationToken); httpRemoteService.OptionsAsByteArray(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.OptionsAsByteArrayAsync(requestUri, configure, cancellationToken); await httpRemoteService.OptionsAsByteArrayAsync(requestUri, completionOption, configure, cancellationToken); // Returns a Stream type httpRemoteService.OptionsAsStream(requestUri, configure, cancellationToken); httpRemoteService.OptionsAsStream(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.OptionsAsStreamAsync(requestUri, configure, cancellationToken); await httpRemoteService.OptionsAsStreamAsync(requestUri, completionOption, configure, cancellationToken); // ============ TRACE ============ // Returns an HttpResponseMessage object httpRemoteService.Trace(requestUri, configure, cancellationToken); httpRemoteService.Trace(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.TraceAsync(requestUri, configure, cancellationToken); await httpRemoteService.TraceAsync(requestUri, completionOption, configure, cancellationToken); // Returns the target T type httpRemoteService.TraceAs(requestUri, configure, cancellationToken); httpRemoteService.TraceAs(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.TraceAsAsync(requestUri, configure, cancellationToken); await httpRemoteService.TraceAsAsync(requestUri, completionOption, configure, cancellationToken); // Returns HttpRemoteResult httpRemoteService.Trace(requestUri, configure, cancellationToken); httpRemoteService.Trace(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.TraceAsync(requestUri, configure, cancellationToken); await httpRemoteService.TraceAsync(requestUri, completionOption, configure, cancellationToken); // Returns a string type httpRemoteService.TraceAsString(requestUri, configure, cancellationToken); httpRemoteService.TraceAsString(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.TraceAsStringAsync(requestUri, configure, cancellationToken); await httpRemoteService.TraceAsStringAsync(requestUri, completionOption, configure, cancellationToken); // Returns a byte array type httpRemoteService.TraceAsByteArray(requestUri, configure, cancellationToken); httpRemoteService.TraceAsByteArray(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.TraceAsByteArrayAsync(requestUri, configure, cancellationToken); await httpRemoteService.TraceAsByteArrayAsync(requestUri, completionOption, configure, cancellationToken); // Returns a Stream type httpRemoteService.TraceAsStream(requestUri, configure, cancellationToken); httpRemoteService.TraceAsStream(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.TraceAsStreamAsync(requestUri, configure, cancellationToken); await httpRemoteService.TraceAsStreamAsync(requestUri, completionOption, configure, cancellationToken); // ============ PATCH ============ // Returns an HttpResponseMessage object httpRemoteService.Patch(requestUri, configure, cancellationToken); httpRemoteService.Patch(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PatchAsync(requestUri, configure, cancellationToken); await httpRemoteService.PatchAsync(requestUri, completionOption, configure, cancellationToken); // Returns the target T type httpRemoteService.PatchAs(requestUri, configure, cancellationToken); httpRemoteService.PatchAs(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PatchAsAsync(requestUri, configure, cancellationToken); await httpRemoteService.PatchAsAsync(requestUri, completionOption, configure, cancellationToken); // Returns HttpRemoteResult httpRemoteService.Patch(requestUri, configure, cancellationToken); httpRemoteService.Patch(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PatchAsync(requestUri, configure, cancellationToken); await httpRemoteService.PatchAsync(requestUri, completionOption, configure, cancellationToken); // Returns string type httpRemoteService.PatchAsString(requestUri, configure, cancellationToken); httpRemoteService.PatchAsString(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PatchAsStringAsync(requestUri, configure, cancellationToken); await httpRemoteService.PatchAsStringAsync(requestUri, completionOption, configure, cancellationToken); // Returns byte array type httpRemoteService.PatchAsByteArray(requestUri, configure, cancellationToken); httpRemoteService.PatchAsByteArray(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PatchAsByteArrayAsync(requestUri, configure, cancellationToken); await httpRemoteService.PatchAsByteArrayAsync(requestUri, completionOption, configure, cancellationToken); // Returns Stream type httpRemoteService.PatchAsStream(requestUri, configure, cancellationToken); httpRemoteService.PatchAsStream(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PatchAsStreamAsync(requestUri, configure, cancellationToken); await httpRemoteService.PatchAsStreamAsync(requestUri, completionOption, configure, cancellationToken); // ============ QUERY ============ // Returns the HttpResponseMessage object httpRemoteService.Query(requestUri, configure, cancellationToken); httpRemoteService.Query(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.QueryAsync(requestUri, configure, cancellationToken); await httpRemoteService.QueryAsync(requestUri, completionOption, configure, cancellationToken); // Returns the target T type httpRemoteService.QueryAs(requestUri, configure, cancellationToken); httpRemoteService.QueryAs(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.QueryAsAsync(requestUri, configure, cancellationToken); await httpRemoteService.QueryAsAsync(requestUri, completionOption, configure, cancellationToken); // Returns HttpRemoteResult httpRemoteService.Query(requestUri, configure, cancellationToken); httpRemoteService.Query(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.QueryAsync(requestUri, configure, cancellationToken); await httpRemoteService.QueryAsync(requestUri, completionOption, configure, cancellationToken); // Returns string type httpRemoteService.QueryAsString(requestUri, configure, cancellationToken); httpRemoteService.QueryAsString(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.QueryAsStringAsync(requestUri, configure, cancellationToken); await httpRemoteService.QueryAsStringAsync(requestUri, completionOption, configure, cancellationToken); // Returns byte array type httpRemoteService.QueryAsByteArray(requestUri, configure, cancellationToken); httpRemoteService.QueryAsByteArray(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.QueryAsByteArrayAsync(requestUri, configure, cancellationToken); await httpRemoteService.QueryAsByteArrayAsync(requestUri, completionOption, configure, cancellationToken); // Returns Stream type httpRemoteService.QueryAsStream(requestUri, configure, cancellationToken); httpRemoteService.QueryAsStream(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.QueryAsStreamAsync(requestUri, configure, cancellationToken); await httpRemoteService.QueryAsStreamAsync(requestUri, completionOption, configure, cancellationToken); ``` ### Specific Feature Methods ```cs showLineNumbers {1,10,19,26,33,40} // Download a file httpRemoteService.DownloadFile(requestUri, destinationPath, onProgressChanged, fileExistsBehavior, configure, cancellationToken); await httpRemoteService.DownloadFileAsync(requestUri, destinationPath, onProgressChanged, fileExistsBehavior, configure, cancellationToken); httpRemoteService.DownloadFileWithConsoleProgress(requestUri, destinationPath, fileExistsBehavior, configure, cancellationToken); await httpRemoteService.DownloadFileWithConsoleProgressAsync(requestUri, destinationPath, fileExistsBehavior, configure, cancellationToken); httpRemoteService.Send(httpFileDownloadBuilder, cancellationToken); await httpRemoteService.SendAsync(httpFileDownloadBuilder, cancellationToken); // Upload a file httpRemoteService.UploadFile(requestUri, filePath, name, onProgressChanged, fileName, configure, cancellationToken); await httpRemoteService.UploadFileAsync(requestUri, filePath, name, onProgressChanged, fileName, configure, cancellationToken); httpRemoteService.UploadFileWithConsoleProgress(requestUri, filePath, name, fileName, configure, cancellationToken); await httpRemoteService.UploadFileWithConsoleProgressAsync(requestUri, filePath, name, fileName, configure, cancellationToken); httpRemoteService.Send(httpFileUploadBuilder, cancellationToken); await httpRemoteService.SendAsync(httpFileUploadBuilder, cancellationToken); // Send a Server-Sent Events request httpRemoteService.ServerSentEvents(requestUri, onMessage, configure, cancellationToken); await httpRemoteService.ServerSentEventsAsync(requestUri, onMessage, configure, cancellationToken); httpRemoteService.Send(httpServerSentEventsBuilder, cancellationToken); await httpRemoteService.SendAsync(httpServerSentEventsBuilder, cancellationToken); // Stress testing httpRemoteService.StressTestHarness(requestUri, numberOfRequests, configure, HttpCompletionOption.ResponseContentRead, cancellationToken); await httpRemoteService.StressTestHarnessAsync(requestUri, numberOfRequests, configure, HttpCompletionOption.ResponseContentRead, cancellationToken); httpRemoteService.Send(httpStressTestHarnessBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); await httpRemoteService.SendAsync(httpStressTestHarnessBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); // Send a long polling request httpRemoteService.LongPolling(requestUri, onDataReceived, configure, cancellationToken); await httpRemoteService.LongPollingAsync(requestUri, onDataReceived, configure, cancellationToken); httpRemoteService.Send(httpLongPollingBuilder, cancellationToken); await httpRemoteService.SendAsync(httpLongPollingBuilder, cancellationToken); // Send an HTTP declarative request httpRemoteService.Declarative(method, args); await httpRemoteService.DeclarativeAsync(method, args); httpRemoteService.SendAs(httpDeclarativeBuilder); await httpRemoteService.SendAsAsync(httpDeclarativeBuilder); ``` --- # 6.19 Adding IHttpRemoteService Extensions > Source: https://http.furion.net/en/docs/advanced-guide/http-remote-service-extensions/ In addition to the `IHttpRemoteService` methods provided by the system, you can also add custom extension methods to it to simplify code and reduce duplication. For example, you can add a `SendAsSpan` method for sending an `HTTP` remote request that returns `Span`. The specific implementation is as follows: ```cs showLineNumbers {1,3,13} public static class HttpRemoteServiceExtensions { public static Span SendAsSpan(this IHttpRemoteService httpRemoteService, HttpRequestBuilder httpRequestBuilder, CancellationToken cancellationToken = default) { // Null check ArgumentNullException.ThrowIfNull(httpRequestBuilder); var str = httpRemoteService.SendAsString(httpRequestBuilder, cancellationToken); return str.AsSpan(); } public static async Task> SendAsSpanAsync(this IHttpRemoteService httpRemoteService, HttpRequestBuilder httpRequestBuilder, CancellationToken cancellationToken = default) { // Null check ArgumentNullException.ThrowIfNull(httpRequestBuilder); var str = await httpRemoteService.SendAsStringAsync(httpRequestBuilder, cancellationToken); return str.AsSpan(); } } ``` Afterwards, you can easily use this method in an `IHttpRemoteService` instance: ```cs showLineNumbers httpRemoteService.SendAsSpan(HttpRequestBuilder.Get("https://furion.net")); await httpRemoteService.SendAsSpanAsync(HttpRequestBuilder.Get("https://furion.net")); ``` By leveraging the features of `C#` extension methods, you can greatly enrich the functionality of `IHttpRemoteService`, reduce duplicated code, and improve code readability and maintainability. --- # 6.20 HttpRemoteResult Return Value > Source: https://http.furion.net/en/docs/advanced-guide/http-remote-result/ `HttpRemoteResult` is a generic type specifically used for response content in the `HTTP` remote request module. The generic parameter `TResult` represents the data type that needs to be converted into in the end. In addition to supporting common `HTTP` response types such as `string`, `byte[]`, `Stream`, `HttpResponseMessage`, `IAsyncEnumerable` and `IActionResult`, it also supports custom types and the framework's built-in `VoidContent` type. This type encapsulates commonly used functions such as `HTTP` response information and request duration. In the `HTTP` remote request module, all default generic request methods that do not contain the `As` keyword return the `HttpRemoteResult` type. The following is an example of obtaining a `HttpRemoteResult` type return value in different ways: ```cs showLineNumbers {2,5} // Request verb approach var httpResult = await httpRemoteService.GetAsync("https://furion.net/"); // Builder approach var httpResult = await httpRemoteService.SendAsync(HttpRequestBuilder.Get("https://furion.net/")); ``` `HttpRemoteResult` contains the following properties and methods: - **Properties**: - `ResponseMessage`: the response message (`HttpResponseMessage` type). - `ContentType`: the content type (`string` type). - `CharSet`: the character set (`string` type). - `ContentEncoding`: the content encoding (`ICollection` type). - `ContentLength`: the content size (`long` type). - `Server`: the raw response header `Server` (`HttpHeaderValueCollection` type). - `RawSetCookies`: the raw response header `Set-Cookie` collection (`List` type). - `SetCookies`: the response `Cookie` collection (`IList` type). - `StatusCode`: the response status code (`HttpStatusCode` type). - `IsSuccessStatusCode`: whether the request succeeded (`bool` type). - `Result`: the target data (`TResult` generic type). - `RequestDuration`: the request duration in milliseconds (`long` type). - `Headers`: the response headers (`HttpResponseHeaders` type). - `ContentHeaders`: the response content headers (`HttpContentHeaders` type). - `Version`: the `HTTP` version (`Version` type). - `HttpClientName`: the configured name of the `HttpClient` instance (`string?` type). - **Methods**: - `ToString()`: outputs a string with indented, detailed request and response information. > **Return Value Type Notes** By default, when the return value type is not `string`, `byte[]`, `Stream`, `HttpResponseMessage`, `VoidContent`, `IAsyncEnumerable` or `IActionResult`, other types are deserialized using `System.Text.Json`. If you need to change this behavior, you can learn how to implement the `IHttpContentConverter` content converter interface for customization in subsequent chapters. In the latest version, the framework introduced support for [deconstruction](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/deconstruction) for the `HttpRemoteResult` type. Deconstruction expressions simplify the object parsing process, making it more convenient to obtain key property values. The following is the sample code: ```cs showLineNumbers // Deconstruction expressions are used to extract the required property values var (result, response) = await httpRemoteService.GetAsync("https://furion.net/"); // You can call ThrowIfNull()/OrDefault() to resolve the null reference warning var (result, response, isSuccess) = await httpRemoteService.GetAsync("https://furion.net/"); // You can call ThrowIfNull()/OrDefault() to resolve the null reference warning var (result, response, isSuccess, statusCode) = await httpRemoteService.GetAsync("https://furion.net/"); // You can call ThrowIfNull()/OrDefault() to resolve the null reference warning ``` In these examples, `result` is of type `TResult`, `response` is of type `HttpResponseMessage`, `isSuccess` is of type `bool`, and `statusCode` is of type `HttpStatusCode`. By using deconstruction expressions, not only is code readability improved, but the development process also becomes more efficient. This improvement allows developers to directly access the data they need, reducing the steps of manually obtaining each property value, thereby making the code more concise and intuitive. --- Additionally, the `HttpRemoteResult` type also has a built-in `ToString()` method, which can clearly print out the detailed information of the request headers and response headers in an indented format, as shown below: ```cs showLineNumbers Console.WriteLine(httpResult.ToString()); // Or use Console.WriteLine(httpResult); ``` The terminal console output is as follows: ```bash showLineNumbers Request Headers: User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0 traceparent: 00-602c9070b85da9bd73fc1eac36fdb3cb-14dded89e0f5266b-00 General: Request URL: https://furion.net/ Request Method: GET Status Code: 200 OK HTTP Version: 1.1 HTTP Content: Content Type: HttpClient Name: Request Duration (ms): 133.00 Response Headers: Server: nginx/1.22.1 Date: Mon, 18 Nov 2024 21:26:06 GMT Connection: keep-alive Vary: Accept-Encoding ETag: "67091697-f32f" Cache-Control: max-age=315360000 Accept-Ranges: bytes Content-Type: text/html Content-Length: 62255 Last-Modified: Fri, 11 Oct 2024 12:14:15 GMT Expires: Thu, 31 Dec 2037 23:55:55 GMT ``` --- # 6.21 Downloading Network Resources > Source: https://http.furion.net/en/docs/advanced-guide/downloading-network-resources/ One of the most common application scenarios for `HTTP` remote requests is downloading network resources and saving them to the local disk, including downloading web page content, images, compressed packages, installation software, and so on. There are multiple ways to download network resources. The most common is to send an `HTTP` request, receive the returned `Stream`, and then write it to the local disk and save it as the corresponding file. ## Downloading in the Conventional Way by Receiving the `Stream` ```cs showLineNumbers {2,5-6} // Get the response Stream var stream = await httpRemoteService.GetAsStreamAsync("https://furion.net/img/furionlogo.png"); // Create a file stream and write to it using var fileStream = new FileStream(@"C:\Workspaces\furionlogo.png", FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true); await contentStream.CopyToAsync(fileStream); ``` However, this way of downloading network resources is not flexible enough when faced with various complex scenarios — for example, it cannot track download progress in real time, properly handle the case where the file already exists, or implement chunked downloads. In addition, it may require developers to write more additional code. Therefore, the framework integrates features specifically designed for downloading network resources to address these issues. ## Downloading with the framework's built-in dedicated download functionality The following example shows how to use the framework's built-in download functionality to download the `ASP.NET Core` runtime: ```cs showLineNumbers {3} // Downloads the ASP.NET Core runtime from the specified URL and saves it to the C:\Workspaces\ directory // If no file name is specified, the framework automatically resolves the file name from the download URL, for example: aspnetcore-runtime-8.0.10-win-x64.exe var fileTransferResult = await httpRemoteService.DownloadFileAsync("https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe" , @"C:\Workspaces\"); // To specify a file name, set it to C:\Workspaces\aspnetcore-runtime.exe ``` > **Download file save path notes** - If the download file name is not specified, the framework automatically resolves the file name from the download URL. - If a custom file name is provided, that name is used to save the final downloaded file. - In addition, if you only provide a target folder (directory) for storing the downloaded file, make sure the folder (directory) path ends with a slash (`/`). After the file download completes, the framework returns a `FileTransferResult` object containing the following properties: - `IsSuccess`: whether the transfer completed successfully (`bool` type). **Note: skipping because the file exists is also considered a success.** - `RequestUri`: the file transfer `URL` (`string` type). - `FilePath`: the path of the file (`string` type). - `FileSize`: the size of the file (a `long` type, in bytes). - `ElapsedMilliseconds`: the transfer duration (a `long` type, in milliseconds). - `StatusCode`: the response status (`HttpStatusCode` type). If the local file already exists, an `InvalidOperationException` is thrown: `System.InvalidOperationException: The destination path 'C:\Workspaces\aspnetcore-runtime-8.0.10-win-x64.exe' already exists.`. In this case, you can use the `fileExistsBehavior` parameter to specify the behavior when the file exists: ```cs showLineNumbers {3} var fileTransferResult = await httpRemoteService.DownloadFileAsync("https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe" , @"C:\Workspaces\" , fileExistsBehavior: FileExistsBehavior.Overwrite); // Overwrite the file if it exists ``` The `FileExistsBehavior` enum contains the following options: - `CreateNew` (default): if the file already exists, an exception is thrown; otherwise, a new file is created. - `Overwrite`: overwrites the existing file. - `Skip`: keeps the existing file and skips the download operation. While downloading a file, you can also obtain real-time download progress. The following example shows how to print the download progress: ```cs showLineNumbers {3-6} var fileTransferResult = await httpRemoteService.DownloadFileAsync("https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe" , @"C:\Workspaces\" , async progress => { Console.WriteLine(await progress.ToSummaryStringAsync()); // Outputs a brief progress string } , fileExistsBehavior: FileExistsBehavior.Overwrite); ``` Example console output of download progress (using `progress.ToSummaryString()`): ```bash showLineNumbers Transferred 0.26MB of 10.09MB (2.63% complete, Speed: 3.86MB/s, Time: 0.07s, ETA: 2.55s), File: aspnetcore-runtime-8.0.10-win-x64.exe, Path: C:\Workspaces\aspnetcore-runtime-8.0.10-win-x64.exe. Transferred 10.09MB of 10.09MB (100.00% complete, Speed: 9.99MB/s, Time: 1.01s, ETA: 0.00s), File: aspnetcore-runtime-8.0.10-win-x64.exe, Path: C:\Workspaces\aspnetcore-runtime-8.0.10-win-x64.exe. ``` To display file download progress in the console in real time, it is recommended to use the `UpdateConsoleProgressAsync()` method. The example is as follows: ```cs showLineNumbers {3,7} var fileTransferResult = await httpRemoteService.DownloadFileAsync("https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe" , @"C:\Workspaces\" , progress => progress.UpdateConsoleProgressAsync() // Updates the file transfer progress bar in the console , fileExistsBehavior: FileExistsBehavior.Overwrite); // ✅ Or use the DownloadFileWithConsoleProgressAsync method (with console progress printing) var fileTransferResult = await httpRemoteService.DownloadFileWithConsoleProgressAsync("https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe" , @"C:\Workspaces\" , fileExistsBehavior: FileExistsBehavior.Overwrite); ``` After execution, the console displays the following progress information: ```bash showLineNumbers {2} File: aspnetcore-runtime-8.0.10-win-x64.exe, Path: C:\Workspaces\aspnetcore-runtime-8.0.10-win-x64.exe [############################## ] 61.35% (6.19MB/10.09MB) Speed: 5.81MB/s, Time: 1.07s, ETA: 0.67s. ``` If you use `progress.ToString()`, the console output will contain more detailed progress information: ```bash showLineNumbers Transfer Progress: File Name: aspnetcore-runtime-8.0.10-win-x64.exe File Path: C:\Workspaces\aspnetcore-runtime-8.0.10-win-x64.exe File Size: 10.09MB Transferred: 0.12MB Percentage Complete: 1.23% Transfer Rate: 2.20MB/s Time Elapsed (s): 0.06 Estimated Time Remaining (s): 4.52 Transfer Progress: File Name: aspnetcore-runtime-8.0.10-win-x64.exe File Path: C:\Workspaces\aspnetcore-runtime-8.0.10-win-x64.exe File Size: 10.09MB Transferred: 10.09MB Percentage Complete: 100.00% Transfer Rate: 9.77MB/s Time Elapsed (s): 1.03 Estimated Time Remaining (s): 0.00 ``` The `progress` parameter is of type `FileTransferProgress` and contains the following properties and methods: - **Properties**: - `FilePath`: the path of the file (`string` type). - `FileName`: the name of the file (`string` type). - `FileSize`: the size of the file (a `long` type, in bytes). - `Transferred`: the amount of data transferred (a `long` type, in bytes). - `PercentageComplete`: the percentage of the transfer completed (`double` type). - `TransferRate`: the current transfer rate (a `double` type, in bytes per second). - `TimeElapsed`: the duration from the start of the transfer to now (`TimeSpan` type). - `EstimatedTimeRemaining`: the estimated remaining transfer time (`TimeSpan` type). - **Methods**: - `ToString()`: outputs a detailed, indented progress string. - `ToStringAsync()`: outputs a detailed, indented progress string. - `ToSummaryString()`: outputs a brief progress string. - `ToSummaryStringAsync()`: outputs a brief progress string. - `UpdateConsoleProgress()`: updates (prints) the file transfer progress bar in the console. - `UpdateConsoleProgressAsync()`: updates (prints) the file transfer progress bar in the console. ## Downloading multiple files in parallel The framework natively supports downloading multiple files in parallel. With the `ParallelUtility.ForEachAsync` utility method, you can easily achieve concurrent downloads and automatically enable **multi-line progress bar mode** — each file occupies two lines (the file header and the progress bar), and all progress bars refresh in real time on the same screen without interfering with each other: ```cs showLineNumbers {10,12} var urls = new[] { "https://img-s.msn.cn/tenant/amp/entityid/AA296jTM.img?w=640&h=1068&m=6", "https://img-s.msn.cn/tenant/amp/entityid/AA297bnQ.img?w=640&h=1240&m=6&x=236&y=233&s=64&d=64", "https://img-s.msn.cn/tenant/amp/entityid/AA296Rr4.img?w=640&h=821&m=6" }; const string saveDir = @"C:\Workspaces\"; // Downloads in parallel; the default maximum concurrency is 4 await ParallelUtility.ForEachAsync(urls, async (url, token) => { await _httpRemoteService.DownloadFileWithConsoleProgressAsync(url, saveDir, FileExistsBehavior.Overwrite, cancellationToken: token); }); ``` After execution, the console displays the download progress of all files at the same time, and each file's progress bar refreshes independently: ```bash showLineNumbers File: AA296jTM.img, Path: C:\Workspaces\AA296jTM.img [########............] 40.12% (0.05MB/0.12MB) Speed: 1.20MB/s, Time: 42ms, ETA: 58ms. File: AA297bnQ.img, Path: C:\Workspaces\AA297bnQ.img [######..............] 30.05% (0.04MB/0.12MB) Speed: 0.95MB/s, Time: 38ms, ETA: 84ms. File: AA296Rr4.img, Path: C:\Workspaces\AA296Rr4.img [##########..........] 50.33% (0.06MB/0.12MB) Speed: 1.55MB/s, Time: 45ms, ETA: 39ms. ``` After all files finish downloading, the progress bars display the `Done!` status one by one: ```bash showLineNumbers File: AA296jTM.img, Path: C:\Workspaces\AA296jTM.img [####################] 100.00% (0.12MB/0.12MB) Speed: 2.61MB/s, Time: 167ms. Done! File: AA297bnQ.img, Path: C:\Workspaces\AA297bnQ.img [####################] 100.00% (0.12MB/0.12MB) Speed: 1.81MB/s, Time: 203ms. Done! File: AA296Rr4.img, Path: C:\Workspaces\AA296Rr4.img [####################] 100.00% (0.12MB/0.12MB) Speed: 1.61MB/s, Time: 204ms. Done! ``` You can also control the maximum concurrency via the `maxDegreeOfParallelism` parameter: ```cs showLineNumbers {5} await ParallelUtility.ForEachAsync(urls, async (url, token) => { await _httpRemoteService.DownloadFileWithConsoleProgressAsync(url, saveDir, FileExistsBehavior.Overwrite, cancellationToken: token); }, maxDegreeOfParallelism: 2); // Download at most 2 files at the same time ``` --- # 6.22 The HttpFileDownloadBuilder builder > Source: https://http.furion.net/en/docs/advanced-guide/the-httpfiledownloadbuilder-builder/ In addition to the methods above, you can also use the `HttpFileDownloadBuilder` builder to configure the various settings required for downloading network resources. ```cs showLineNumbers {1} var fileTransferResult = await httpRemoteService.SendAsync(HttpRequestBuilder.DownloadFile("https://furion.net/img/furionlogo.png", @"C:\Workspaces\")); ``` The `HttpFileDownloadBuilder` builder provides the various settings specifically used by the framework to download network resources. The constructor of `HttpFileDownloadBuilder` is private, so it cannot be instantiated directly with the `new` keyword; however, the framework provides several static overloads of `HttpRequestBuilder.DownloadFile` to create instances of `HttpFileDownloadBuilder`. ```cs showLineNumbers HttpRequestBuilder.DownloadFile(httpMethod, requestUri, destinationPath, onProgressChanged, fileExistsBehavior, configure); HttpRequestBuilder.DownloadFile(requestUri, destinationPath, onProgressChanged, fileExistsBehavior, configure); // Defaults to a GET request ``` In addition, `HttpFileDownloadBuilder` includes the following configuration features: ```cs showLineNumbers {1,5,8,11,14,17,20,23,26,29,32-33,36,39,42,45,48} // Defaults to a GET request. If the save file name is not specified, the file name is resolved automatically, e.g., the final download path is C:\Workspaces\furionlogo.png HttpRequestBuilder.DownloadFile("https://furion.net/img/furionlogo.png", @"C:\Workspaces\") // Sets the buffer size used for the transfer operation, in bytes; the default value is 80 KB .SetBufferSize(80 * 1024) // Sets the destination path where the file is saved; it can be set to null, in which case the DefaultFileDownloadDirectory property of HttpRemoteOptions or the application's execution directory is used .SetDestinationPath(@"C:\Workspaces\") // Sets the behavior when the target file already exists .SetFileExistsBehavior(FileExistsBehavior.Overwrite) // Sets the interval for file transfer progress (notifications) .SetProgressInterval(TimeSpan.FromSeconds(1)) // Sets the action to run when the file transfer starts .SetOnTransferStarted(() => {}) // Sets the delegate to execute when the transfer progress changes .SetOnProgressChanged(async progress => { }) // Sets the action to run when the file transfer completes; the delegate parameter is the total time spent on the file transfer (in milliseconds) .SetOnTransferCompleted(duration => {}) // Sets the action to run when an exception occurs during the file transfer .SetOnTransferFailed(exception => {}) // Sets the action to run when the file exists and is configured to be skipped .SetOnFileExistAndSkip(() => {}) // Sets the HTTP file transfer event handler; CustomFileTransferEventHandler is a type that implements the IHttpFileTransferEventHandler interface .SetEventHandler() .SetEventHandler(typeof(CustomFileTransferEventHandler)) // Sets the HttpRequestBuilder instance .With(builder => {}) // Supports further extension // Sets the maximum number of download threads .SetMaxThreads(4) // Sets the maximum idle wait time for a single data read (sliding window timeout) .SetChunkTimeout(TimeSpan.FromSeconds(100)) // Sets the maximum number of retries for multi-threaded chunked downloads .SetChunkMaxRetries(3) // Enables high-speed download mode .EnableHighSpeedMode(); // Supports passing in the maximum number of download threads ``` After successfully building a `HttpFileDownloadBuilder` instance via the `HttpRequestBuilder.DownloadFile` method, you can use the `Send` method or the asynchronous `SendAsync` method to execute the send operation. ```cs showLineNumbers var fileTransferResult = httpRemoteService.Send(httpFileDownloadBuilder, cancellationToken); var fileTransferResult = await httpRemoteService.SendAsync(httpFileDownloadBuilder, cancellationToken); ``` --- # 6.23 File transfer event handler > Source: https://http.furion.net/en/docs/advanced-guide/file-transfer-event-handler/ The `IHttpFileTransferEventHandler` interface allows you to define pre-processing operations for downloading or uploading files. By implementing this interface, you can create a custom file transfer event handler, such as the `CustomFileTransferEventHandler` class: ```cs showLineNumbers {1} public class CustomFileTransferEventHandler : IHttpFileTransferEventHandler { // Action when the file transfer starts public void OnTransferStarted() {} // Action when the transfer progress changes public Task OnProgressChangedAsync(FileTransferProgress fileTransferProgress) {} // Action when the file transfer completes public void OnTransferCompleted(long duration) {} // Action when an exception occurs during the file transfer public void OnTransferFailed(Exception exception) {} } ``` To enable this handler in your application, register the `CustomFileTransferEventHandler` service in the `Startup.cs` or `Program.cs` file: ```cs showLineNumbers services.TryAddSingleton(); ``` Next, you can specify this handler when building the `HTTP` request: ```cs showLineNumbers {2,5} HttpRequestBuilder.DownloadFile("https://furion.net/img/furionlogo.png", @"C:\Workspaces\") .SetEventHandler(); HttpRequestBuilder.DownloadFile("https://furion.net/img/furionlogo.png", @"C:\Workspaces\") .SetEventHandler(typeof(CustomFileTransferEventHandler)); // Set using the type approach ``` > **Reuse tip** You can create a custom type that implements the `IHttpFileTransferEventHandler` interface and reuse that implementation across multiple `HttpFileDownloadBuilder` instances. > **Trigger timing notes** When a `HttpFileDownloadBuilder` instance is configured with the `SetOnTransferStarted`, `SetOnProgressChanged`, `OnTransferCompleted`, or `OnTransferFailed` methods, these callback methods will be triggered. If the `IHttpFileTransferEventHandler` interface is also implemented, its methods (`OnTransferStarted`, `OnProgressChangedAsync`, `OnTransferCompleted`, and `OnTransferFailed`) will be invoked later than the series of methods set on the `HttpFileDownloadBuilder` instance. --- # 6.24 Uploading file resources > Source: https://http.furion.net/en/docs/advanced-guide/uploading-file-resources/ In internet applications, users uploading files is a common requirement, covering scenarios such as setting avatars, publishing image-and-text posts, uploading albums to cloud drives, and sharing `Vlog` videos to video communities. The following shows several ways to implement file uploads. ## Uploading Using the `Form` Form Method ```cs showLineNumbers {2-3} await httpRemoteService.PostAsync("https://localhost:7044/HttpRemote/AddFile", builder => builder .SetMultipartContent(multipart => multipart .AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file"))); ``` If you need to upload multiple files, simply keep adding them to `multipart` (keeping the form name consistent, e.g. `files`): ```cs showLineNumbers {3-4} await httpRemoteService.PostAsync("https://localhost:7044/HttpRemote/AddFiles", builder => builder .SetMultipartContent(multipart => multipart .AddFileAsStream(@"C:\Workspaces\httptest.jpg", "files") .AddFileFromRemote("https://furion.net/img/furionlogo.png", "files"))); ``` In addition, the builder pattern is also supported, along with retrieving the return value of the upload. For more details, refer to Section 2.1. ```cs showLineNumbers {2} // Use the builder pattern await httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://localhost:7044/HttpRemote/AddFile") .SetMultipartContent(multipart => multipart .AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file"))); // For more detailed usage, refer to Section 2.1 ``` However, this approach to uploading file resources is not flexible enough when facing various complex scenarios — for example, it cannot track upload progress in real time, restrict upload file type and size, or implement resumable uploads. In addition, it may require developers to write more extra code. Therefore, the framework integrates features specifically designed for uploading file resources to address these issues. ## Uploading Using the Framework's Built-in Dedicated Upload Feature (Form Method) In applications such as video sharing, users typically need to view real-time progress when uploading files. For this purpose, you can use the `UploadFile` extension method, which supports retrieving progress in real time and allows restricting file type and size. The following example shows how to print the upload progress: ```cs showLineNumbers {2-5} await httpRemoteService.UploadFileAsync("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file" , async progress => { Console.WriteLine(await progress.ToSummaryStringAsync()); // Print brief progress information }); ``` Console output example: ```bash showLineNumbers Transferred 0.01MB of 0.01MB (100.00% complete, Speed: 0.86MB/s, Time: 0.01s, ETA: 0.00s), File: httptest.jpg, Path: C:\Workspaces\httptest.jpg. ``` If you need to display file upload progress in the console in real time, it is recommended to use the `UpdateConsoleProgressAsync()` method. An example follows: ```cs showLineNumbers {2,5} await httpRemoteService.UploadFileAsync("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file" , progress => progress.UpdateConsoleProgressAsync()); // Update the file transfer progress bar in the console // ✅ Or use the UploadFileWithConsoleProgressAsync method (with console progress printing) await httpRemoteService.UploadFileWithConsoleProgressAsync("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file"); ``` After execution, the console will display the following progress information: ```bash showLineNumbers {2} File: httptest.jpg, Path: C:\Workspaces\httptest.jpg. [##################################################] 61.35% (0.01MB/0.01MB) Speed: 0.86MB/s, Time: 0.01s, ETA: 0.00s. ``` If you need to restrict file type and size, do the following: ```cs showLineNumbers {1,6-7} await httpRemoteService.SendAsync(HttpRequestBuilder.UploadFile("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file" , async progress => { Console.WriteLine(await progress.ToSummaryStringAsync()); // Print brief progress information }) .SetAllowedFileExtensions(".jpg;.png") // Allow only jpg and png types .SetMaxFileSizeInBytes(5 * 1024 * 1024)); // Restrict file size to 5MB ``` If you need to append additional form parameters when uploading a file, do the following: ```cs showLineNumbers {6-9} await httpRemoteService.SendAsync(HttpRequestBuilder.UploadFile("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file" , async progress => { Console.WriteLine(await progress.ToSummaryStringAsync()); // Print brief progress information }) .WithMultipart(multipart => { multipart.AddText("Furion", "name"); }); ``` With the above approaches, you can flexibly meet various file upload requirements. > **About Multiple File Upload** The `UploadFile` extension approach supports only single-file upload and cannot handle multiple file uploads simultaneously. --- # 6.25 The HttpFileUploadBuilder Builder > Source: https://http.furion.net/en/docs/advanced-guide/the-httpfileuploadbuilder-builder/ In addition to the above methods, you can also use the `HttpFileUploadBuilder` builder to configure the various settings required for uploading file resources. ```cs showLineNumbers {1} await httpRemoteService.SendAsync(HttpRequestBuilder.UploadFile("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file"); ``` The `HttpFileUploadBuilder` builder is provided by the framework specifically to configure the various settings required for uploading file resources. The constructor of `HttpFileUploadBuilder` is private, so it cannot be instantiated directly with the `new` keyword; however, the framework provides several static overloads of `HttpRequestBuilder.UploadFile` to create an instance of `HttpFileUploadBuilder`. ```cs showLineNumbers HttpRequestBuilder.UploadFile(httpMethod, requestUri, filePath, name, onProgressChanged, fileName, configure); HttpRequestBuilder.UploadFile(requestUri, filePath, name, onProgressChanged, fileName, configure); // Defaults to a POST request ``` In addition, `HttpFileUploadBuilder` includes the following configuration capabilities: ```cs showLineNumbers {1,5,8-9,12,15,18,21,24,27,30-31,34,37} // Defaults to a POST request, with a default form name of file HttpRequestBuilder.UploadFile("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file") // Set the content type (file type) .SetContentType("image/jpeg") // Set the allowed file extensions .SetAllowedFileExtensions([".jpg", ".png"]) .SetAllowedFileExtensions(".jpg;.png") // Set the allowed file size, in bytes .SetMaxFileSizeInBytes(5 * 1024 * 1024) // Set the interval for file transfer progress (notifications) .SetProgressInterval(TimeSpan.FromSeconds(1)) // Set the action to perform when the file transfer starts .SetOnTransferStarted(() => {}) // Set the delegate to execute when the transfer progress changes .SetOnProgressChanged(async progress => { }) // Set the action to perform when the file transfer completes; the delegate parameter is the total time spent on the transfer (in milliseconds) .SetOnTransferCompleted(duration => {}) // Set the action to perform when the file transfer encounters an exception .SetOnTransferFailed(exception => {}) // Set the HTTP file transfer event handler; CustomFileTransferEventHandler is a type implementing the IHttpFileTransferEventHandler interface .SetEventHandler() .SetEventHandler(typeof(CustomFileTransferEventHandler)) // Append multipart form content .WithMultipart(multipart => {}); // Set the HttpRequestBuilder instance .With(builder => {}); // Supports further extensions ``` After successfully building the `HttpFileUploadBuilder` instance via the `HttpRequestBuilder.UploadFile` method, you can use the `Send` method or the asynchronous `SendAsync` method to perform the send operation. ```cs showLineNumbers httpRemoteService.Send(httpFileUploadBuilder, cancellationToken); await httpRemoteService.SendAsync(httpFileUploadBuilder, cancellationToken); ``` --- # 6.26 File Transfer Event Handler > Source: https://http.furion.net/en/docs/advanced-guide/file-transfer-event-handler-2/ The `IHttpFileTransferEventHandler` interface allows you to define preprocessing operations for downloading or uploading files. By implementing this interface, you can create a custom file transfer event handler, such as the `CustomFileTransferEventHandler` class: ```cs showLineNumbers {1} public class CustomFileTransferEventHandler : IHttpFileTransferEventHandler { // Action to perform when the file transfer starts public void OnTransferStarted() {} // Action to perform when the transfer progress changes public Task OnProgressChangedAsync(FileTransferProgress fileTransferProgress) {} // Action to perform when the file transfer completes public void OnTransferCompleted(long duration) {} // Action to perform when the file transfer encounters an exception public void OnTransferFailed(Exception exception) {} } ``` To enable this handler in your application, register the `CustomFileTransferEventHandler` service in the `Startup.cs` or `Program.cs` file: ```cs showLineNumbers services.TryAddSingleton(); ``` Next, you can specify this handler when building the `HTTP` request: ```cs showLineNumbers {2,5} HttpRequestBuilder.UploadFile("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file") .SetEventHandler(); HttpRequestBuilder.UploadFile("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file") .SetEventHandler(typeof(CustomFileTransferEventHandler)); // Set it using the type approach ``` > **Reuse Tip** You can create a custom type that implements the `IHttpFileTransferEventHandler` interface and reuse that implementation across multiple `HttpFileUploadBuilder` instances. > **Trigger Timing Notes** When an `HttpFileUploadBuilder` instance is configured with the `SetOnTransferStarted`, `SetOnProgressChanged`, `OnTransferCompleted`, or `OnTransferFailed` methods, these callback methods will be triggered. If the `IHttpFileTransferEventHandler` interface is also implemented, its methods (`OnTransferStarted`, `OnProgressChangedAsync`, `OnTransferCompleted`, and `OnTransferFailed`) will be invoked later than the series of methods set on the `HttpFileUploadBuilder` instance. > **Disable the Request Profiler** When printing request content, the `Stream` object may be read repeatedly or become unreadable. This is because the stream is read into memory ahead of time, and its position pointer moves to the end. This makes it impossible to obtain accurate upload progress. Therefore, when using the framework's dedicated upload feature, it is recommended to disable the request profiler to ensure accurate upload progress information is obtained. --- # 6.27 Stress and Simulation Testing > Source: https://http.furion.net/en/docs/advanced-guide/stress-and-simulation-testing/ When developing application systems that face the internet or must withstand concurrent access from many users, performance stress testing and automated interface simulation testing become critical steps before deployment. Using the report metrics obtained from these two types of testing, we can optimize the code before the system goes live and ensure it meets the minimum launch requirements. Taking the official website of the `Furion` framework as an example, run a stress test: ```cs showLineNumbers {1-2} var stressTestHarnessResult = await httpRemoteService.StressTestHarnessAsync("https://furion.net/"); Console.WriteLine(stressTestHarnessResult.ToString()); // Print the stress test result ``` Test result overview: ```bash showLineNumbers Stress Test Harness Result: Total Requests: 100 // Total number of requests Total Time (s): 7.95 // Total time (seconds) Successful Requests: 100 // Number of successful requests Failed Requests: 0 // Number of failed requests QPS: 12.58 // Queries per second (QPS) Min RT (ms): 676.38 // Minimum response time (milliseconds) Max RT (ms): 7,419.72 // Maximum response time (milliseconds) Avg RT (ms): 3,314.94 // Average response time (milliseconds) P10 RT (ms): 1,288.82 // P10 response time (milliseconds) P25 RT (ms): 2,057.10 // P25 response time (milliseconds) P50 RT (ms): 3,064.56 // P50 response time (milliseconds) P75 RT (ms): 4,100.03 // P75 response time (milliseconds) P90 RT (ms): 5,026.08 // P90 response time (milliseconds) P95 RT (ms): 7,330.71 // P95 response time (milliseconds) P99 RT (ms): 7,416.20 // P99 response time (milliseconds) P99.99 RT (ms): 7,419.72 // P99.99 response time (milliseconds) ``` The `stressTestHarnessResult` variable is of type `StressTestHarnessResult`, which contains the following properties and methods: - **Properties**: - `TotalRequests`: Total number of requests (`long` type). - `TotalTimeInSeconds`: Total time in seconds (`double` type). - `SuccessfulRequests`: Number of successful requests (`long` type). - `FailedRequests`: Number of failed requests (`long` type). - `QueriesPerSecond`: Queries per second (`QPS`) (`double` type). - `MinResponseTime`: Minimum response time in milliseconds (`double` type). - `MaxResponseTime`: Maximum response time in milliseconds (`double` type). - `AverageResponseTime`: Average response time in milliseconds (`double` type). - `Percentile10ResponseTime`: `P10` response time in milliseconds (`double` type). - `Percentile25ResponseTime`: `P25` response time in milliseconds (`double` type). - `Percentile50ResponseTime`: `P50` response time in milliseconds (`double` type). - `Percentile75ResponseTime`: `P75` response time in milliseconds (`double` type). - `Percentile90ResponseTime`: `P90` response time in milliseconds (`double` type). - `Percentile95ResponseTime`: `P95` response time in milliseconds (`double` type). - `Percentile99ResponseTime`: `P99` response time in milliseconds (`double` type). - `Percentile9999ResponseTime`: `P99.99` response time in milliseconds (`double` type). - **Methods**: - `ToString()`: Outputs an indented, detailed report string. By default, the stress test runs `1` round, each containing `100` concurrent requests, with a maximum concurrency of `100`. To obtain more accurate test results, adjust these parameters as needed: ```cs showLineNumbers {2-4,7,9} var stressTestHarnessResult = await httpRemoteService.SendAsync(HttpRequestBuilder.StressTestHarness("https://furion.net/") .SetNumberOfRequests(1000) // Set the number of concurrent requests .SetNumberOfRounds(5) // Set the number of stress test rounds .SetMaxDegreeOfParallelism(500)); // Set the maximum degree of concurrency // In most cases, you only need to set the number of concurrent requests var stressTestHarnessResult = await httpRemoteService.StressTestHarnessAsync("https://furion.net/", 500); var stressTestHarnessResult = await httpRemoteService.SendAsync(HttpRequestBuilder.StressTestHarness("https://furion.net/", 500)); ``` > **Generate Test Reports Quickly** When running a stress test, a `GET` request is used by default and the full response content is downloaded (`HttpCompletionOption.ResponseContentRead`). If the full response content is not needed, you can choose a `HEAD` request and set `completionOption` to `ResponseHeadersRead` to generate the stress test report quickly. > **Abuse Notice** **When running a stress test, the `X-Stress-Test: Harness` request header is automatically added to prevent abuse that could harm the target system.** In addition, since the test results are affected by various factors such as hardware devices, operating systems, and code implementation, they are for reference only. Furthermore, **to obtain more accurate data, the request profiler is disabled by default**. --- # 6.28 HttpStressTestHarnessBuilder Builder > Source: https://http.furion.net/en/docs/advanced-guide/httpstresstestharnessbuilder-builder/ The `HttpStressTestHarnessBuilder` builder provides the various settings the framework offers specifically for stress testing and simulation testing. The `HttpStressTestHarnessBuilder` constructor is private, so it cannot be instantiated directly with the `new` keyword; however, the framework provides several static overloads of `HttpRequestBuilder.StressTestHarness` to create an instance of `HttpStressTestHarnessBuilder`. ```cs showLineNumbers HttpRequestBuilder.StressTestHarness(httpMethod, requestUri, numberOfRequests, configure); HttpRequestBuilder.StressTestHarness(requestUri, numberOfRequests, configure); // Defaults to a GET request ``` In addition, `HttpStressTestHarnessBuilder` provides the following configuration capabilities: ```cs showLineNumbers {1,5,8,11,14,17} // Defaults to a GET request, with a default of 100 concurrent requests HttpRequestBuilder.StressTestHarness("https://furion.net/") // Sets the number of concurrent requests; default is 100 .SetNumberOfRequests(500) // Sets the maximum degree of parallelism; default is 100 .SetMaxDegreeOfParallelism(500) // Sets the number of stress-test rounds; default is 1 .SetNumberOfRounds(5) // Disables the HTTP cache .DisableCache() // Sets the HttpRequestBuilder instance .With(builder => {}); ``` After successfully building an `HttpStressTestHarnessBuilder` instance through the `HttpRequestBuilder.StressTestHarness` method, you can use the `Send` method or the asynchronous `SendAsync` method to perform the send operation. ```cs showLineNumbers httpRemoteService.Send(httpStressTestHarnessBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); await httpRemoteService.SendAsync(httpStressTestHarnessBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); ``` --- # 6.29 Long Polling > Source: https://http.furion.net/en/docs/advanced-guide/long-polling/ Long polling (`Long Polling`) is a technique for pushing data from the server to the client. It simulates the effect of server push by keeping the `HTTP` connection open until new data is sent to the client or until a timeout occurs. Long polling is an improvement over traditional polling (in which the client periodically sends requests to the server to check for new data), reducing unnecessary requests and improving efficiency. How long polling works: 1. The client sends a request to the server. 2. If there is no new data on the server, the server does not respond immediately but instead holds the request. 3. Once new data is available to send, or the preset timeout is reached, the server responds to the request and sends the data to the client. 4. After the client processes the data, it sends a new request to the server again, repeating the process above. ![long-polling](/images/long-polling.png) Use cases for long polling: - **Real-time notifications**: for example, in an online chat application, when a user receives a new message, the server can push the message to the client in a timely manner via long polling. - **Online collaboration tools**: in applications where multiple people edit a document simultaneously, long polling can be used to synchronize users' edits in real time. - **Game updates**: in online games, long polling can be used to update game state in real time, such as player position, score, and other information. - **Stock market updates**: financial applications use long polling to display stock price changes in real time. - **Configuration center**: in a microservices architecture, the configuration center uses long polling to ensure that each service can immediately receive the latest configuration changes. When a configuration changes, the configuration center can quickly push the update to all relevant service instances, ensuring configuration consistency and timeliness. The following example shows how to use a long polling request: ```cs showLineNumbers {1-2,9-11} await httpRemoteService.LongPollingAsync("https://localhost:7044/HttpRemote/LongPolling" , async (responseMessage, token) => { Console.WriteLine(await responseMessage.Content.ReadAsStringAsync(token)); await Task.CompletedTask; }, cancellationToken: cancellationToken); // Using the builder pattern await httpRemoteService.SendAsync(HttpRequestBuilder .LongPolling("https://localhost:7044/HttpRemote/LongPolling" , async (responseMessage, token) => { Console.WriteLine(await responseMessage.Content.ReadAsStringAsync(token)); await Task.CompletedTask; }), cancellationToken: cancellationToken); ``` Long polling also supports consuming data as `IAsyncEnumerable`, allowing you to use `await foreach` to iterate over each polling response: ```cs showLineNumbers {1,4,6,11,13,15} await foreach (var responseMessage in httpRemoteService.LongPollingAsAsyncEnumerable("https://localhost:7044/HttpRemote/LongPolling", cancellationToken: cancellationToken)) { // Note: each response must be disposed manually after use (or use using) using (responseMessage) { Console.WriteLine(await responseMessage.Content.ReadAsStringAsync(cancellationToken)); } } // Using the builder pattern await foreach (var responseMessage in httpRemoteService.SendAsAsyncEnumerable(HttpRequestBuilder.LongPolling("https://localhost:7044/HttpRemote/LongPolling"), cancellationToken)) { using (responseMessage) { Console.WriteLine(await responseMessage.Content.ReadAsStringAsync(cancellationToken)); } } ``` Although long polling addresses the need for real-time communication to a certain extent, it also has some drawbacks: for example, it may place significant pressure on the server under high concurrency, and long-lived connections may affect server performance. As `Web` technologies evolve, more advanced techniques such as `Server-Sent Events` or `WebSocket` have gradually become the preferred choice for real-time bidirectional communication. Nevertheless, in certain constrained environments, long polling remains a viable option. --- # 6.30 HttpLongPollingBuilder Builder > Source: https://http.furion.net/en/docs/advanced-guide/httplongpollingbuilder-builder/ The `HttpLongPollingBuilder` builder provides the various settings the framework offers specifically for sending long polling requests. The `HttpLongPollingBuilder` constructor is private, so it cannot be instantiated directly with the `new` keyword; however, the framework provides several static overloads of `HttpRequestBuilder.LongPolling` to create an instance of `HttpLongPollingBuilder`. ```cs showLineNumbers HttpRequestBuilder.LongPolling(httpMethod, requestUri, onDataReceived, configure); HttpRequestBuilder.LongPolling(requestUri, onDataReceived, configure); // Defaults to a GET request HttpRequestBuilder.LongPolling(httpMethod, requestUri, configure); HttpRequestBuilder.LongPolling(requestUri, configure); // Defaults to a GET request ``` In addition, `HttpLongPollingBuilder` provides the following configuration capabilities: ```cs showLineNumbers {1,10,13,16,19,22,25-26,29} // Defaults to a GET request HttpRequestBuilder.LongPolling("https://localhost:7044/HttpRemote/LongPolling" , async (responseMessage, token) => { Console.WriteLine(await responseMessage.Content.ReadAsStringAsync(cancellationToken)); await Task.CompletedTask; }) // Sets the polling retry interval; default is 2 seconds .SetRetryInterval(TimeSpan.FromSeconds(2)) // Sets the maximum number of retries; default is 100 .SetMaxRetries(500) // Sets the operation for receiving data when the server returns a 200~299 status code // .SetOnDataReceived(async responseMessage => {}) // Can be passed in at initialization // Sets the operation for receiving data when the server returns a status code other than 200~299 .SetOnError(async responseMessage => {}) // Sets the operation triggered when the response headers contain X-End-Of-Stream .SetOnEndOfStream(async ResponseMessage => {}) // Sets the long polling event handler .SetEventHandler() .SetEventHandler(typeof(CustomLongPollingEventHandler)) // Sets the HttpRequestBuilder instance .With(builder => {})); // Supports further extensions ``` After successfully building an `HttpLongPollingBuilder` instance through the `HttpRequestBuilder.LongPolling` method, you can use `Send`, `SendAsync`, or `SendAsAsyncEnumerable` to perform the send operation. ```cs showLineNumbers httpRemoteService.Send(httpLongPollingBuilder, cancellationToken); await httpRemoteService.SendAsync(httpLongPollingBuilder, cancellationToken); await foreach (var response in httpRemoteService.SendAsAsyncEnumerable(httpLongPollingBuilder, cancellationToken)) { // Process each response } ``` --- # 6.31 Long Polling Event Handler > Source: https://http.furion.net/en/docs/advanced-guide/long-polling-event-handler/ The `IHttpLongPollingEventHandler` interface allows you to define pre-processing operations for sending long polling requests. By implementing this interface, you can create a custom long polling event handler, such as the `CustomLongPollingEventHandler` class: ```cs showLineNumbers {1} public class CustomLongPollingEventHandler : IHttpLongPollingEventHandler { // Used to receive data when the server returns a 200~299 status code public Task OnDataReceivedAsync(HttpResponseMessage httpResponseMessage, CancellationToken cancellationToken) {} // Used to receive data when the server returns a status code other than 200~299 public Task OnErrorAsync(HttpResponseMessage httpResponseMessage, CancellationToken cancellationToken) {} // Used for the operation triggered when the response headers contain X-End-Of-Stream public Task OnEndOfStreamAsync(HttpResponseMessage httpResponseMessage, CancellationToken cancellationToken) {} } ``` To enable this handler in your application, register the `CustomLongPollingEventHandler` service in the `Startup.cs` or `Program.cs` file: ```cs showLineNumbers services.TryAddSingleton(); ``` Next, you can specify this handler when building the `HTTP` request: ```cs showLineNumbers {7,15} HttpRequestBuilder.LongPolling("https://localhost:7044/HttpRemote/LongPolling" , async (responseMessage, token) => { Console.WriteLine(await responseMessage.Content.ReadAsStringAsync(cancellationToken)); await Task.CompletedTask; }) .SetEventHandler(); HttpRequestBuilder.LongPolling("https://localhost:7044/HttpRemote/LongPolling" , async (responseMessage, token) => { Console.WriteLine(await responseMessage.Content.ReadAsStringAsync(cancellationToken)); await Task.CompletedTask; }) .SetEventHandler(typeof(CustomLongPollingEventHandler)); // Using the type-based approach ``` > **Reuse Tip** You can create a custom type that implements the `IHttpLongPollingEventHandler` interface and reuse that implementation across multiple `HttpLongPollingBuilder` instances. > **Trigger Timing Notes** When an `HttpLongPollingBuilder` instance configures the `SetOnDataReceived`, `SetOnError`, or `SetOnEndOfStream` methods, these callback methods will be triggered. If the `IHttpLongPollingEventHandler` interface is also implemented, its methods (`OnDataReceivedAsync`, `OnErrorAsync`, and `OnEndOfStreamAsync`) will be invoked later than the methods configured on the `HttpLongPollingBuilder` instance. --- # 6.32 Terminating a Long Polling Request > Source: https://http.furion.net/en/docs/advanced-guide/terminating-a-long-polling-request/ In addition to using `CancellationToken` to cancel a long polling request, the framework also checks the response headers for `X-End-Of-Stream`; if that header is present, it terminates the long polling request. --- # 6.33 Server-Sent Events Unidirectional Communication > Source: https://http.furion.net/en/docs/advanced-guide/server-sent-events-unidirectional-communication/ With the rapid rise in popularity of the AI chatbot `ChatGPT`, the typewriter-effect conversation design in its user interface left a deep impression. This vivid, lifelike interactive experience is actually achieved through a technology called "Server-Sent Events" (`Server-Sent Events`, `SSE`). `Server-Sent Events` is a communication technology that allows the server to proactively send real-time update data to the client (usually a browser). **Unlike the traditional client-request / server-response pattern, `SSE` implements unidirectional, asynchronous communication from the server to the client, thereby eliminating the need for the client to continually poll the server for the latest data.** This technology greatly reduces the burden on the server and improves the efficiency and real-time nature of data transmission. Use cases for `Server-Sent Events`: 1. **Real-time notifications**: It can be used to implement real-time message alerts or notification systems, such as new-message prompts on social networks or email arrival notifications. 2. **Data stream updates**: For data that needs to be continuously updated, such as stock prices, weather information, or sports results, `SSE` can provide instant data updates. 3. **Progress reporting**: When executing long-running tasks, such as file uploads or complex computations, `SSE` can be used to report task progress to the client. 4. **Logs and monitoring**: In the development and operations domains, `SSE` can be used to display changes in log files in real time or to monitor the health status of systems. The following example shows how to use `Server-Sent Events` to retrieve data from the server: ```cs showLineNumbers {1,3,11,13} await httpRemoteService.ServerSentEventsAsync("https://localhost:7044/HttpRemote/Events" // Action to perform when data is received , async (data, token) => { Console.WriteLine(data.Data); await Task.CompletedTask; }, cancellationToken: cancellationToken); // Using the builder pattern await httpRemoteService.SendAsync(HttpRequestBuilder .ServerSentEvents("https://localhost:7044/HttpRemote/Events" // Action to perform when data is received , async (data, token) => { Console.WriteLine(data.Data); await Task.CompletedTask; }), cancellationToken: cancellationToken); ``` `Server-Sent Events` also supports consuming data as `IAsyncEnumerable`, allowing you to use `await foreach` to iterate over each polled response: ```cs showLineNumbers {1,3,7,9} await foreach (var data in httpRemoteService.ServerSentEventsAsAsyncEnumerable("https://localhost:7044/HttpRemote/Events", cancellationToken: cancellationToken)) { Console.WriteLine(data.Data); } // Using the builder pattern await foreach (var data in httpRemoteService.SendAsAsyncEnumerable(HttpRequestBuilder.ServerSentEvents("https://localhost:7044/HttpRemote/Events"), cancellationToken)) { Console.WriteLine(data.Data); } ``` The `data` parameter is of type `ServerSentEventsData`, which contains the following properties: - **Properties**: - `Event`: The event type (of type `string`). - `Data`: The message (of type `string`). - `RawLine`: The raw message line (of type `string`). - `Id`: The event `ID` (of type `string`). - `Retry`: The reconnection interval (of type `int`, in milliseconds). - `CustomFields`: Custom field data (of type `IReadOnlyCollection>`). You can also listen for the events that fire when the connection opens and when an error occurs: ```cs showLineNumbers {9,14,29,34} await httpRemoteService.ServerSentEventsAsync("https://localhost:7044/HttpRemote/Events" // Action to perform when data is received , async (data, token) => { Console.WriteLine(data.Data); await Task.CompletedTask; }, builder => builder // Action when the connection is opened .SetOnOpen(() => { Console.WriteLine("Connected."); }) // Action when the connection fails to open .SetOnError((ex) => { Console.WriteLine("Connection error: " + ex.Message); }), cancellationToken: cancellationToken); // Using the builder pattern await httpRemoteService.SendAsync(HttpRequestBuilder .ServerSentEvents("https://localhost:7044/HttpRemote/Events" // Action to perform when data is received , async (data, token) => { Console.WriteLine(data.Data); await Task.CompletedTask; }) // Action when the connection is opened .SetOnOpen(() => { Console.WriteLine("Connected."); }) // Action when the connection fails to open .SetOnError((ex) => { Console.WriteLine("Connection error: " + ex.Message); }), cancellationToken: cancellationToken); ``` `Server-Sent Events` is especially well suited to scenarios where the server needs to send updates to the client but the client does not need to send requests to the server frequently. Whether for updating data in real time, providing progress reports, or implementing a simple notification system, `SSE` is a choice worth considering. > **Disabling the request profiler** When sending `Server-Sent Events` (server-sent events), because it returns data as a streaming `Stream`, enabling the request profiler causes each part of the streamed data to be loaded into memory and read ahead of time. This not only severely impacts the real-time display of the streamed data, but can also cause excessive memory usage when a large amount of data is returned. Therefore, it is recommended to disable the request profiler when sending `Server-Sent Events` requests. > **Request verb notes** Standard `Server-Sent Events (SSE)` only supports receiving server-pushed events via the `GET` method. However, the framework supports configuring `SSE` through any request verb (such as `POST` in the example): ```cs showLineNumbers {} HttpRequestBuilder .ServerSentEvents(HttpMethod.Post, new Uri("https://localhost:7044/HttpRemote/Events")); ``` --- # 6.34 HttpServerSentEventsBuilder builder > Source: https://http.furion.net/en/docs/advanced-guide/httpserversenteventsbuilder-builder/ The `HttpServerSentEventsBuilder` builder is provided by the framework specifically to configure the various settings needed to receive `Server-Sent Events` pushed by the server. The constructor of `HttpServerSentEventsBuilder` is private, so it cannot be instantiated directly with the `new` keyword; however, the framework provides multiple static overload methods of `HttpRequestBuilder.ServerSentEvents` to create an instance of `HttpServerSentEventsBuilder`. ```cs showLineNumbers HttpRequestBuilder.ServerSentEvents(requestUri, onMessage, configure); // Defaults to a GET request HttpRequestBuilder.ServerSentEvents(httpMethod, requestUri, onMessage, configure); HttpRequestBuilder.ServerSentEvents(requestUri, configure); // Defaults to a GET request HttpRequestBuilder.ServerSentEvents(httpMethod, requestUri, configure); ``` In addition, `HttpServerSentEventsBuilder` provides the following configuration capabilities: ```cs showLineNumbers {1,10,13,16,19,22,25-26,30,33} // Defaults to a GET request HttpRequestBuilder.ServerSentEvents("https://localhost:7044/HttpRemote/Events" , async (data, token) => { Console.WriteLine(data.Data); await Task.CompletedTask; }) // Sets the default reconnection interval; defaults to 2 seconds .SetDefaultRetryInterval(2000) // Unit: milliseconds // Sets the maximum number of retries; defaults to 100 .SetMaxRetries(500) // Sets the action to perform when the connection to the event source is opened .SetOnOpen(() => {}) // Sets the action to perform when data is received from the event source // .SetOnMessage(async (data, token) => {}) // Can be passed in initially // Sets the action to perform when the connection to the event source fails to open .SetOnError(exception => {}) // Sets the Server-Sent Events event handler .SetEventHandler() .SetEventHandler(typeof(CustomServerSentEventsEventHandler)) // Sets whether to automatically correct the request method // If true, when the request is GET or HEAD and contains request content, the method is automatically changed to POST; defaults to true .SetAutoCorrectMethod(true) // Sets the HttpRequestBuilder instance .With(builder => {})); // Supports more extensions ``` After successfully building a `HttpServerSentEventsBuilder` instance through the `HttpRequestBuilder.ServerSentEvents` method, you can use `Send`, `SendAsync`, or `SendAsAsyncEnumerable` to perform the send operation. ```cs showLineNumbers httpRemoteService.Send(httpServerSentEventsBuilder, cancellationToken); await httpRemoteService.SendAsync(httpServerSentEventsBuilder, cancellationToken); await foreach (var data in httpRemoteService.SendAsAsyncEnumerable(httpServerSentEventsBuilder, cancellationToken)) { // Process each item of data } ``` --- # 6.35 Server-Sent Events event handler > Source: https://http.furion.net/en/docs/advanced-guide/server-sent-events-event-handler/ The `IHttpServerSentEventsEventHandler` interface allows you to define pre-processing operations for receiving `Server-Sent Events` pushed by the server. By implementing this interface, you can create a custom `Server-Sent Events` event handler, such as the `CustomServerSentEventsEventHandler` class: ```cs showLineNumbers {1} public class CustomServerSentEventsEventHandler : IHttpServerSentEventsEventHandler { // Action to perform when the connection to the event source is opened void OnOpen(); // Action to perform when data is received from the event source Task OnMessageAsync(ServerSentEventsData serverSentEventsData, CancellationToken cancellationToken); // Action to perform when the connection to the event source fails to open void OnError(Exception exception); } ``` To enable this handler in your application, register the `CustomServerSentEventsEventHandler` service in the `Startup.cs` or `Program.cs` file: ```cs showLineNumbers services.TryAddSingleton(); ``` Next, you can specify this handler when building the `HTTP` request: ```cs showLineNumbers {7,15} HttpRequestBuilder.ServerSentEvents("https://localhost:7044/HttpRemote/Events" , async (data, token) => { Console.WriteLine(data.Data); await Task.CompletedTask; }) .SetEventHandler(); HttpRequestBuilder.ServerSentEvents("https://localhost:7044/HttpRemote/Events" , async (data, token) => { Console.WriteLine(data.Data); await Task.CompletedTask; }) .SetEventHandler(typeof(CustomServerSentEventsEventHandler)); // Using the type-based approach ``` > **Reuse tip** You can create a custom implementation type of the `IHttpServerSentEventsEventHandler` interface and reuse that implementation across multiple `HttpServerSentEventsBuilder` instances. > **Trigger timing notes** When a `HttpServerSentEventsBuilder` instance is configured with the `SetOnOpen`, `SetOnMessage`, or `SetOnError` methods, these callback methods will be triggered. If the `IHttpServerSentEventsEventHandler` interface is also implemented, its methods (`OnOpen`, `OnMessageAsync`, and `OnError`) will be invoked after the set of methods configured on the `HttpServerSentEventsBuilder` instance. --- # 6.36 WebSocket Duplex Communication > Source: https://http.furion.net/en/docs/advanced-guide/websocket-duplex-communication/ `WebSocket` is a protocol that performs full-duplex communication over a single `TCP` connection. `WebSocket` makes data exchange between the client and the server simpler, allowing the server to actively push data to the client. In the `WebSocket API`, the browser and the server only need to complete a single handshake, after which they can directly create a persistent connection and carry out bidirectional data transmission. The application scenarios of `WebSocket`: - **Real-time chat applications**: `WebSocket` enables real-time message delivery, making communication between users nearly latency-free. - **Online games**: For games that require fast responses, `WebSocket` can provide low-latency data transmission. - **Stock market updates**: Update stock prices and other financial information in real time. - **Collaborative editing tools**: Allow multiple users to edit the same document simultaneously and see each other's changes in real time. - **Real-time map applications**: For example, real-time traffic condition updates in navigation applications. The following example shows how to use `WebSocketClient` to connect to a server: ```cs showLineNumbers {1,4,10,16,22,29,32,38,45,49} using var webSocketClient = new WebSocketClient("wss://ws.postman-echo.com/raw"); // Supports ws:// and wss:// // Connected event webSocketClient.Connected += (sender, s) => { Console.WriteLine("Connected"); return Task.CompletedTask; }; // Connection closed event webSocketClient.Closed += (sender, args) => { Console.WriteLine("Connection closed"); return Task.CompletedTask; }; // Receive text messages webSocketClient.TextReceived += (sender, s) => { Console.WriteLine(s.Message); return Task.CompletedTask; }; // Receive binary messages webSocketClient.BinaryReceived += (sender, s) => { Console.WriteLine(s.Message); return Task.CompletedTask; }; // Connect to the server await webSocketClient.ConnectAsync(); // Start a task that sends messages in a loop _ = Task.Run(async () => { var i = 0; while (i < 5) { // Send a text message await webSocketClient.SendAsync("Hello, WebSocket!"); await Task.Delay(1000); i++; } // Close the connection await webSocketClient.CloseAsync(); }); // Wait for receive-message and close events (blocking) await webSocketClient.WaitAsync(); ``` The difference between `WebSocket` and `Server-Sent Events (SSE)`: - **Communication direction**: `WebSocket` supports full-duplex bidirectional communication, while `SSE` only supports the server pushing data to the client unidirectionally. - **Protocol**: `WebSocket` uses the standalone `WebSocket` protocol (`ws://` or `wss://`), while `SSE` is based on the `HTTP` protocol. - **Handshake process**: `WebSocket` requires a special `HTTP` upgrade header to switch protocols, while `SSE` requires no special handshake and establishes the connection directly through an `HTTP` request. - **Connection persistence**: A `WebSocket` connection persists until it is explicitly closed, while `SSE` may be disconnected due to network issues, but the browser will automatically reconnect. - **Data format**: `WebSocket` supports multiple data formats, including binary data, while the `SSE` data format is relatively fixed and is usually simple text messages. - **Cross-origin support**: `WebSocket` checks the cross-origin policy when establishing the connection and is unrestricted afterward, while `SSE` depends on the `CORS` policy. Whether to use `WebSocket` or `SSE` mainly depends on the specific application requirements: - If bidirectional communication or handling large data streams is required, `WebSocket` is the better choice; - If you only need the server to push updates to the client and have low requirements for the data format, `SSE` may be lighter-weight and easier to implement. --- # 6.37 The WebSocketClient Client > Source: https://http.furion.net/en/docs/advanced-guide/the-websocketclient-client/ The framework includes the built-in `WebSocketClient` type, making it easy for users to establish a connection with a `WebSocket` server via the `ws` or `wss` protocol. To use the `WebSocket` functionality, you first need to create and initialize an instance of `WebSocketClient`. The following details all the functional configuration options available on a `WebSocketClient` instance: - **Creating a `WebSocketClient` Client** The following are three ways to create a `WebSocketClient` instance. They are implemented through different constructor overloads, but essentially all of them ultimately call the constructor with the `WebSocketClientOptions` parameter to configure the connection: ```cs showLineNumbers {2,5,8,11} // Use a URL string directly (supports ws:// and wss://) using var webSocketClient = new WebSocketClient("wss://localhost:7044/ws"); // Use a Uri object using var webSocketClient = new WebSocketClient(new Uri("wss://localhost:7044/ws")); // Use a WebSocketClientOptions object for detailed configuration using var webSocketClient = new WebSocketClient(new WebSocketClientOptions("wss://localhost:7044/ws")); // Configure the internal ClientWebSocketOptions instance using var webSocketClient = new WebSocketClient("wss://localhost:7044/ws", options => {}); ``` The `WebSocketClientOptions` type contains a variety of configuration properties for customizing the detailed settings of the [`ClientWebSocket`](https://learn.microsoft.com/zh-cn/dotnet/api/system.net.websockets.clientwebsocket?view=net-9.0) connection. `WebSocketClientOptions` includes the following properties: - **Properties**: - `ServerUri`: The server address (`Uri` type). - `ReconnectInterval`: The reconnect interval (in milliseconds); the default value is `2` seconds. (`TimeSpan` type). - `MaxReconnectRetries`: The maximum number of reconnect attempts, defaulting to `10`. (`int` type). - `Timeout`: The timeout duration (`TimeSpan` type). - `ReceiveBufferSize`: The size of the buffer for receiving new messages from the server (an `int` type in bytes). - `Configure`: Configures the internal `ClientWebSocketOptions` instance (`Action` type). --- - **`WebSocketClient` Client Events** The `WebSocketClient` client provides a variety of events, allowing developers to insert custom logic at various stages of `WebSocket` communication. The following example shows how to subscribe to these events: ```cs showLineNumbers {4,7,10,13,16,19,22,25,28,31} using var webSocketClient = new WebSocketClient("wss://localhost:7044/ws"); // Supports ws:// and wss:// // Event triggered when connection starts webSocketClient.Connecting += (s, e) => {}; // Event triggered when the connection succeeds webSocketClient.Connected += (s, e) => { }; // Event triggered when reconnecting starts webSocketClient.Reconnecting += (s, e) => { }; // Event triggered when reconnecting succeeds webSocketClient.Reconnected += (s, e) => { }; // Event triggered when closing starts webSocketClient.Closing += (s, e) => { }; // Event triggered when the connection is closed successfully webSocketClient.Closed += (s, e) => { }; // Event triggered when message receiving starts webSocketClient.ReceivingStarted += (s, e) => { }; // Event triggered when message receiving stops webSocketClient.ReceivingStopped += (s, e) => { }; // Event for receiving text messages; result is of type WebSocketTextReceiveResult webSocketClient.TextReceived += (s, result) => { }; // Event for receiving binary messages; result is of type WebSocketBinaryReceiveResult webSocketClient.BinaryReceived += (s, result) => { }; ``` The `WebSocketTextReceiveResult` type derives from [`WebSocketReceiveResult`](https://learn.microsoft.com/zh-cn/dotnet/api/system.net.websockets.websocketreceiveresult?view=net-9.0) and includes the following properties: - **Properties**: - `Message`: The text message (`string` type). - For other properties, refer to [WebSocketReceiveResult](https://learn.microsoft.com/zh-cn/dotnet/api/system.net.websockets.websocketreceiveresult?view=net-9.0) The `WebSocketBinaryReceiveResult` type derives from [`WebSocketReceiveResult`](https://learn.microsoft.com/zh-cn/dotnet/api/system.net.websockets.websocketreceiveresult?view=net-9.0) and includes the following properties: - **Properties**: - `Message`: The binary message (`byte[]` type). - For other properties, refer to [WebSocketReceiveResult](https://learn.microsoft.com/zh-cn/dotnet/api/system.net.websockets.websocketreceiveresult?view=net-9.0) --- **`WebSocketClient` Client Methods** The `WebSocketClient` class encapsulates the key operations for interacting with a `WebSocket` server, specifically three methods: connecting, sending messages, and closing the connection. ```cs showLineNumbers {4,7-9,12,15,16} using var webSocketClient = new WebSocketClient("wss://localhost:7044/ws"); // Supports ws:// and wss:// // Connect to the server await webSocketClient.ConnectAsync(cancellationToken); // Send a message to the server await webSocketClient.SendAsync(message, endOfMessage, cancellationToken); // Send a string message await webSocketClient.SendAsync(byteArray, endOfMessage, cancellationToken); // Send a binary message await webSocketClient.SendAsync(message, webSocketMessageType, endOfMessage, cancellationToken); // Send a message of the specified type (text or binary) // Wait for messages (blocking) await webSocketClient.WaitAsync(cancellationToken); // Close the connection await webSocketClient.CloseAsync(cancellationToken); // Close without additional information await webSocketClient.CloseAsync(closeStatus, closeDescription, cancellationToken); // Close providing close status and description ``` --- # 6.38 HttpContext Forwarding and Proxying > Source: https://http.furion.net/en/docs/advanced-guide/httpcontext-forwarding-and-proxying/ `HttpContext` forwarding refers to the process, within an `ASP.NET Core` application, of forwarding the context information of one `HTTP` request (including request headers, request content, query strings, response headers, response content, and so on) from one request to another internal request or service. This technique allows developers to redirect a request to another processing point without changing the client request, thereby implementing request proxying or routing functionality. Use cases for `HttpContext` forwarding: - **`API Gateway` pattern**: Acts as the entry point for all external requests, routing requests to the correct backend services. - **Load balancing and failover**: Forwards requests to other available service instances to ensure system stability and reliability. - **Request logging and auditing**: Records request information to a logging system or auditing service for easier monitoring and debugging. - **Security filtering and validation**: Checks the request's authentication information and permissions during forwarding to ensure the legitimacy of the request. - **A/B testing and blue-green deployment**: Routes a portion of traffic to a new version of a service to gradually validate new features. - **Cross-origin request handling**: Handles cross-origin requests to ensure that requests execute successfully. Before using `HttpContext` for forwarding operations, make sure you have completed the following two steps: > **Standalone Library Notes** If you are using the standalone `HttpAgent` library, install `HttpAgent.AspNetCore` instead of `HttpAgent`. 1. Register and enable the `IHttpContextAccessor` service. Register and enable the `IHttpContextAccessor` service in the `Startup.cs` or `Program.cs` file, and configure the forwarding target allowlist. ```cs showLineNumbers {1,4,7} services.AddHttpContextAccessor(); // Not required with the Furion framework (already injected by default) // Globally configure HttpContext forwarding configuration options services.Configure(options => { // Allowlist of target hosts allowed for forwarding; must be configured explicitly. If not configured or empty, any forwarding via the X-Forward-To header will be rejected options.AllowedHosts = ["*"]; // "*" means allow all hosts and protocols (high risk; recommended only in trusted environments) }); ``` **Detailed explanation of the `AllowedHosts` allowlist rules:** - `"furion.net"` — Hostname only; matches the default port (`80/443`) of any protocol (`http/https`). - `"furion.net:8080"` — Host + port; matches the specified port of any protocol. - `"furion.net:*"` — Host + port wildcard; matches any port under any protocol. - `"https://furion.net"` — Protocol + host; matches only the default port of the specified protocol. - `"http://furion.net:8080"` — Protocol + host + port; exact match. - `"https://furion.net:*"` — Protocol + host + port wildcard; matches any port of the specified protocol only. - `"[::1]"` — `IPv6` host (wrapped in square brackets); matches the default port of any protocol. - `"[::1]:8080"` — `IPv6` host + port; matches the specified port of any protocol. - `"[::1]:*"` — `IPv6` host + port wildcard; matches any port under any protocol. - `"http://[2001:db8::1]:8080"` — Protocol + IPv6 host + port; exact match. - `"*"` — Global wildcard; allows any host and protocol (completely bypasses all host validation). > **Security Risk Notice** - **Always configure `AllowedHosts` explicitly**; leaving it empty or unconfigured rejects all `X-Forward-To` forwarding requests to prevent `SSRF` attacks. - Using the global wildcard `*` fully exposes the application to `SSRF` risk; enable it only when you fully trust the request source (such as an internal management service) and understand the risks. - Whenever possible, use the strictest rules (such as specifying the protocol and port) and combine them with a network firewall to restrict outbound traffic. - All hostname and protocol matching is case-insensitive to prevent case-confusion bypasses. 2. Enable the request body buffering middleware to support repeated reading of the request content. ```cs showLineNumbers app.UseEnableBuffering(); ``` 3. **(Optional)** If a certificate error such as `The SSL connection could not be established, see inner exception.` occurs during forwarding, you can add the following configuration to ignore `SSL` certificate validation: ```cs showLineNumbers {3,6-7,12,14,17-18} // Default client configuration services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { // Ignore SSL certificate validation ServerCertificateCustomValidationCallback = HttpRemoteUtility.IgnoreSslErrors, SslProtocols = HttpRemoteUtility.AllSslProtocols }); // If using SocketsHttpHandler, you can ignore SSL certificate validation with the following configuration services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler() { SslOptions = new SslClientAuthenticationOptions { // Ignore SSL certificate validation RemoteCertificateValidationCallback = HttpRemoteUtility.IgnoreSocketSslErrors, EnabledSslProtocols = HttpRemoteUtility.AllSslProtocols }, }); ``` The following is a simple example showing how to implement `HttpContext` forwarding in `ASP.NET Core`: ```cs showLineNumbers {3,10,18-19,27-28,35-35} [ApiController] [Route("[controller]/[action]")] public class GetStartController(IHttpRemoteService httpRemoteService, IHttpContextAccessor httpContextAccessor) : ControllerBase { // Forward proxy to a website [HttpGet] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] // Disable browser caching public Task ForwardToWebSite() { return httpContextAccessor.HttpContext.ForwardAsResultAsync("https://github.com"); } // Forward proxy to an image [HttpGet] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] // Disable browser caching public Task ForwardToImage() { return httpContextAccessor.HttpContext.ForwardAsResultAsync( "https://img-s-msn-com.akamaized.net/tenant/amp/entityid/AA1u7RJI.img?w=584&h=326&m=6"); } // Forward proxy to a download [HttpGet] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] // Disable browser caching public Task ForwardToDownload() { return httpContextAccessor.HttpContext.ForwardAsResultAsync( "https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe"); } // Forward proxy to a form [HttpPost] public Task ForwardToForm(int id, [FromForm] YourRemoteFormModel model) { return httpContextAccessor.HttpContext.ForwardAsAsync( "https://localhost:7044/HttpRemote/AddForm"); } } ``` > **The `X-Forward-To` Request Header** In addition to manually configuring the forwarding target address, the system also supports automatically setting the target address by parsing the `X-Forward-To` request header. **Note**: When using this header, the target host must be in the `AllowedHosts` allowlist; otherwise the forwarding will be rejected. With `HttpContext` forwarding, you can combine `Middleware` technology in `ASP.NET Core` applications to implement flexible request routing and handling mechanisms, suitable for various scenarios such as `API Gateway`, load balancing, request logging, security validation, and more. > **Possible Causes of `GET` Request Forwarding Failures** In certain special scenarios, such as forwarding a `GET` request to a specific file or image, forwarding may fail. This may be caused by `TLS/SSL` certificate issues. In such cases, make sure that the target application used for forwarding is deployed over the `HTTPS` protocol. --- # 6.39 The HttpContextForwardBuilder Builder > Source: https://http.furion.net/en/docs/advanced-guide/the-httpcontextforwardbuilder-builder/ The `HttpContextForwardBuilder` builder is provided by the framework specifically for configuring the various settings required to convert the `HttpContext` request context. The constructor of `HttpContextForwardBuilder` is private, so it cannot be instantiated directly with the `new` keyword. However, the framework provides multiple extension method overloads of `HttpContext.CreateForwardBuilder` to create instances of `HttpContextForwardBuilder`. ```cs showLineNumbers httpContext.CreateForwardBuilder(httpMethod, requestUri, forwardOptions); // The type of the forwardOptions parameter is HttpContextForwardOptions httpContext.CreateForwardBuilder(requestUri, forwardOptions); // Automatically obtains HttpContext.Request.Method for configuration ``` The framework does not yet provide the ability to manually construct `HttpContextForwardBuilder` and forward using the `Send/SendAsync` methods; please use the `ForwardAsync`-related methods instead. --- # 6.40 HttpContextForwardOptions Configuration Options > Source: https://http.furion.net/en/docs/advanced-guide/httpcontextforwardoptions-configuration-options/ The `HttpContextForwardOptions` type is specifically used to configure the forwarding behavior of `HttpContext`. You can register and configure this service in the project's `Startup.cs` or `Program.cs` file: ```cs showLineNumbers {2-3,9} // Register in the HttpRemote service services.AddHttpRemote(builder => {}) .ConfigureForwardOptions(options => // .ConfigureForwardOptions((options, serviceProvider) => { // Add custom configuration here }); // Register in services services.Configure(options => { // Add custom configuration here }); ``` In addition, you can manually create an `HttpContextForwardOptions` instance and pass it in when forwarding: ```cs showLineNumbers {1} httpContext.ForwardAsResult("https://furion.net", forwardOptions: new HttpContextForwardOptions { // Add custom configuration here }); ``` The `HttpContextForwardOptions` contains the following properties: - **Properties**: - **`AllowedHosts`**: The allowlist of target hosts allowed for forwarding (type `ICollection?`). Used to prevent **Server-Side Request Forgery (`SSRF`)** attacks. Forwarding is allowed only when the host of the target address (including port and protocol) matches one of the entries in the list. **Supported formats** (matching is case-insensitive): - `"furion.net"` – Hostname only; matches the default port (`80/443`) of any protocol (`http/https`). - `"furion.net:8080"` – Host + port; matches the specified port of any protocol. - `"furion.net:*"` – Host + port wildcard; matches any port under any protocol. - `"https://furion.net"` – Protocol + host; matches only the default port of the specified protocol. - `"http://furion.net:8080"` – Protocol + host + port; exact match. - `"https://furion.net:*"` – Protocol + host + port wildcard; matches any port of the specified protocol only. - `"*"` – Global wildcard; allows any host and protocol (completely bypasses validation, **high risk**). **If not configured or empty, all target addresses specified via the `X-Forward-To` request header will be rejected** to prevent unauthorized forwarding. Whenever possible, use exact rules and only open the wildcard to fully trusted sources. - `WithQueryParameters`: Whether to forward query parameters (`URL` parameters); default value is `true` (type `bool`). - `WithRequestHeaders`: Whether to forward request headers; default value is `true` (type `bool`). - `WithResponseStatusCode`: Whether to forward the response status code; default value is `true` (type `bool`). - `WithResponseHeaders`: Whether to forward response headers; default value is `true` (type `bool`). - `WithResponseContentHeaders`: Whether to forward response content headers; default value is `true` (type `bool`). - `ResetHostRequestHeader`: Whether to reset the `Host` request header; default value is `false` (type `bool`). - `IgnoreQueryParameters`: List of query parameters (`URL` parameters) to skip during forwarding (type `string[]?`). - `IgnoreRequestHeaders`: List of request headers to skip during forwarding (type `string[]?`). - `IgnoreResponseHeaders`: List of response headers to skip during forwarding (type `string[]?`). - `OnForward`: Used to perform custom operations before forwarding the response (type `Action`). > **Security Risk Notice** - `AllowedHosts` is the core configuration for preventing `SSRF` attacks; be sure to set it explicitly in production environments and avoid using the `"*"` wildcard. - `ResetHostRequestHeader` may need to be enabled when some target servers require validation of the `Host` header. Its default value is `false`; adjust it according to your actual situation. --- # 6.41 HttpContext Forwarding Extension Methods > Source: https://http.furion.net/en/docs/advanced-guide/httpcontext-forwarding-extension-methods/ The framework provides various extension methods for `HttpContext` to meet the `HTTP` request forwarding needs of various scenarios. ```cs showLineNumbers {1,7,13,19,25,31,37,43} // Returns an HttpResponseMessage object httpContext.Forward(requestUri, configure, completionOption, forwardOptions); // Automatically obtains HttpContext.Request.Method for configuration httpContext.Forward(httpMethod, requestUri, configure, completionOption, forwardOptions); await httpContext.ForwardAsync(requestUri, configure, completionOption, forwardOptions); // Automatically obtains HttpContext.Request.Method for configuration await httpContext.ForwardAsync(httpMethod, requestUri, configure, completionOption, forwardOptions); // Returns HttpRemoteResult httpContext.Forward(requestUri, configure, completionOption, forwardOptions); // Automatically obtains HttpContext.Request.Method for configuration httpContext.Forward(httpMethod, requestUri, configure, completionOption, forwardOptions); await httpContext.ForwardAsync(requestUri, configure, completionOption, forwardOptions); // Automatically obtains HttpContext.Request.Method for configuration await httpContext.ForwardAsync(httpMethod, requestUri, configure, completionOption, forwardOptions); // Returns the target type T httpContext.ForwardAs(requestUri, configure, completionOption, forwardOptions); // Automatically obtains HttpContext.Request.Method for configuration httpContext.ForwardAs(httpMethod, requestUri, configure, completionOption, forwardOptions); await httpContext.ForwardAsAsync(requestUri, configure, completionOption, forwardOptions); // Automatically obtains HttpContext.Request.Method for configuration await httpContext.ForwardAsAsync(httpMethod, requestUri, configure, completionOption, forwardOptions); // Returns a string type httpContext.ForwardAsString(requestUri, configure, completionOption, forwardOptions); // Automatically obtains HttpContext.Request.Method for configuration httpContext.ForwardAsString(httpMethod, requestUri, configure, completionOption, forwardOptions); await httpContext.ForwardAsStringAsync(requestUri, configure, completionOption, forwardOptions); // Automatically obtains HttpContext.Request.Method for configuration await httpContext.ForwardAsStringAsync(httpMethod, requestUri, configure, completionOption, forwardOptions); // Returns a byte array type httpContext.ForwardAsByteArray(requestUri, configure, completionOption, forwardOptions); // Automatically obtains HttpContext.Request.Method for configuration httpContext.ForwardAsByteArray(httpMethod, requestUri, configure, completionOption, forwardOptions); await httpContext.ForwardAsByteArrayAsync(requestUri, configure, completionOption, forwardOptions); // Automatically obtains HttpContext.Request.Method for configuration await httpContext.ForwardAsByteArrayAsync(httpMethod, requestUri, configure, completionOption, forwardOptions); // Returns a Stream type httpContext.ForwardAsStream(requestUri, configure, completionOption, forwardOptions); // Automatically obtains HttpContext.Request.Method for configuration httpContext.ForwardAsStream(httpMethod, requestUri, configure, completionOption, forwardOptions); await httpContext.ForwardAsStreamAsync(requestUri, configure, completionOption, forwardOptions); // Automatically obtains HttpContext.Request.Method for configuration await httpContext.ForwardAsStreamAsync(httpMethod, requestUri, configure, completionOption, forwardOptions); // Returns an IActionResult type httpContext.ForwardAsResult(requestUri, configure, completionOption, forwardOptions); // Automatically obtains HttpContext.Request.Method for configuration httpContext.ForwardAsResult(httpMethod, requestUri, configure, completionOption, forwardOptions); await httpContext.ForwardAsResultAsync(requestUri, configure, completionOption, forwardOptions); // Automatically obtains HttpContext.Request.Method for configuration await httpContext.ForwardAsResultAsync(httpMethod, requestUri, configure, completionOption, forwardOptions); // Returns an object type httpContext.ForwardAs(resultType, requestUri, configure, completionOption, forwardOptions); // Automatically obtains HttpContext.Request.Method for configuration httpContext.ForwardAs(resultType, httpMethod, requestUri, configure, completionOption, forwardOptions); await httpContext.ForwardAsAsync(resultType, requestUri, configure, completionOption, forwardOptions); // Automatically obtains HttpContext.Request.Method for configuration await httpContext.ForwardAsAsync(resultType, httpMethod, requestUri, configure, completionOption, forwardOptions); ``` > **Forwarding Results as the `IActionResult` Type** When using forwarding that returns an `IActionResult` type, you can flexibly forward various content, including web pages, images, resource downloads and uploads, videos, audio, `JSON` data, files, and more. --- # 6.42 Advantages of HttpContext Forwarding > Source: https://http.furion.net/en/docs/advanced-guide/advantages-of-httpcontext-forwarding/ When integrating with third-party `API` endpoints, the common approach is to create an entry program and call the `HTTP` remote request service within it to send requests to the specified third-party endpoint. Assume the third-party endpoint's controller is defined as follows: ```cs showLineNumbers {5-9} [ApiController] [Route("[controller]/[action]")] public class VendorController : ControllerBase { [HttpPost] public VendorModel Add(VendorModel model) { return model; } } ``` The traditional approach is to use the `HTTP` remote request service to send the request, for example: ```cs showLineNumbers {3,8-11} [ApiController] [Route("[controller]/[action]")] public class YourController(IHttpRemoteService httpRemoteService) : ControllerBase { [HttpPost] public async Task AddVendorAsync() { return await httpRemoteService.SendAsync( HttpRequestBuilder.Post("https://www.furion.net/vendor/add") .SetJsonContent(new VendorModel()) ); } } ``` However, by using the `HttpContext` forwarding feature, we can simplify the code. We only need to create a controller declaration that matches the third-party endpoint's interface, such as the `Add` method of `VendorController`, as shown below: ```cs showLineNumbers {3,9-10} [ApiController] [Route("[controller]/[action]")] public class YourController(IHttpContextAccessor httpContextAccessor) : ControllerBase { [HttpPost] public async Task AddAsync(VendorModel model) // Both synchronous and asynchronous are supported { // Automatically forwards the model, no additional setup required return await httpContextAccessor.Context .ForwardAsync("https://www.furion.net/vendor/add"); } } ``` In this way, the code becomes more concise and clear. Instead of manually building and sending the `HTTP` request, we leverage the `HttpContext` forwarding feature to directly invoke the third-party endpoint. > **Leveraging the `X-Forward-To` Request Header to Improve Code Flexibility** In addition to explicitly setting the target address in code, we can also automatically obtain the forwarding address through the client request's `X-Forward-To` header. Assuming the client has already set the request header `X-Forward-To: https://www.furion.net/vendor/add`, our code can be further simplified as follows: ```cs showLineNumbers {3,9} [ApiController] [Route("[controller]/[action]")] public class YourController(IHttpContextAccessor httpContextAccessor) : ControllerBase { [HttpPost] public async Task AddAsync(VendorModel model) // Both synchronous and asynchronous are supported { // Automatically resolves the X-Forward-To header address from the request return await httpContextAccessor.Context.ForwardAsync(); } } ``` In this way, the code becomes even more concise and flexible, and is able to dynamically forward any content to the target address. This approach has enormous potential, and you can explore and experiment with it according to your needs. --- # 6.43 Applying HttpContext Forwarding in Microservices > Source: https://http.furion.net/en/docs/advanced-guide/applying-httpcontext-forwarding-in-microservices/ In a microservices architecture, the `HttpContext` forwarding feature demonstrates significant value. Communication between microservices typically relies on `HTTP` or `gRPC`, among which `HTTP` is widely adopted due to its excellent compatibility. Adopting `HttpContext` forwarding not only reduces the amount of `HTTP` request code, but also makes the code structure clearer and easier to maintain. This advantage is particularly evident for large projects or team collaboration projects. With the `HttpContext` forwarding feature, we can implement dynamic request distribution. By applying a certain weighting algorithm, the system can automatically forward requests to different servers, thereby achieving load balancing and failover. For example: ```cs showLineNumbers {3,13-26} [ApiController] [Route("[controller]/[action]")] public class YourController(IHttpContextAccessor httpContextAccessor) : ControllerBase { [HttpPost] public async Task AddAsync(VendorModel model) { string targetUrl = "https://furion.net/"; // Default server address // Select the target server address based on some algorithm (e.g., a load balancing strategy) // In a microservices architecture, this is typically determined by the service registration and discovery mechanism // The code below is only a simulated example if (condition1) { targetUrl = "https://s1.furion.net/"; // s1 server address } else if (condition2) { targetUrl = "https://s2.furion.net/"; // s2 server address } else if (condition3) { targetUrl = "https://s3.furion.net/"; // s3 server address } return await _httpContextAccessor.ForwardAsync(targetUrl); } } ``` In addition, **the `HttpContext` forwarding feature also makes building a gateway center possible. All external requests can be sent to the gateway center first, where the gateway performs authentication, rate limiting, and other processing before forwarding them to the target service**. This greatly improves the system's security and manageability. In summary, `HttpContext` forwarding is an indispensable component in a microservices architecture, providing strong support for efficient and flexible microservice communication. --- # 6.44 Notes on Headers Ignored During Forwarding > Source: https://http.furion.net/en/docs/advanced-guide/notes-on-headers-ignored-during-forwarding/ When using the `HttpContext` forwarding feature, the system automatically ignores the following request and response headers to ensure the validity and accuracy of the forwarding: - **Request headers that will be ignored**: - `X-Forward-To` - `Host` - `Content-Length` - **Response headers that will be ignored**: - `Content-Type` - `Transfer-Encoding` - `Keep-Alive` - `Upgrade` - `Proxy-Connection` If you need to add more settings for ignored request or response headers, you can configure them through `HttpContextForwardOptions`. There are two configuration approaches: - **Global configuration**: In the project's `Startup.cs` or `Program.cs` file, register and configure the service: ```cs showLineNumbers {1,4,7} services.Configure(options => { // List of request headers to skip during forwarding options.IgnoreRequestHeaders = ["Framework"]; // List of response headers to skip during forwarding options.IgnoreResponseHeaders = ["Content-Length"]; }); ``` - **Per-forward configuration**: For a single forward, you can pass `HttpContextForwardOptions` for configuration: ```cs showLineNumbers {1,4,7} httpContext.ForwardAsResult("https://furion.net", forwardOptions: new HttpContextForwardOptions { // List of request headers to skip during forwarding options.IgnoreRequestHeaders = ["Framework"]; // List of response headers to skip during forwarding options.IgnoreResponseHeaders = ["Content-Length"]; }); ``` > **About Forwarding the `Content-Length` Response Content Header** If the response headers contain `Content-Length` and its value does not match the actual size of the response content, it may cause the `"Error while copying content to a stream."` error. Ignoring this header helps avoid errors caused by length mismatch. --- # 6.45 The ForwardAttribute Forwarding Attribute > Source: https://http.furion.net/en/docs/advanced-guide/the-forwardattribute-forwarding-attribute/ To simplify forwarding operations, the framework provides the convenient `[Forward]` controller action forwarding attribute. Compared to manually calling the `HttpContext` `Forward` extension methods, this attribute significantly reduces repetitive hardcoding. The following is an example of using the `[Forward]` attribute: ```cs showLineNumbers {11,14,23,26,35,38,48,52,59,62,70,73} [ApiController] [Route("[controller]/[action]")] public class GetStartController : ControllerBase { /// /// Forwards/proxies to the website /// /// [HttpGet] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] // Disable browser caching [Forward("https://github.com", AllowedHosts = ["*"])] public Task ForwardToWebSite() { throw new NotImplementedException(); } /// /// Forwards/proxies to the image /// /// [HttpGet] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] // Disable browser caching [Forward("https://img-s-msn-com.akamaized.net/tenant/amp/entityid/AA1u7RJI.img?w=584&h=326&m=6", AllowedHosts = ["*"])] public Task ForwardToImage() { throw new NotImplementedException(); } /// /// Forwards/proxies to the file /// /// [HttpGet] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] // Disable browser caching [Forward("https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe", AllowedHosts = ["*"])] public Task ForwardToDownload() { throw new NotImplementedException(); } /// /// Forwards/proxies to the form /// /// /// /// [HttpPost] [Forward("https://localhost:7044/HttpRemote/AddForm", AllowedHosts = ["*"])] public Task ForwardToForm(int id, [FromForm] YourRemoteFormModel model) { throw new NotImplementedException(); } /// /// Forwards/proxies to the string /// /// [HttpGet] [Forward("https://localhost:7044/GetStart/PostRawString", AllowedHosts = ["*"])] public Task ForwardToString() { throw new NotImplementedException(); } /// /// Forwards/proxies to no return value /// /// [HttpGet] [Forward("https://localhost:7044/GetStart/PostRawString", AllowedHosts = ["*"])] public Task ForwardToVoid() { throw new NotImplementedException(); } } ``` In the code above, we only need to add the `[Forward]` attribute to the controller actions that need to be forwarded and specify the target `URL`. The framework automatically handles the forwarding logic, so no implementation code needs to be written in the method body (typically a `NotImplementedException` is thrown to indicate that this is a forwarding operation handled automatically by the framework). This approach is particularly convenient in microservice applications, greatly simplifying code writing and maintenance. > **Scope of the `ForwardAttribute` Attribute** The `ForwardAttribute` attribute applies to methods. `ForwardAttribute` contains the following properties: - **Properties**: - `RequestUri`: the forwarding address (type `string`). - `Method`: the forwarding method. If not set, the current request method is automatically adopted as the forwarding method (type `HttpMethod`). - `HttpClientName`: the configuration name of the `HttpClient` instance, with a default value of `null` (type `string`). - `CompletionOption`: indicates how the response content is handled, with a default value of `ResponseHeadersRead` (type `HttpCompletionOption`). - **`AllowedHosts`**: the allowlist of target hosts permitted for forwarding (type `string[]?`). Used to defend against **Server-Side Request Forgery (`SSRF`)** attacks. Forwarding is permitted only when the target address's host (including port and protocol) matches one of the entries in the list. **Supported formats** (matching is case-insensitive): - `"furion.net"` – hostname only, matches the default ports (`80/443`) of any protocol (`http/https`). - `"furion.net:8080"` – host + port, matches the specified port of any protocol. - `"furion.net:*"` – host + port wildcard, matches any port under any protocol. - `"https://furion.net"` – protocol + host, matches only the default port of the specified protocol. - `"http://furion.net:8080"` – protocol + host + port, exact match. - `"https://furion.net:*"` – protocol + host + port wildcard, matches only any port of the specified protocol. - `"*"` – global wildcard, allows any host and protocol (completely bypasses validation, **high risk**). **If not configured or empty, all target addresses specified through the `X-Forward-To` request header will be rejected** to prevent unauthorized forwarding. It is recommended to use precise rules whenever possible, and only open wildcards to fully trusted sources. - `WithQueryParameters`: whether to forward query parameters (`URL` parameters), default value `true` (type `bool`). - `WithRequestHeaders`: whether to forward request headers, default value `true` (type `bool`). - `WithResponseStatusCode`: whether to forward the response status code, default value `true` (type `bool`). - `WithResponseHeaders`: whether to forward response headers, default value `true` (type `bool`). - `WithResponseContentHeaders`: whether to forward response content headers, default value `true` (type `bool`). - `ResetHostRequestHeader`: whether to reset the `Host` request header, default value `false` (type `bool`). - `IgnoreQueryParameters`: the list of query parameters (`URL` parameters) to skip when forwarding (type `string[]?`). - `IgnoreRequestHeaders`: the list of request headers to skip when forwarding (type `string[]?`). - `IgnoreResponseHeaders`: the list of response headers to skip when forwarding (type `string[]?`). > **Security Risk Warning** - `AllowedHosts` is the core configuration for preventing `SSRF` attacks. Make sure to set it explicitly in production environments and avoid using the `"*"` wildcard. - `ResetHostRequestHeader` may need to be enabled when certain target servers require validating the `Host` header. Its default value is `false`; adjust it according to your actual situation. --- # 6.46 HTTP Request Pipeline Handler > Source: https://http.furion.net/en/docs/advanced-guide/pipeline/ The `HTTP` request pipeline handler is the core mechanism by which the framework sends `HTTP` remote requests; the final execution logic that actually issues the request is completed through the collaboration of this series of handlers. Almost all key features—such as automatic redirects, request analysis, timeout management, retry policies, exception suppression, request assertions, and automatic `Access Token` management—are each implemented by an independent pipeline handler. Each handler focuses on a single responsibility, making it easy to extend and maintain. To customize a pipeline handler, simply implement the `IHttpRequestPipelineHandler` interface. For example, the following example implements a simple printing handler that outputs logs before and after a request: ```cs showLineNumbers {1,4,6,9,11} internal sealed class PrintPipelineHandler : IHttpRequestPipelineHandler { /// public async Task HandleAsync(HttpRequestPipelineContext context, Func> next) { Console.WriteLine("Before the request"); // Call the next handler's delegate var httpResponseMessage = await next(); Console.WriteLine("Response received"); return httpResponseMessage; } } ``` Then, when configuring the `HttpRemote` service in `Startup.cs` or `Program.cs`, register the handler via `AddPipelineHandler` to enable it: ```cs showLineNumbers {3} services.AddHttpRemote(builder => { builder.AddPipelineHandler(); }); ``` After registration, each remote request prints "Before the request" before being sent and prints "Response received" after receiving a response, allowing you to observe the request lifecycle intuitively. > **Request Pipeline Handler Execution Order** When registering, the **last** registered handler is placed at the **head** of the handler collection, that is, the outermost layer of the pipeline; the **first** registered one is located at the innermost layer. The overall execution order follows the **outer-to-inner** principle: the first type in the list executes first (outermost layer), and the last type executes last (innermost layer). This design allows outer handlers to uniformly handle cross-cutting concerns (such as logging and authentication), while inner handlers focus on the core request logic. `HttpRequestPipelineContext` contains the following properties: - **Properties**: - `OriginalBuilder`: The original `HttpRequestBuilder` (`HttpRequestBuilder` type). - `Builder`: The currently used `HttpRequestBuilder` (`HttpRequestBuilder` type). - `HttpClient`: The `HttpClient` instance (`HttpClient` type). - `CompletionOption`: Indicates how the response content is handled (`HttpCompletionOption` type). - `CancellationToken`: The currently effective cancellation token (`CancellationToken` type). - `SendAsync`: The send delegate (`Func>` type). **The delegate that actually issues the `HTTP` request.** - `RequestMessage`: The most recently built `HttpRequestMessage` (`HttpRequestMessage?` type). - `ResponseMessage`: The most recently built `HttpResponseMessage` (`HttpResponseMessage?` type). - `RequestDuration`: The request duration (milliseconds) (`long` type). - `Items`: The shared data dictionary (`IDictionary` type) To learn about all of the built-in request pipeline handlers in the framework, refer to the repository source code: [https://github.com/monksoul/HttpAgent/tree/master/src/HttpAgent/src/Pipelines/Handlers](https://github.com/monksoul/HttpAgent/tree/master/src/HttpAgent/src/Pipelines/Handlers) --- # 6.47 FTP Client Feature Outlook > Source: https://http.furion.net/en/docs/advanced-guide/ftp/ > **`FTP` Client Feature Notes** Since Google Chrome announced the discontinuation of support for the `FTP` protocol in 2019, Microsoft followed suit in 2022 and removed support for the `FTP` protocol from its Edge browser. Although our framework has not yet officially integrated `FTP` client functionality, the related development work has been initially completed. If the demand for `FTP` protocol functionality grows among developers in the future, we will seriously consider incorporating it into the framework to meet the needs of developers. --- # 6.48 Canceling HTTP Requests with CancellationToken > Source: https://http.furion.net/en/docs/advanced-guide/cancellation/ The framework provides cancellation functionality for all methods that send `HTTP` remote requests, which can be achieved simply through the `cancellationToken` parameter. The following are examples of three ways to cancel requests: - **Define a `CancellationToken` parameter in the controller `Action`**: When the user closes the browser or interrupts the request, the `HTTP` remote request operation can be canceled. ```cs showLineNumbers {3,6,8} [ApiController] [Route("[controller]/[action]")] public class HttpRemoteController(IHttpRemoteService httpRemoteService) : ControllerBase { [HttpGet] public async Task GetContent(CancellationToken cancellationToken) { await httpRemoteService.GetAsAsync("https://furion.net/", cancellationToken: cancellationToken); } } ``` - **Use the `HttpContext.RequestAborted` property**: When the user closes the browser or interrupts the request, the `HTTP` remote request operation can be canceled. ```cs showLineNumbers {4,10} [ApiController] [Route("[controller]/[action]")] public class HttpRemoteController(IHttpRemoteService httpRemoteService, IHttpContextAccessor httpContextAccessor) : ControllerBase { [HttpGet] public async Task GetContent() { await httpRemoteService.GetAsAsync("https://furion.net/" , cancellationToken: httpContextAccessor.HttpContext.RequestAborted); } } ``` - **Create a `CancellationTokenSource` object to cancel the request manually**: Creating a `CancellationTokenSource` instance allows precise control over when to cancel the `HTTP` request. ```cs showLineNumbers {8-9,12} [ApiController] [Route("[controller]/[action]")] public class HttpRemoteController(IHttpRemoteService httpRemoteService) : ControllerBase { [HttpGet] public async Task GetContent() { using var cancellationTokenSource = new CancellationTokenSource(); cancellationTokenSource.CancelAfter(100); await httpRemoteService.GetAsAsync("https://furion.net/" , cancellationToken: cancellationTokenSource.Token); } } ``` --- # 6.49 HttpClient Instance Configuration > Source: https://http.furion.net/en/docs/advanced-guide/httpclient-instance-configuration/ The `HTTP` remote request service uses `HttpClient` internally to send requests. By configuring the `HttpClient` client, you can adjust the behavior of the framework when sending `HTTP` remote requests. You can configure client behavior in the following two ways: **Global Configuration** Configure clients for all requests or for a specific name in `Startup.cs` or `Program.cs`: ```cs showLineNumbers {2,5,8} // Enable for the default client services.AddHttpClient(string.Empty, client => {}); // Enable for a specific client services.AddHttpClient("weixin", client => {}); // If you need to resolve services, use the following overload services.AddHttpClient(string.Empty, (serviceProvider, client) => {}); ``` **Per-Request Configuration** Use `SetHttpClientProvider` to customize the client for a specific request: ```cs showLineNumbers {2-5} HttpRequestBuilder.Get("https://furion.net/") .SetHttpClientProvider(() => (new HttpClient(new HttpClientHandler { // ... }), client => client.Dispose())); ``` --- # 6.50 Common Property Configuration > Source: https://http.furion.net/en/docs/advanced-guide/common-property-configuration/ ```cs showLineNumbers {2,5,8,11,14,17,20,24} // Enable for the default client services.AddHttpClient(string.Empty, client => { // Configure the base address client.BaseAddress = new Uri("http://localhost:5000"); // Configure the timeout client.Timeout = TimeSpan.FromMinutes(10); // Configure the maximum buffer size for response content client.MaxResponseContentBufferSize = 5 * 1024; // Configure the default HTTP version client.DefaultRequestVersion = HttpVersion.Version10; // Configure default request headers, such as "User-Agent" client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0"); // Configure the version policy used by default when establishing an HTTP connection client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher; }); // Enable for a specific client services.AddHttpClient("weixin", client => { // Configure HttpClient properties }); ``` --- # 6.51 Configuring IHttpClientBuilder > Source: https://http.furion.net/en/docs/advanced-guide/configuring-ihttpclientbuilder/ The `.AddHttpClient(name, configure)` method returns an `IHttpClientBuilder` instance, allowing further configuration, such as setting the `HttpMessageHandler`. ### Example of Configuring Default and Specific Clients ```cs showLineNumbers {2-3,16-17} // Configure the default client services.AddHttpClient(string.Empty, client => {}) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { // Allow automatic redirects AllowAutoRedirect = true, // Use default credentials UseDefaultCredentials = true, // Enable cookies UseCookies = true, }); // Configure the specific client named "weixin" services.AddHttpClient("weixin", client => {}) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler()); ``` ### Uniformly Configuring All `HttpClient` Instances In addition to configuring each client individually, you can also configure all `HttpClient` instances uniformly: ```cs showLineNumbers {1,3,8,10} services.ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler()); }); // Or use the IHttpRemoteBuilder extension method for one-click configuration services.AddHttpRemote() .ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler()); }); ``` This configuration approach makes the code more concise and modular, and easier to maintain and manage. --- # 6.52 Configuring SSL Certificates > Source: https://http.furion.net/en/docs/advanced-guide/httpclient-ssl/ When using `HttpClient` to make `HTTPS` requests, if you need to configure a custom `SSL/TLS` certificate, this typically involves using the `HttpClientHandler` class and specifying a callback method through the `ServerCertificateCustomValidationCallback` property to validate the server's certificate. If you need to use a client certificate, you can add it through the `HttpClientHandler.ClientCertificates` property. ### Client Certificate Authentication ```cs showLineNumbers {5-8} services.AddHttpClient(string.Empty, client => {}) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { ClientCertificates = { X509CertificateLoader.LoadPkcs12FromFile("path/to/client_certificate.pfx", "password") } }); ``` ### Custom Server Certificate Validation If you need to customize the server certificate validation logic, you can set the `ServerCertificateCustomValidationCallback` property: ```cs showLineNumbers {5-15} services.AddHttpClient(string.Empty, client => {}) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => { // If the certificate is the expected self-signed certificate, accept it if (cert.Subject == "CN=YourExpectedSubject") { return true; // Accept the certificate } // Otherwise, use the default validation logic return errors == System.Net.Security.SslPolicyErrors.None; } }); ``` ### Ignoring `SSL` Certificate Validation In addition to configuring `SSL` certificates, you can also ignore `SSL` certificate validation by adding the following configuration: ```cs showLineNumbers {3,6-7,12,14,17-18} // Default client configuration services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { // Ignore SSL certificate validation ServerCertificateCustomValidationCallback = HttpRemoteUtility.IgnoreSslErrors, SslProtocols = HttpRemoteUtility.AllSslProtocols }); // If using SocketsHttpHandler, you can ignore SSL certificate validation with the following configuration services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler() { SslOptions = new SslClientAuthenticationOptions { // Ignore SSL certificate validation RemoteCertificateValidationCallback = HttpRemoteUtility.IgnoreSocketSslErrors, EnabledSslProtocols = HttpRemoteUtility.AllSslProtocols }, }); ``` ### Setting the `SSL` Certificate for a Single Request In addition to validating `SSL` certificates in global configuration, you can also set the `SSL` certificate for a single request using the `SetHttpClientProvider` method. The sample code is as follows: ```cs showLineNumbers {2-7} HttpRequestBuilder.Get("https://furion.net/") .SetHttpClientProvider(() => (new HttpClient(new HttpClientHandler { // Ignore SSL certificate validation ServerCertificateCustomValidationCallback = HttpRemoteUtility.IgnoreSslErrors, SslProtocols = HttpRemoteUtility.AllSslProtocols }), client => client.Dispose())); ``` --- # 6.53 Configuring the Proxy Server > Source: https://http.furion.net/en/docs/advanced-guide/configuring-the-proxy-server/ If you need to send requests through a proxy server, you can do so by configuring the `Proxy` property of `HttpClientHandler`. The following example shows how to configure a proxy server, including basic authentication. ```cs showLineNumbers {5-9} services.AddHttpClient(string.Empty, client => {}) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler // or use new SocketsHttpHandler {} { Proxy = new WebProxy("http://proxyserver:8080", false) { Credentials = new NetworkCredential("username", "password") // if authentication is required }, UseProxy = true // UseProxy is true by default; ensure it is enabled }); ``` --- # 6.54 Configuring HTTP/3 Support > Source: https://http.furion.net/en/docs/advanced-guide/configuring-http3-support/ `HTTP/3` is the latest version of the `HTTP` protocol, built on top of the `QUIC` (Quick UDP Internet Connections) protocol, and is designed to provide lower latency, higher throughput, and improved multiplexing. You can add the following configuration to enable `HTTP/3` support: ```cs showLineNumbers {2,5,8} // Default client configuration services.AddHttpClient(string.Empty, client => { // Set the default request version to HTTP/3 client.DefaultRequestVersion = HttpVersion.Version30; // Specify the version policy as exact request version match to ensure HTTP/3 is used client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact; }); ``` With the above configuration, we can ensure that the specified `HTTP` version (i.e., `HTTP/3`) is strictly used when sending remote `HTTP` requests, thereby taking full advantage of the performance improvements and functional benefits provided by `HTTP/3`. --- # 6.55 HttpClientOptions Extended Configuration (JSON Serialization) > Source: https://http.furion.net/en/docs/advanced-guide/httpclientoptions-extended-configuration-json-serialization/ The framework supports customizing options for `HttpClient` (such as `JSON` serialization behavior) through the `IHttpClientBuilder.ConfigureOptions()` extension method, and provides an overload that supports injectable service resolution. The example is as follows: ```cs showLineNumbers {3,10} // Configure the default client services.AddHttpClient(string.Empty) .ConfigureOptions(options => // or use the overload: .ConfigureOptions((options, serviceProvider) => { options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); // Configure a specific client services.AddHttpClient("github") .ConfigureOptions(options => // or use the overload: .ConfigureOptions((options, serviceProvider) => { options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); ``` With this configuration, you can customize the `JSON` serialization behavior for different `HTTP` client instances to meet the needs of specific remote requests. `HttpClientOptions` contains the following properties: - **Properties**: - `JsonSerializerOptions`: `JSON` serialization configuration (of type `JsonSerializerOptions`). - `JsonResponseWrapper`: Specifies the `JSON` response deserialization wrapper (of type `JsonResponseWrapper?`). - `UseJsonResponseWrapper`: Whether to globally enable the `JSON` response deserialization wrapper (of type `bool`). - `AccessTokenProvider`: `Access Token` provider configuration (of type `IHttpAccessTokenProvider?`). - `RequestEventHandler`: `HTTP` event handler provider configuration (of type `IHttpRequestEventHandler?`). - `QuotaLimits`: API call quota limit configuration (of type `Dictionary?`). --- # 6.56 Service Resolution Configuration > Source: https://http.furion.net/en/docs/advanced-guide/service-resolution-configuration/ When configuring `HttpClient` globally, it is sometimes necessary to resolve dependent services before configuring the client. For example, the following example sets the request base address by resolving the configuration service: ```cs showLineNumbers {1,4} services.AddHttpClient(string.Empty, (serviceProvider, client) => { // Resolve the configuration service var configuration = serviceProvider.GetRequiredService(); client.BaseAddress = new Uri(configuration["your-base-address"]); }); ``` --- # 6.57 Learn More About Configuration > Source: https://http.furion.net/en/docs/advanced-guide/learn-more-about-configuration/ For more information about the configuration of `HttpMessageHandler` and `SocketsHttpHandler`, please refer to the Microsoft official documentation: - [`HttpClientHandler` Class](https://learn.microsoft.com/zh-cn/dotnet/api/system.net.http.httpclienthandler?view=net-9.0) - [`SocketsHttpHandler` Class](https://learn.microsoft.com/zh-cn/dotnet/api/system.net.http.socketshttphandler?view=net-9.0) In addition, the framework creates `HttpClient` instances through `IHttpClientFactory` at the underlying layer. For detailed information about `IHttpClientFactory`, please refer to the Microsoft official documentation: - [Using `IHttpClientFactory` in `.NET`](https://learn.microsoft.com/zh-cn/dotnet/core/extensions/httpclient-factory) - [Making `HTTP` Requests in `ASP.NET Core` with `IHttpClientFactory`](https://learn.microsoft.com/zh-cn/aspnet/core/fundamentals/http-requests?view=aspnetcore-8.0) --- # 6.58 DelegatingHandler Request Processing Delegation (Interception) > Source: https://http.furion.net/en/docs/advanced-guide/delegatinghandler-request-processing-delegation-interception/ `DelegatingHandler` is an important component of the `HttpClient` class in C#, and it allows you to process `HTTP` requests and responses in a chained manner. `DelegatingHandler` is an abstract class typically used to create custom message handlers that can be inserted into the `HTTP` message processing pipeline. By inheriting from `DelegatingHandler` and overriding its `Send` and `SendAsync` methods, you can implement custom behaviors such as adding request headers, logging, authentication, error handling, and more. **Simply put, `DelegatingHandler` can be viewed as "middleware" that intercepts remote `HTTP` requests.** --- # 6.59 DelegatingHandler Use Cases > Source: https://http.furion.net/en/docs/advanced-guide/delegatinghandler-use-cases/ - **Pre-request processing**: Before the request is sent to the server, the request can be modified or enhanced, such as adding authentication information, setting specific request headers, and so on. - **Post-response processing**: After a response is received from the server but before the end user sees the result, the response can be processed, such as decompression, decryption, caching the response, and so on. - **Error handling**: Handle all exceptions that may occur in `HTTP` requests in a unified way, providing a consistent error handling mechanism. - **Logging**: Record request and response information for easier debugging and monitoring. The framework's built-in `ProfilerDelegatingHandler` is a request analysis tool that implements its functionality by implementing the `DelegatingHandler` interface. You can [click here to view the source code](https://github.com/monksoul/HttpAgent/blob/master/src/HttpAgent/src/Delegates/ProfilerDelegatingHandler.cs) to gain a deeper understanding of its implementation details. --- # 6.60 Custom DelegatingHandler > Source: https://http.furion.net/en/docs/advanced-guide/custom-delegatinghandler/ The following is a simple `DelegatingHandler` example showing how to add a custom request header before the request is sent and log the response status code after the response is received: ```csharp showLineNumbers {1,3,9,12,18} public class CustomHandler : DelegatingHandler { protected override HttpResponseMessage Send(HttpRequestMessage httpRequestMessage, CancellationToken cancellationToken) { // See the async SendAsync method return base.Send(httpRequestMessage, cancellationToken); } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { // Request pre-processing: add a custom request header request.Headers.Add("Custom-Header", "Value"); // Send the request and get the response var response = await base.SendAsync(request, cancellationToken); // Response post-processing: log the response status code Console.WriteLine($"Response status code: {response.StatusCode}"); return response; } } ``` > **About Dependency Injection** Derived classes of `DelegatingHandler` support dependency injection through the constructor. Next, register `CustomHandler` in the `Program.cs` or `Startup.cs` file: ```csharp showLineNumbers {2,6,10} // Register CustomHandler as a service services.TryAddSingleton(); // Enable CustomHandler for the default HttpClient client services.AddHttpClient() .AddHttpMessageHandler(); // To enable it for a specific named HttpClient client, configure as follows // services.AddHttpClient("weixin") // .AddHttpMessageHandler(); ``` The `.AddHttpMessageHandler` method can be called multiple times to add multiple handlers. For example: ```csharp showLineNumbers {2-4} services.AddHttpClient(string.Empty) .AddHttpMessageHandler() .AddHttpMessageHandler() .AddHttpMessageHandler(); ``` **Note**: Handlers execute in registration order, meaning handlers registered earlier execute first. --- # 6.61 Implementing Automatic Authorization Token Refresh > Source: https://http.furion.net/en/docs/advanced-guide/implementing-automatic-authorization-token-refresh/ > **Better Approach** It is recommended to prefer the approach introduced in the **2.22 Automatic `Access Token` Management** section, which has a more complete built-in refresh mechanism and exception handling, so there is no need to manually write a `DelegatingHandler`. The following content is retained as a traditional implementation reference for scenarios that require fully customized refresh logic. When integrating with third-party `API` interfaces, it is usually necessary to carry an authorization `Token` in the request header. Because a `Token` is time-sensitive, developers need to refresh the `Token` periodically. The following example shows how to implement automatic `Token` refresh logic via `DelegatingHandler`. When the response returns a `401` status code, the system automatically re-fetches the authorization `Token` and updates the request header. ```cs showLineNumbers {13,16,19-22,25,29,32,36} public class AuthorizationDelegatingHandler : DelegatingHandler { protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) { // See the SendAsync code return base.Send(request, cancellationToken); } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { // Clone the original request (to solve the problem that StreamContent can only be read once) var clonedRequest = await request.CloneAsync(cancellationToken); // Send the request for the first time var response = await base.SendAsync(clonedRequest, cancellationToken); // Detect the 401 status code if (response.StatusCode != HttpStatusCode.Unauthorized) { return response; } // Refresh the Token var newToken = await GetNewTokenAsync(); // Implement the logic for getting a new Token // Clone the request again and add the new Token clonedRequest = await clonedRequest.CloneAsync(cancellationToken); clonedRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", newToken); // Retry the request response = await base.SendAsync(clonedRequest, cancellationToken); return response; } private async Task GetNewTokenAsync() { // Implement the logic for getting a new Token here // For example: call the authentication service to get a new Token return "newToken"; } } ``` Next, register `AuthorizationDelegatingHandler` in the `Program.cs` or `Startup.cs` file: ```csharp showLineNumbers {2,6,10} // Register AuthorizationDelegatingHandler as a service services.TryAddSingleton(); // Enable AuthorizationDelegatingHandler for the default HttpClient client services.AddHttpClient() .AddHttpMessageHandler(); // To enable it for a specific named HttpClient client, configure as follows // services.AddHttpClient("weixin") // .AddHttpMessageHandler(); ``` With the above code, we have implemented an automatic `Token` refresh mechanism. When the `Token` expires, the system automatically obtains a new `Token` and retries the request, thereby ensuring continuous and effective communication with third-party `API`s. --- # 6.62 HttpClientHandler Underlying Handler for Sending Requests > Source: https://http.furion.net/en/docs/advanced-guide/httpclienthandler-underlying-handler-for-sending-requests/ `HttpClientHandler` is the core message handler in `.NET` used to configure and execute actual `HTTP` requests. It is a concrete implementation class of `HttpMessageHandler`, typically serving as the **last stage** of the `HttpClient` message processing pipeline, and is responsible for interacting with the operating system's network layer to complete the actual sending of `HTTP` requests and receiving of responses. Although both `DelegatingHandler` and `HttpClientHandler` inherit from `HttpMessageHandler`, they have clearly distinct responsibilities: - **`DelegatingHandler`**: Used to build pluggable, chained **middleware logic** (such as logging, authentication, retry, and so on). It does not initiate network requests directly; instead, it delegates the request to the next handler. - **`HttpClientHandler`**: Is the **handler that ultimately executes network `I/O`**, encapsulating platform-specific underlying implementations (such as `WinHTTP`, `libcurl`, or `SocketsHttpHandler`), and provides rich configuration options to control connection behavior, security policies, proxy settings, and more. --- # 6.63 Core Features of HttpClientHandler > Source: https://http.furion.net/en/docs/advanced-guide/core-features-of-httpclienthandler/ `HttpClientHandler` provides fine-grained control over `HTTP` client behavior. Common configurations include: - **Automatic redirection** (`AllowAutoRedirect`) - **`Cookie` container management** (`UseCookies` + `CookieContainer`) - **Proxy settings** (`Proxy` + `UseProxy`) - **Certificate validation and TLS configuration** (`ServerCertificateCustomValidationCallback`) - **Connection timeout and request timeout** (`Timeout`, `ConnectTimeout`) - **Whether to use default credentials** (`UseDefaultCredentials`) - **Compression support** (`AutomaticDecompression`) --- # 6.64 Collaboration with DelegatingHandler > Source: https://http.furion.net/en/docs/advanced-guide/collaboration-with-delegatinghandler/ When you create an `HttpClient` via `services.AddHttpClient()`, `.NET` provides you with a `HttpClientHandler` by default as the "endpoint" of the pipeline. All `DelegatingHandler` instances added through `.AddHttpMessageHandler()` are **wrapped outside the `HttpClientHandler`**, forming the following call chain: ``` CustomHandler3 → CustomHandler2 → CustomHandler1 → HttpClientHandler → Network ``` That is to say: - Requests start from the outermost `DelegatingHandler` and are passed inward layer by layer; - Responses return from `HttpClientHandler` and are passed back outward layer by layer; - **`HttpClientHandler` is the only component that actually issues network requests.** --- # 6.65 Custom HttpClientHandler > Source: https://http.furion.net/en/docs/advanced-guide/custom-httpclienthandler/ Although most interception logic should be implemented through `DelegatingHandler`, `HttpClientHandler` itself is also inheritable, allowing you to insert custom behavior at the **layer closest to network `I/O`**. This is suitable for scenarios that require deep control over the underlying request/response flow, such as: - Simulating specific network errors - Injecting monitoring before and after the `TLS` handshake - Bypassing the default `Cookie` or proxy handling logic - Capturing or transforming underlying network exceptions The following is an example that inherits from `HttpClientHandler`, showing how to add a custom request header before sending the request and log the status code after receiving the response: ```cs showLineNumbers {1,4,11,14,20} public class CustomHttpHandler : HttpClientHandler { /// protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) { // Refer to the asynchronous SendAsync method return base.Send(request, cancellationToken); } /// protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { // Request pre-processing: add a custom request header request.Headers.Add("Custom-Header", "Value"); // Send the request and get the response var response = await base.SendAsync(request, cancellationToken); // Response post-processing: log the response status code Console.WriteLine($"Response status code: {response.StatusCode}"); return response; } } ``` Next, configure and initialize `CustomHttpHandler` in the `Program.cs` or `Startup.cs` file: ```csharp showLineNumbers {3,7} // Enable for the default HttpClient client services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new CustomHttpHandler()); // To enable for a specifically named HttpClient client, configure as follows // services.AddHttpClient("weixin") // .ConfigurePrimaryHttpMessageHandler(() => new CustomHttpHandler()); ``` **Note**: `ConfigurePrimaryHttpMessageHandler` configures the **underlying handler** of the entire pipeline. It must be a concrete implementation of `HttpMessageHandler` (such as `HttpClientHandler` or `SocketsHttpHandler`), and cannot be a `DelegatingHandler`. --- # 6.66 Best Practices > Source: https://http.furion.net/en/docs/advanced-guide/best-practices/ - **Do not casually replace the default `HttpClientHandler`** unless you need special security or connection behavior. - If you need to add business logic (such as logging, retry, or authentication), prefer using `DelegatingHandler`. - Only consider a custom `HttpClientHandler` when you need fine-grained control over **underlying network behavior** such as `TLS`, proxy, `Cookie`, or timeout policy. --- # 6.67 RateLimitedStream Rate-Limited Stream > Source: https://http.furion.net/en/docs/advanced-guide/ratelimitedstream-rate-limited-stream/ On `SaaS/PaaS` application platforms, users are often billed based on resource usage (such as bandwidth and traffic). In particular, when users download or upload resources, the platform applies rate limiting. In such cases, the `RateLimitedStream` provided by the framework is very useful. This stream can adjust read/write speed according to a configured rate limit, making it ideal for resource control. Using `RateLimitedStream` is very simple — you only need to pass the `Stream` object to be rate-controlled and the maximum allowed bytes per second (`bytesPerSecond`) through the constructor. For example: ```cs showLineNumbers {4,6} var stream = await httpRemoteService.GetAsStreamAsync("https://furion.net/", HttpCompletionOption.ResponseHeadersRead); // Wrap the stream with RateLimitedStream and return a new stream; for subsequent operations, simply use rateLimitedStream instead of stream var rateLimitedStream = new RateLimitedStream(stream, 1024 * 1024 * 1); // Limit the maximum read/write speed to 1MB/s // At this point, read/write operations on rateLimitedStream will be kept within 1MB/s. ✅ ``` > **Tip** - `RateLimitedStream` is especially suitable for rate control of resource uploads and downloads. - It is implemented based on the [token bucket algorithm](https://baike.baidu.com/item/令牌桶算法/6597000), ensuring the effectiveness of rate control. - Please note that due to factors such as the implementation mechanism and system load, the actual rate may have an error of about `5%`; this is normal. Overall, `RateLimitedStream` is a powerful stream wrapper that helps developers better manage resource usage on `SaaS/PaaS` platforms, ensuring user operations comply with billing conditions while improving system stability and performance. --- # 6.68 FileTypeMapper File MIME Type Mapping > Source: https://http.furion.net/en/docs/advanced-guide/filetypemapper-file-mime-type-mapping/ During file upload and download, it is often necessary to obtain or set the `MIME` type based on the file extension. This step usually requires developers to configure it manually and correctly. To reduce the developer's burden, the framework provides the `FileTypeMapper` type, which can automatically return the corresponding `MIME` type based on the file extension. For example: ```cs showLineNumbers {1,3-4} var fileTypeMapper = new FileTypeMapper(); fileTypeMapper.TryGetContentType("image.jpg", out var mimeType); // mimeType is "image/jpeg" fileTypeMapper.TryGetContentType("image.png", out mimeType); // mimeType is "image/png" ``` The framework has a built-in dictionary containing `389` file extensions and their corresponding `MIME` types, covering almost all mainstream and non-mainstream file types. In addition, all methods provided by the framework for adding file content (such as `AddFile`, `AddFileAsStream`, `AddFileAsByteArray`, `AddFileFromRemote`, etc.) automatically resolve the file's `MIME` type when the user does not specify the `contentType` parameter. If it cannot be resolved, it defaults to `application/octet-stream`. For example: ```cs showLineNumbers {5} var content = await httpRemoteService.PostAsStringAsync("https://localhost:7044/HttpRemote/AddForm?id=1", builder => builder.SetMultipartContent(multipart => { // Without passing contentType, it is automatically resolved to "image/jpeg" multipart.AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file") })); ``` With the framework's automatic resolution, developers no longer need to manually look up the file's `MIME` type when uploading files, greatly simplifying the workflow. --- # 6.69 JwtTokenUtility Parsing JWT Token > Source: https://http.furion.net/en/docs/advanced-guide/jwttokenutility-parsing-jwt-token/ `JwtTokenUtility` is a lightweight `JWT` utility class that can parse the `Payload` portion of a `JWT` and extract common claims (`Claims`) without depending on an external `JWT` library. It is suitable for scenarios that require quickly obtaining information such as expiration time, issuer, and subject from a `JWT`. **Parsing the `JWT` `Payload`** Using the `JwtTokenUtility.Parse` method, you can parse a complete `JWT` string or a standalone `Payload` fragment, returning a `JwtPayload` instance: ```cs showLineNumbers {2,5} var jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; var jwtPayload = JwtTokenUtility.Parse(jwt); // Get the expiration time (UTC) var exp = jwtPayload.GetExpirationTimeUtc(); ``` Internally, the `Parse` method automatically handles standard `Base64Url` encoding and pads `=` as needed, requiring no manual processing. **Reading Standard Claims** `JwtPayload` provides complete methods for reading standard `JWT` claims: ```cs showLineNumbers {1,3-9} var jwtPayload = JwtTokenUtility.Parse(jwt); string? issuer = jwtPayload.GetIssuer(); // iss string? subject = jwtPayload.GetSubject(); // sub string? audience = jwtPayload.GetAudience(); // aud long? expiration = jwtPayload.GetExpiration(); // exp (Unix seconds) long? issuedAt = jwtPayload.GetIssuedAt(); // iat (Unix seconds) long? notBefore = jwtPayload.GetNotBefore(); // nbf (Unix seconds) string? jwtId = jwtPayload.GetJwtId(); // jti // More convenience methods... ``` **Checking Whether the `JWT` Is Expired or Valid** ```cs showLineNumbers {2,5} // Whether it has expired bool expired = jwtPayload.IsExpired(); // Whether it is currently valid (already effective and not expired) bool active = jwtPayload.IsActive(); ``` **Reading Custom Claims** In addition to standard claims, `JwtPayload` also supports reading the value of any custom claim: ```cs showLineNumbers {4,7-8,11} var jwtPayload = JwtTokenUtility.Parse(jwt); // Read a string claim string? name = jwtPayload.GetString("name"); // Read an integer claim int? age = jwtPayload.GetInt32("age"); long? timestamp = jwtPayload.GetInt64("timestamp"); // Check whether a claim exists bool hasEmail = jwtPayload.Contains("email"); ``` **Getting the Raw `JSON` String** The `JwtPayload` object exposes the raw `JSON` string, making custom parsing convenient: ```cs showLineNumbers {2} var jwtPayload = JwtTokenUtility.Parse(jwt); string rawJson = jwtPayload.RawJson; ``` **Using It Together with `Access Token` Management** This utility is often used in `FurionAccessTokenProvider` to parse the expiration time from the refresh token returned by the server and update the `ExpiresAt` of `HttpAccessToken`: ```cs showLineNumbers {7} // In the post-response callback of the Configure method httpRequestBuilder.SetOnPostReceiveResponse(httpResponseMessage => { var newRefreshToken = httpResponseMessage.Headers.GetValues("x-access-token").FirstOrDefault(); if (!string.IsNullOrWhiteSpace(newRefreshToken)) { httpAccessToken.ExpiresAt = JwtTokenUtility.Parse(newRefreshToken).GetExpirationTimeUtc()!.Value; } }); ``` With `JwtTokenUtility`, you can easily parse `JWT`s, extract claims, and perform validity checks without introducing a heavy third-party `JWT` library, making it especially suitable for use on the client side or in lightweight `SDK`s. --- # 6.70 DigestCredentials Digest Authentication > Source: https://http.furion.net/en/docs/advanced-guide/digestcredentials-digest-authentication/ `Digest` authentication is an enhanced-security method in the `HTTP` protocol for verifying user identity; compared to basic `HTTP Basic` authentication, it is more secure. During `Digest` authentication, the user's password is not transmitted in plaintext over the network, but is instead encrypted before being sent, thereby improving security. The framework provides the `DigestCredentials` type to facilitate generating digest authorization credentials: ```cs showLineNumbers {2,5,7} // Send a request to the specified server and generate the authorization credential string required for digest authentication var digestCredentials = DigestCredentials.GetDigestCredentials("https://furion.net/digest", "admin", "a123456789", HttpMethod.Get); // Set the generated authorization credential string as the value of the Authorization request header httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Digest", digestCredentials); // Send the request again ``` In this way, the `DigestCredentials` type makes it convenient to implement `Digest` authentication and ensure secure verification of user identity. --- # 6.71 HTTP Request Logging (Disabling) > Source: https://http.furion.net/en/docs/advanced-guide/logging/ By default, the system prints relevant log information when sending `HTTP` remote requests, as shown below: ```bash showLineNumbers {2,4,6,8} info: System.Net.Http.HttpClient.Default.LogicalHandler[100] Start processing HTTP request GET https://furion.net/ info: System.Net.Http.HttpClient.Default.ClientHandler[100] Sending HTTP request GET https://furion.net/ info: System.Net.Http.HttpClient.Default.ClientHandler[101] Received HTTP response headers after 93.0553ms - 200 info: System.Net.Http.HttpClient.Default.LogicalHandler[101] End processing HTTP request after 122.2355ms - 200 ``` If you want to disable these log messages, you can make the following configuration in the project's `appsettings.json` and `appsettings.Development.json` configuration files: ```cs showLineNumbers {2,3,7} { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning", "Microsoft.EntityFrameworkCore": "Information", "System.Net.Http.HttpClient": "Warning" // Set the log level to Warning to disable Info-level logging } } } ``` By setting the log level of `System.Net.Http.HttpClient` to `Warning`, you can effectively disable `Info`-level `HTTP` request log messages. Please note that settings in `appsettings.Development.json` will override the corresponding settings in `appsettings.json` (if both exist), which is typically used to provide a different logging strategy in the development environment. In addition to the above configuration approach, you can also disable `HTTP` remote request logging within the program. The specific steps are as follows: ```cs showLineNumbers {2-3,6-7,10-13,17-20} // Disable logging for the default client services.AddHttpClient(string.Empty) .RemoveAllLoggers(); // Disable logging for a specific client //services.AddHttpClient("weixin") // .RemoveAllLoggers(); // You can also disable logging for all clients at once services.ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.RemoveAllLoggers(); }); // Or use the IHttpRemoteBuilder extension method for one-click configuration services.AddHttpRemote() .ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.RemoveAllLoggers(); }); ``` ### Custom Logging Service (Inheriting `HttpRemoteLoggerBase`) `HttpRemoteBuilder` provides the `UseLogger` method, allowing you to replace the built-in logging implementation with a custom logging service. A custom logging service must inherit from the `HttpRemoteLoggerBase` abstract class and implement (override) the `Log` method, as shown below: ```cs showLineNumbers {2,5,8,11,13,18} // Custom logging service: inherit from HttpRemoteLoggerBase internal sealed class CustomHttpRemoteLogger( ILogger logger, IOptionsMonitor httpRemoteOptions, bool isLoggingRegistered) : HttpRemoteLoggerBase { /// public override void Log(LogLevel logLevel, Exception? exception, string? message, params object?[] args) { // Check whether a logging provider is registered if (isLoggingRegistered) { logger.Log(logLevel, exception, message, args); } else { // Invoke the fallback log output delegate httpRemoteOptions.CurrentValue.FallbackLogger?.Invoke(LogMessageFormatter.Value(message, args)); } } } ``` > **Constructor parameter order requirements** The `bool isLoggingRegistered` parameter in the constructor **must be placed after the dependency injection service parameters** (i.e. as the last parameter). This parameter tells you whether a logging provider is currently configured (registered), and the framework passes it automatically when resolving the service; if your custom logging service also needs to inject other services, they must also be declared **before** the `isLoggingRegistered` parameter. As shown below: ```cs showLineNumbers {2} // Other injected services must be declared before isLoggingRegistered public class CustomHttpRemoteLogger(IMyService myService, ILogger logger, IOptionsMonitor httpRemoteOptions, bool isLoggingRegistered) : HttpRemoteLoggerBase { // Implementation omitted } ``` Once defined, register it via the `UseLogger` method: ```cs showLineNumbers {1,4} services.AddHttpRemote(builder => { // Register the custom logging service builder.UseLogger(); // Or register by Type // builder.UseLogger(typeof(CustomHttpRemoteLogger)); }); ``` **Manual registration without `UseLogger`:** If you want full control — for example, passing more parameters to a custom constructor (not just `isLoggingRegistered`) — you can skip the `UseLogger` method and register the `IHttpRemoteLogger` service yourself, as shown below: ```cs showLineNumbers {2,5,8-10} // Check whether a logging provider is registered var isLoggingRegistered = services.Any(u => u.ServiceType == typeof(ILoggerProvider)); // Remove all existing IHttpRemoteLogger registrations services.RemoveAll(); // Manually register the custom logging service, passing more parameters services.AddSingleton(provider => (IHttpRemoteLogger)ActivatorUtilities.CreateInstance(provider, typeof(CustomHttpRemoteLogger), isLoggingRegistered, extraParam1, extraParam2 /* ... more parameters can be passed here */)); ``` This gives you complete control over constructor parameter passing, allowing you to inject any required services or parameters as needed. --- # 7.1 Use Cases > Source: https://http.furion.net/en/docs/cases/use-cases/ This section summarizes common use cases of `HTTP` remote requests in application development. --- # 7.2 Using the Dynamic Object Clay to Build and Receive Request Data > Source: https://http.furion.net/en/docs/cases/using-the-dynamic-object-clay-to-build-and-receive-request-data/ The dynamic object (`Clay`) has a very wide range of application scenarios in `HTTP` remote requests, especially when integrating with third-party `API` interfaces. Typically, these interfaces need to pass or receive data in `JSON` format, and the dynamic object can simplify the process of building and parsing data. The following are the configuration steps for using the dynamic object in the `HTTP` remote request module: ### 1. Configuring the Dynamic Object `JSON` Serialization Converter When using the dynamic object for `HTTP` remote requests, **you first need to configure `AddClayConverters()` so that the dynamic object can be serialized into a `JSON` format string**. A configuration example is as follows: ```cs showLineNumbers {2-3,5,9-10,12} // Global configuration (applies to all clients) services.AddHttpRemote(options => {}) .ConfigureOptions(options => { options.JsonSerializerOptions.AddClayConverters(); }); // Client-level configuration (higher priority) services.AddHttpClient("client-name") .ConfigureOptions(options => { options.JsonSerializerOptions.AddClayConverters(); }); ``` ### 2. Sending and Receiving `JSON` Data After configuration is complete, you can use the dynamic object to build the request content and send the `HTTP` request, and at the same time convert the response content into a dynamic object for processing. The following is an example: ```cs showLineNumbers {2-4,8,11} // Build the request content dynamic payload = new Clay(); payload.id = 1; payload.name = "furion"; // Send the HTTP remote request var content = await httpRemoteService.PostAsStringAsync("https://localhost:7044/HttpRemote/AddModel", builder => builder.SetJsonContent(payload)); // Convert the response content into a dynamic object dynamic clay = Clay.Parse(content); ``` --- ### Custom `Clay` Content Converter Simplifies Manual Conversion ✅ To simplify the code and avoid manually converting `JSON` format strings into dynamic objects (such as `dynamic clay = Clay.Parse(content);`), you can **customize the `ClayContentConverter` content converter**. In this way, you can directly use the `Clay` type as a generic receiving parameter in `HTTP` requests. The following is the implementation of the custom converter: **[【One-click download `ClayContentConverter.cs` file】✅](/img/ClayContentConverter.cs)** ```cs showLineNumbers {1,5,10-11,16,20,25-26} public class ClayContentConverter : HttpContentConverterBase { /// public override Clay? Read(HttpContentConverterContext context, CancellationToken cancellationToken = default) => AsyncUtility.RunSync(() => ReadAsync(context, cancellationToken)); /// public override async Task ReadAsync(HttpContentConverterContext context, CancellationToken cancellationToken = default) { var str = await context.ResponseMessage.Content.ReadAsStringAsync(cancellationToken); return Clay.Parse(str, ClayOptions.Flexible); // or use Clay.Parse(str, ClayOptions.Flexible); // ignore property casing } } // supports converting dynamic types to dynamic objects (optional, but recommended!!!) public class DynamicContentConverter : HttpContentConverterBase { /// public override dynamic? Read(HttpContentConverterContext context, CancellationToken cancellationToken = default) => AsyncUtility.RunSync(() => ReadAsync(context, cancellationToken)); /// public override async Task ReadAsync(HttpContentConverterContext context, CancellationToken cancellationToken = default) { var str = await context.ResponseMessage.Content.ReadAsStringAsync(cancellationToken); return Clay.Parse(str, ClayOptions.Flexible); // or use Clay.Parse(str, ClayOptions.Flexible); // ignore property casing } } ``` Next, configure and register the `HttpRemote` service in the `Startup.cs` or `Program.cs` file to enable the custom content converter functionality: ```cs showLineNumbers {1,3} services.AddHttpRemote(options => { options.AddHttpContentConverters(() => [ new ClayContentConverter(), new DynamicContentConverter()]); // new DynamicContentConverter() (optional, but recommended!!) }); ``` After the configuration is complete, you can directly use the dynamic object type `Clay` as the generic receiving parameter: ```cs showLineNumbers {2,6} // send an HTTP remote request and convert the response content into a dynamic object dynamic clay = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddModel", builder => builder.SetJsonContent(payload)); // if DynamicContentConverter is configured, you can also use dynamic to receive it dynamic clay = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddModel", builder => builder.SetJsonContent(payload)); ``` > **Explanation of `ClayJsonConverter` and `ClayContentConverter`** - **`ClayJsonConverter/AddClayConverters()`**: used to serialize dynamic objects into `JSON` format strings, typically used as **input** parameters. - **`ClayContentConverter/DynamicContentConverter`**: used to deserialize `JSON` format strings into dynamic objects, typically used as **output** parameters. By combining dynamic objects with `HTTP` remote requests, developers can handle dynamic `JSON` data more efficiently and simplify the integration process with third-party `API`s. The dynamic nature of dynamic objects makes data construction and parsing more flexible, while custom content converters further improve development efficiency. --- # 7.3 Using in Blazor WebAssembly Applications > Source: https://http.furion.net/en/docs/cases/blazor/ > **`Blazor WebAssembly` Network Stack Limitations** `Blazor WebAssembly` applications run in the browser's sandbox environment and cannot use the operating system's underlying `TCP` sockets. The entire network stack is replaced with an implementation based on the browser `fetch` `API`, so the `SocketsHttpHandler` that relies on `System.Net.Sockets` and the `HttpClientHandler` that calls platform-native network `API`s are both unavailable. Any setting of socket or platform-specific handler properties (such as `AutomaticDecompression`) will directly throw a `PlatformNotSupportedException`. During development, you should avoid configuring these `Handler`s and rely entirely on the network capabilities provided by the browser. `HTTP` remote requests support use in `Blazor WebAssembly` client applications. The following are the detailed steps for configuration and use: **1. Register the `HttpRemote` Service** In the `Startup.cs` or `Program.cs` file, register the `HttpRemote` service and configure `FallbackBaseAddress`: ```cs showLineNumbers {1,2,4-5} builder.Services.AddHttpRemote() .ConfigureOptions((options, serviceProvider) => { var navigation = serviceProvider.GetRequiredService(); options.FallbackBaseAddress = new Uri(navigation.BaseUri); }); ``` **2. Import the Namespace in `_Imports.razor`** In the `_Imports.razor` file, add the namespace for the `HttpRemote` service: ```cs showLineNumbers @using HttpAgent; // if using the Furion framework, use @using Furion.HttpRemote; ``` **3. Use It in `*.razor` Files** In `*.razor` files, inject `IHttpRemoteService` and send `HTTP` remote requests: ```cs showLineNumbers {2,11} @page "/weather" @inject IHttpRemoteService Http // other code... @code { private WeatherForecast[] forecasts; protected override async Task OnInitializedAsync() { forecasts = await Http.GetAsAsync("sample-data/weather.json"); } } ``` Through the above steps, you can easily use the `HTTP` remote request feature in `Blazor WebAssembly` applications, dynamically fetch data, and render it to the page. --- # 7.4 Using in File-Based Apps Applications > Source: https://http.furion.net/en/docs/cases/file-based-apps/ Starting with the `.NET 10` `SDK`, `.NET` provides `File-Based Apps`: a single `.cs` source file can be built, run and published — no project file (`.csproj`) or solution required. Dependencies and settings are declared with `#:`-prefixed directives at the top of the file (the `#:include`, `#:package`, `#:project`, `#:property` and `#:sdk` directives are supported). > For the complete directive reference and more usage details of `File-Based Apps`, see the official Microsoft documentation "[File-based apps](https://learn.microsoft.com/en-us/dotnet/core/sdk/file-based-apps)". Since a `File-Based App` has no project file, you cannot use the `dotnet add package` command. Instead, reference the `HttpAgent` package with the `#:package` directive. `HTTP` remote requests have fully supported the `File-Based Apps` application type since version `1.53`, so use that version or later (`@*` means the latest version, currently `2.1.5`): **1. Write the `app.cs` file** ```cs showLineNumbers {1,5} #:package HttpAgent@* using HttpAgent; var result = await HttpRemoteClient.Service.GetAsStringAsync("https://furion.net/"); Console.WriteLine(result); ``` > Note: omitting the version after `#:package` currently only works when using central package management (`Directory.Packages.props`), so `@*` is used here to reference the latest version; you can also pin an explicit version such as `#:package HttpAgent@2.1.5`. **2. Run the application** Run the following command in the terminal: ```bash showLineNumbers {1} dotnet run app.cs ``` You can also use the `--file` option (`dotnet run --file app.cs`) or the shorthand syntax (`dotnet app.cs`). To pass arguments to the application, place them after `--`: `dotnet run app.cs -- arg1 arg2`. **3. Enable the Profiler (optional)** To print the full request/response traffic, call `HttpRemoteClient.Configure` in the file to configure the default `HttpClient`: ```cs showLineNumbers {1,5-8,10} #:package HttpAgent@* using HttpAgent; HttpRemoteClient.Configure(services => { services.AddHttpClient(string.Empty).AddProfilerDelegatingHandler(); }); var result = await HttpRemoteClient.Service.GetAsStringAsync("https://furion.net/"); Console.WriteLine(result); ``` > **Tip** `File-Based Apps` enable native `AOT` publishing by default. They also support the `#:property` directive for setting `MSBuild` properties, the `#:sdk` directive for switching the `SDK` (e.g. `#:sdk Microsoft.NET.Sdk.Web`) and the `#:include` directive for including other source files. See the official Microsoft documentation "[File-based apps](https://learn.microsoft.com/en-us/dotnet/core/sdk/file-based-apps)" for more details. --- # 7.5 Using in MAUI Applications > Source: https://http.furion.net/en/docs/cases/maui/ `.NET MAUI` has built-in dependency injection support based on `Microsoft.Extensions.DependencyInjection`: register services on `builder.Services` in the app entry point `MauiProgram.CreateMauiApp()`, then inject and use them through the constructors of pages (`Page`) or view models (`ViewModel`). For more details about dependency injection (registration approaches, service lifetimes, etc.), see the official Microsoft documentation "[Dependency injection in .NET MAUI](https://learn.microsoft.com/en-us/dotnet/maui/fundamentals/dependency-injection)". **1. Register the service in `MauiProgram.cs`** Call `AddHttpRemote()` in the `CreateMauiApp()` method of `MauiProgram.cs` to register the `HTTP` remote request service: ```cs showLineNumbers {1,16} using HttpAgent; public static class MauiProgram { public static MauiApp CreateMauiApp() { var builder = MauiApp.CreateBuilder(); builder .UseMauiApp() .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); }); builder.Services.AddHttpRemote(); // register the HTTP remote request service return builder.Build(); } } ``` **2. Inject and use it in a page** Inject `IHttpRemoteService` through the constructor of a page (or a view model) to send `HTTP` remote requests: ```cs showLineNumbers {1,7,15} using HttpAgent; public partial class MainPage : ContentPage { private readonly IHttpRemoteService _httpRemoteService; public MainPage(IHttpRemoteService httpRemoteService) { InitializeComponent(); _httpRemoteService = httpRemoteService; } private async Task LoadContentAsync() { var result = await _httpRemoteService.GetAsStringAsync("https://furion.net/"); // render result ... } } ``` > Note: in `Shell` applications, if a page's constructor needs dependency injection, register the page with the container as well (e.g. `builder.Services.AddTransient();`) so `Shell` navigation can resolve the page instance from the container; alternatively, inject `IHttpRemoteService` into a registered view model and inject that view model into the page. --- # 8.1 FAQ > Source: https://http.furion.net/en/docs/faq/faq/ This section summarizes some common issues you may encounter when sending `HTTP` remote requests. --- # 8.2 Ignoring SSL certificate validation (https errors) > Source: https://http.furion.net/en/docs/faq/ignoring-ssl-certificate-validation-https-errors/ If a certificate error such as `The SSL connection could not be established, see inner exception.` occurs while sending an `HTTP` remote request, you can ignore `SSL` certificate validation by adding the following configuration: ```cs showLineNumbers {3,6-7,12,14,17-18} // Default client configuration services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { // Ignore SSL certificate validation ServerCertificateCustomValidationCallback = HttpRemoteUtility.IgnoreSslErrors, SslProtocols = HttpRemoteUtility.AllSslProtocols }); // If using SocketsHttpHandler, you can ignore SSL certificate validation with the following configuration services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler() { SslOptions = new SslClientAuthenticationOptions { // Ignore SSL certificate validation RemoteCertificateValidationCallback = HttpRemoteUtility.IgnoreSocketSslErrors, EnabledSslProtocols = HttpRemoteUtility.AllSslProtocols }, }); ``` In addition to ignoring `SSL` certificate validation in the global configuration, you can also ignore `SSL` certificate validation for a single request via the `SetHttpClientProvider` method. The sample code is as follows: ```cs showLineNumbers {2-7} HttpRequestBuilder.Get("https://furion.net/") .SetHttpClientProvider(() => (new HttpClient(new HttpClientHandler { // Ignore SSL certificate validation ServerCertificateCustomValidationCallback = HttpRemoteUtility.IgnoreSslErrors, SslProtocols = HttpRemoteUtility.AllSslProtocols }), client => client.Dispose())); ``` --- # 8.3 A sent request hangs or blocks for a long time > Source: https://http.furion.net/en/docs/faq/a-sent-request-hangs-or-blocks-for-a-long-time/ If a long hang or blocking occurs when sending an `HTTP` remote request, **you may be using code like the following to obtain the local machine's `IP` or `MAC` address when constructing the request**: ```cs showLineNumbers var addressList = Dns.GetHostEntry(Dns.GetHostName()).AddressList; ``` `Dns.GetHostName()` returns the local host name, while `Dns.GetHostEntry(host name)` resolves that name through mechanisms such as `DNS` and `NetBIOS`. If the host name is not registered in `DNS` or the `DNS` server is unreachable, the resolution process waits until the system times out (about `10-15` seconds by default on `Windows`), causing every request to be blocked. In this case, use the framework's built-in `HttpRemoteUtility` utility methods instead. They read the local network interface information directly, **with no `DNS/NetBIOS` lookup at all and an execution time of `< 1` millisecond**: ```cs showLineNumbers {2,5} // Get the local IPv4 address var ip = HttpRemoteUtility.GetLocalIPv4(); // Get the local MAC address var mac = HttpRemoteUtility.GetLocalMacAddress(); ``` Using the methods above eliminates the long blocking caused by local host name resolution and restores normal `HTTP` request/response speed. --- # 8.4 Forcing IPv4 or IPv6 requests > Source: https://http.furion.net/en/docs/faq/forcing-ipv4-or-ipv6-requests/ By configuring the `ConnectCallback` of `SocketsHttpHandler`, you can force `HttpClient` to initiate requests using a specified `IP` version or local egress address, which is suitable for network policy control or multi-NIC environments to optimize performance or meet network requirements. The example is as follows: ```cs showLineNumbers {1,8,15,22,30} // Force IPv4 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { ConnectCallback = HttpRemoteUtility.IPv4ConnectCallback }); // Force IPv6 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { ConnectCallback = HttpRemoteUtility.IPv6ConnectCallback }); // Default (auto-select) services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { ConnectCallback = HttpRemoteUtility.UnspecifiedConnectCallback }); // Multi-NIC scenario: specify the local egress IPv4 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { ConnectCallback = (context, token) => HttpRemoteUtility.ConnectWithLocalIPv4(IPAddress.Parse("192.168.0.103"), context, token) }); // Multi-NIC scenario: specify the local egress IPv6 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { ConnectCallback = (context, token) => HttpRemoteUtility.ConnectWithLocalIPv6(IPAddress.Parse("::1"), context, token) }); ``` --- # 8.5 Handling redirect responses > Source: https://http.furion.net/en/docs/faq/handling-redirect-responses/ When sending an `HTTP` remote request, if the target server returns a redirect response (such as `301 Moved Permanently`, `302 Found`, etc.), **the framework follows redirects automatically by default**. To handle these situations precisely, refer to the following approaches: - **Globally enable or disable automatic redirects via `HttpRemoteOptions`** When sending an `HTTP` remote request, if the target server returns a redirect response (such as `301 Moved Permanently`, `302 Found`, etc.), **the framework follows redirects automatically by default**. To enable or disable this behavior, use the following configuration: ```cs showLineNumbers {2,5,8} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { // Sets whether requests should follow redirect responses; default is true options.AllowAutoRedirect = false; // Sets the maximum number of redirects a request follows; default is 50 options.MaximumAutomaticRedirections = 50; }); ``` - **Globally enable or disable automatic redirects via `HttpClient`** To enable or disable whether `HttpClient` automatically handles redirects, configure `HttpClientHandler` or `SocketsHttpHandler`. ```cs showLineNumbers {3,5-6} // Configure the default client services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { AllowAutoRedirect = true, // Allow automatic redirects; default is true (allowed) MaxAutomaticRedirections = 20 // Set the maximum number of redirects }); ``` > **Configuration for disabling automatic redirects** If you want to completely disable automatic redirects, make sure to configure both of the following code snippets: ```cs showLineNumbers {2,4,8,9,11} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { options.AllowAutoRedirect = false; }); // Configure the default client services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { AllowAutoRedirect = false }); ``` Note that the global configuration above affects not only the redirect behavior of the `HTTP` remote request service, but also redirect forwarding in `HttpContext`. - **By handling redirects manually** You can also check the response status code. If the status code is in the `300-399` range, it indicates a redirect status code; in this case you can manually parse the `Location` header and resend the request using that address. 1. Disable `HttpClient`'s automatic redirect feature ```cs showLineNumbers {3,5} // Configure the default client services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { AllowAutoRedirect = false }); ``` 2. Manually manage the redirect flow ```cs showLineNumbers {4,7-16} var responseMessage = await httpRemoteService.GetAsync("https://furion.net/redirect"); // Check whether the response message contains a redirect status code var statusCode = responseMessage.StatusCode; // A while loop is usually used for this check, since multiple redirects may be involved if (statusCode is HttpStatusCode.Ambiguous or HttpStatusCode.Moved or HttpStatusCode.Redirect or HttpStatusCode.RedirectMethod or HttpStatusCode.RedirectKeepVerb || (int)statusCode == 308) { // Get the redirect address var redirectUrl = responseMessage.Headers.Location; if (redirectUrl != null) { // Send the request again with the new URL response = await httpRemoteService.GetAsync(redirectUrl); } } ``` Which approach you choose depends on your specific needs. If you need finer-grained control over each redirect process — for example, to log or execute specific logic — manual handling may be the better choice. If you simply want the client to handle all redirects automatically, enabling automatic redirects is more convenient. --- # 8.6 Enabling standard request headers > Source: https://http.furion.net/en/docs/faq/enabling-standard-request-headers/ To improve the compatibility of network requests sent through the `HTTP` client and avoid being blocked by a WAF (Web Application Firewall), the framework provides a one-click configuration method to quickly and uniformly set standard request headers: ```cs showLineNumbers {5} // Configure for the default client services.AddHttpClient(string.Empty, client => { // Enable standard request headers client.UseStandardRequestHeaders(); }); ``` In addition to the global `HttpClient` configuration, you can also configure specific settings for a single request, as shown below: ```cs showLineNumbers {2} HttpRequestBuilder.Get("https://furion.net/") .UseStandardRequestHeaders(); // Enable standard request headers ``` After enabling standard request headers, the request automatically adds the following headers: - **`Accept`**: `application/json`, `text/plain;q=0.9`, `*/*;q=0.8` (explicit media type priorities to avoid being blocked by a `WAF`) - **`Connection`**: enables persistent connections (`Keep-Alive`) to reduce the overhead of establishing and closing `TCP` connections --- # 8.7 Setting the default User-Agent > Source: https://http.furion.net/en/docs/faq/setting-the-default-user-agent/ When sending an `HTTP` request, if the user does not specify a `User-Agent` request header, the framework uses the `Edge` browser (version `142`) `User-Agent` by default. The example is as follows: ``` Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0 ``` To customize the default `User-Agent`, use the following approaches: **Global configuration**: ```cs showLineNumbers {2,4} // Configure the default client's User-Agent services.AddHttpClient(string.Empty, client => { client.DefaultRequestHeaders.Add("User-Agent", UserAgents.Chrome.PC); // Recommended: use the UserAgents static class // client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0"); // Or set the User-Agent string directly }); ``` **Per-request configuration**: ```cs showLineNumbers {2} HttpRequestBuilder.Post("https://furion.net/") .SetUserAgent(UserAgents.Chrome.PC); // Recommended: use the UserAgents static class // .SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0"); // Or set the User-Agent string directly // .WithHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0", replace: true); // Or use the WithHeader method ``` --- # 8.8 Sending relative-address (internal) requests > Source: https://http.furion.net/en/docs/faq/sending-relative-address-internal-requests/ In `Web` applications such as `ASP.NET` or `Blazor`, relative addresses are typically used when sending internal `HTTP` requests. However, since the host address and port cannot be obtained before the project starts, `BaseAddress` cannot be configured directly through `services.AddHttpClient()`. In this case, you can configure the `FallbackBaseAddress` of `HttpRemoteOptions` instead. - **`ASP.NET` applications** ```cs showLineNumbers {2,4-5} services.AddHttpRemote() .ConfigureOptions((options, serviceProvider) => { var serverAddressesFeature = serviceProvider.GetRequiredService().Features.Get()!; options.FallbackBaseAddress = new Uri(serverAddressesFeature.Addresses.FirstOrDefault()); }); ``` > **Using relative addresses in classes derived from `BackgroundService`** Because `BackgroundService` is a background service, it does not run in the `Web` thread by default. If you send a relative-address (internal) `HTTP` request in a class derived from it, `serverAddressesFeature.Addresses` may be empty. This is because classes derived from `BackgroundService` start before the `Web` host starts, at which point the `Web` host address and port have not yet been assigned. If this happens, you can read the `Urls` node from the `IConfiguration` configuration to set the `FallbackBaseAddress` address. - **`Blazor WebAssembly` applications** ```cs showLineNumbers {2,4-5} services.AddHttpRemote() .ConfigureOptions((options, serviceProvider) => { var navigation = serviceProvider.GetRequiredService(); options.FallbackBaseAddress = new Uri(navigation.BaseUri); }); ``` With the above configuration, you can send **relative-address (internal) requests**, as shown in the following example: ```cs showLineNumbers var content = await httpRemoteService.GetAsStringAsync("/user/1"); // Relative address (internal) ``` When sending a relative-address request, the system automatically obtains the host address and port used when the `Web` host started and concatenates them. For example, the final request address sent may be: `https://localhost:5001/user/1`. This approach simplifies the configuration of internal requests and ensures the host address and port are obtained dynamically at project startup, thereby avoiding maintenance problems caused by hardcoding. --- # 8.9 Getting the Response Cookie > Source: https://http.furion.net/en/docs/faq/getting-the-response-cookie/ In an `HTTP` request, if the server sets a `Cookie`, the response headers will contain one or more `Set-Cookie` key-value pairs. After the client receives the response, it can obtain the `Cookie` information by reading these `Set-Cookie` key-value pairs. The framework provides the following two convenient ways to obtain the `Cookie`: **1. Using the `HttpRemoteResult` return value type** `HttpRemoteResult` is a generic type specifically designed to encapsulate the response content in the `HTTP` remote request module. In addition to the commonly used `HTTP` response information, this type also provides features such as request elapsed time. ```cs showLineNumbers {3-4} // Using the request verb approach (result type is HttpRemoteResult) var result = await httpRemoteService.GetAsync("https://furion.net/"); var setCookies = result.SetCookies; // Get the Cookie collection in the response (IList type) var rawSetCookies = result.RawSetCookies; // Get the Set-Cookie collection from the raw response headers (List type) // The builder approach works the same way (result type is HttpRemoteResult) var result = await httpRemoteService.SendAsync(HttpRequestBuilder.Get("https://furion.net/")); ``` **2. Using the `TryGetSetCookies` extension method on `HttpResponseMessage`** The framework also provides the `TryGetSetCookies` extension method for the `HttpResponseMessage` and `HttpResponseHeaders` types, allowing you to conveniently read and parse the `Set-Cookie` response header information. ```cs showLineNumbers {2,5} var httpResponseMessage = await httpRemoteService.GetAsync("https://furion.net/"); httpResponseMessage.TryGetSetCookies(out var setCookies, out var rawSetCookies); // Or get it through the Headers property // httpResponseMessage.Headers.TryGetSetCookies(out var setCookies, out var rawSetCookies); ``` Through the two methods above, developers can easily obtain and process the `Cookie` information in the `HTTP` response. --- # 8.10 Configuring Windows Authentication > Source: https://http.furion.net/en/docs/faq/configuring-windows-authentication/ `Windows` authentication is a security mechanism provided by Microsoft that verifies the identity of users or entities, ensuring that their access to systems, network resources, and applications complies with security policies. This mechanism is typically used in the `Windows` operating system, allowing users to log in to the system or to applications that depend on this mechanism without manually entering a username and password. In some traditional `Web` application systems deployed on `Windows` servers, sending `HTTP` remote requests may require enabling `Windows` authentication. The following are two common configuration approaches: ### 1. Using the user currently logged in to the `Windows` system (recommended) ```cs showLineNumbers {3,5,9} // Configure the default client services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler // Or use SocketsHttpHandler { UseDefaultCredentials = true }); // Configure a specific client services.AddHttpClient("furion") .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler // Or use SocketsHttpHandler { UseDefaultCredentials = true }); ``` ### 2. Manually entering the `Windows` system username and password ```cs showLineNumbers {3,5-6,10} // Configure the default client services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler // Or use SocketsHttpHandler { Credentials = new NetworkCredential("windowsLoginUsername", "windowsLoginPassword"), PreAuthenticate = true }); // Configure a specific client services.AddHttpClient("furion") .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler // Or use SocketsHttpHandler { Credentials = new NetworkCredential("windowsLoginUsername", "windowsLoginPassword"), PreAuthenticate = true }); ``` Through the configuration above, you can choose to use the current system user or manually enter credentials to enable `Windows` authentication as needed. --- # 8.11 Configuring Kerberos and Active Directory Authentication > Source: https://http.furion.net/en/docs/faq/configuring-kerberos-and-active-directory-authentication/ `Kerberos` is a network authentication protocol that uses symmetric-key cryptography to verify the identities of users and services. Through the ticket mechanism, `Kerberos` ensures that communication over the network is secure and can effectively prevent security threats such as eavesdropping and replay attacks. Since `Windows 2000`, `Kerberos` has become the default authentication protocol in domain environments. `Active Directory (AD)` is a set of directory management services provided by Microsoft for the management and security configuration of `Windows` domain networks. It allows `IT` administrators to manage users, devices, and other resources in the network, and provides features such as authentication and authorization. `AD` supports multiple authentication protocols, including `NTLM` (`NT LAN Manager`), but `Kerberos` is more recommended because of its higher security. --- **If your application runs in a domain environment and the target service supports `Windows` authentication (such as `Negotiate` or `NTLM`), you can directly use the `Windows` authentication configuration approach**. The following is a specific configuration example: ### 1. Using the current domain user's credentials (recommended) This approach applies when the application and the target service run in the same domain environment. The system automatically authenticates using the current domain user's credentials. ```cs showLineNumbers {3,5,9} // Configure the default client services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler // Or use SocketsHttpHandler { UseDefaultCredentials = true }); // Configure a specific client services.AddHttpClient("furion") .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler // Or use SocketsHttpHandler { UseDefaultCredentials = true }); ``` ### 2. `Kerberos` authentication across domains If the application and the target service are not in the same domain environment, cross-domain authentication can be implemented by installing the [Microsoft.Identity.Client](https://www.nuget.org/packages?q=Microsoft.Identity.Client) library. This library provides support for `Kerberos` and other authentication protocols. #### Installing the NuGet package ```bash showLineNumbers dotnet add package Microsoft.Identity.Client ``` #### Using `Microsoft.Identity.Client` to implement authentication The following is a simple example showing how to use `Microsoft.Identity.Client` to obtain an access token and perform authentication: ```cs showLineNumbers {3,12,16,18} using Microsoft.Identity.Client; // Configure authentication parameters var clientId = "yourClientId"; var tenantId = "yourTenantId"; var authority = $"https://login.microsoftonline.com/{tenantId}"; var app = PublicClientApplicationBuilder.Create(clientId) .WithAuthority(authority) .Build(); // Obtain the access token var scopes = new[] { "api://scopeOfTheTargetService" }; var result = await app.AcquireTokenInteractive(scopes).ExecuteAsync(); // Use the access token to call the target service httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://furion.net/") .AddBearerAuthentication(result.AccessToken)); ``` ### 3. Reference documentation - [Microsoft.Identity.Client NuGet package](https://www.nuget.org/packages/Microsoft.Identity.Client) - [Microsoft Authentication Library (MSAL) for .NET documentation](https://learn.microsoft.com/zh-cn/entra/msal/dotnet/) - [How to authenticate with Kerberos using .NET](https://dev.to/lucaspsilveira/how-to-authenticate-with-kerberos-using-net-2gh2) Through the configuration above, you can choose the appropriate authentication approach based on the environment in which your application runs, ensuring secure access to the target service. --- # 8.12 Response Content Decompression (Supporting gzip, deflate, brotli and zstd) and Content Encoding Issues > Source: https://http.furion.net/en/docs/faq/decompression/ > **`WebAssembly` Note** The following content does not apply to `Blazor WebAssembly` applications. In modern mainstream web frameworks, most have built-in support for compressing server response content, with the most commonly used compression methods being `gzip`, `deflate`, `brotli` and `zstd`. When making an `HTTP` request, if the response content returned by the server has compression enabled, by default the framework automatically decompresses this content (for the `gzip`, `deflate`, `brotli` and `zstd` formats). If you need to enable custom decompression, you can achieve this by configuring the client behavior. The specific method is as follows: ### Configuring automatic decompression ```cs showLineNumbers {3,5} // Configure the default client services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All, // Enable automatic decompression for gzip, deflate, brotli and zstd // AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate // Enable automatic decompression for gzip and deflate only }); ``` With the configuration above, if `AutomaticDecompression` is set to `DecompressionMethods.All`, the framework automatically handles response content compressed with the `gzip`, `deflate`, `brotli` and `zstd` formats; if it is set only to `DecompressionMethods.GZip | DecompressionMethods.Deflate`, then only content compressed with `gzip` and `deflate` is automatically decompressed. Note that the framework only attempts automatic decompression when the `Content-Encoding` response header contains one of the compression methods above. Therefore, in a configuration that does not include `brotli`, the corresponding response content will not be automatically decompressed. ### Manually handling decompression (such as when automatic decompression fails or when processing non-standard encodings) When you need to handle decompression manually, follow these steps: 1. Disable automatic decompression ```cs showLineNumbers {5} // Configure the default client and disable all automatic decompression services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AutomaticDecompression = DecompressionMethods.None // Disable automatic decompression }); ``` 2. Check the `Content-Encoding` response header ```cs showLineNumbers {2,4} var response = await httpRemoteService.GetAsync("https://furion.com"); if (response.Content.Headers.ContentEncoding.Contains("gzip")) { // gzip encoding detected, manual decompression can be performed } ``` 3. Perform manual decompression (using `gzip` as an example) ```cs showLineNumbers {1-3,5} using var responseStream = await response.Content.ReadAsStreamAsync(); using var gzipStream = new GZipStream(responseStream, CompressionMode.Decompress); using var reader = new StreamReader(gzipStream); var content = await reader.ReadToEndAsync(); Console.WriteLine(content); ``` --- # 8.13 JSON Serialization Configuration > Source: https://http.furion.net/en/docs/faq/json-serialization/ The framework uses `System.Text.Json` by default to handle `JSON` serialization for `HTTP` requests, supporting the following configuration approaches: - **Global default configuration** Set unified `JSON` serialization behavior for all `HttpClient` instances through `HttpRemoteOptions`: ```cs showLineNumbers {2} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => // Or use the overload: .ConfigureOptions((options, serviceProvider) => { options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); ``` - **Client-level configuration (higher priority)** When both global and client-level configurations exist, the framework gives priority to the client-level configuration: ```cs showLineNumbers {3,10} // Configure the default client services.AddHttpClient(string.Empty) .ConfigureOptions(options => // Or use the overload: .ConfigureOptions((options, serviceProvider) => { options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); // Configure a specific client services.AddHttpClient("furion") .ConfigureOptions(options => // Or use the overload: .ConfigureOptions((options, serviceProvider) => { options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); ``` Usage example: ```cs showLineNumbers {2} var model = await httpRemoteService.SendAsAsync(HttpRequestBuilder.Get("https://furion.net/test-json") .SetHttpClientName("furion")); // If not set, the default is string.Empty ``` - **Manual handling (full control)** When special handling is required, you can directly operate on the raw response: ```cs showLineNumbers {2} var jsonString = await httpRemoteService.GetAsStringAsync("https://furion.net/test-json"); var model = JsonSerializer.Deserialize(jsonString, new JsonSerializerOptions()); ``` --- If you want to replace the framework's default `System.Text.Json` serialization provider — for example, using `Newtonsoft.Json` to provide `JSON` serialization configuration options — you can satisfy this customization requirement by implementing the `IHttpContentProcessor` interface. > **Framework Recommendation** Note, however, that unless there is a compelling reason, it is generally recommended to use `System.Text.Json`, because it is tightly integrated with `.NET Core` and offers excellent performance. --- # 8.14 Disabling the Distributed Tracing Context (the traceparent Header) > Source: https://http.furion.net/en/docs/faq/disabling-the-distributed-tracing-context-the-traceparent-header/ When making an `HTTP` remote request, by default the distributed tracing context contained in the current `Activity` (such as `traceparent` and `tracestate`) is automatically injected into the request's `HTTP` headers. This mechanism helps downstream services correctly identify and correlate with the same distributed tracing chain. If, for certain business or security requirements, you need to disable this automatic injection behavior, you can configure it with the following code: ```csharp showLineNumbers {3,5} // Configure the default client services.AddHttpClient(string.Empty, client => { }) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler() { ActivityHeadersPropagator = null // Disable distributed context propagation, or use DistributedContextPropagator.CreateNoOutputPropagator() }); ``` By setting `ActivityHeadersPropagator` to `null`, you can prevent the runtime from automatically injecting the current active distributed tracing information into the HTTP request headers, thereby controlling tracing context propagation. Related Issues: [#109558](https://github.com/dotnet/runtime/issues/109558), [#90407](https://bgithub.xyz/dotnet/runtime/issues/90407), [#IC7ZZS](https://github.com/monksoul/HttpAgent/issues/IC7ZZS). --- # 8.15 Setting Content-Type (MIME) > Source: https://http.furion.net/en/docs/faq/setting-content-type-mime/ When sending an `HTTP` remote request, if the request includes a data body, it is usually necessary to correctly set the `Content-Type` request header to specify the `MIME` type of the content being sent, such as `application/json` or `application/x-www-form-urlencoded`. **Manually entering `MIME` types is prone to spelling errors**, which can lead to potential problems. To avoid such errors, it is recommended to use the `MediaTypeNames` static class provided by the framework. This class encapsulates the commonly used standard `MIME` type constants, effectively improving code readability and robustness. The following is an example of using `MediaTypeNames` to set `Content-Type`: ```cs showLineNumbers {2,5,8} HttpRequestBuilder.Get("https://furion.net/") .SetContentType(MediaTypeNames.Text.Plain); // text/plain HttpRequestBuilder.Post("https://furion.net/") .SetContentType(MediaTypeNames.Application.Json); // application/json HttpRequestBuilder.Post("https://furion.net/") .SetContentType(MediaTypeNames.Application.FormUrlEncoded); // application/x-www-form-urlencoded ``` By setting `Content-Type` in the manner above, you not only avoid manual input errors but also improve development efficiency and code maintainability. --- # 8.16 Using in Non-Dependency-Injection Environments (Console/WinForms/WPF) > Source: https://http.furion.net/en/docs/faq/di/ In applications such as `ASP.NET Core` or `Worker Service`, dependency injection support is usually built in. You only need to register the required services via `services` or `builder.Services` in the `Startup.cs` or `Program.cs` file. However, in certain specific scenarios — for example console applications (`Console`), `WinForms`, or `WPF` projects — `.NET` does not integrate a complete dependency injection container by default. For these scenarios, you can use the `Service` property of the `HttpRemoteClient` static class to send remote `HTTP` requests: ```cs showLineNumbers var result = await HttpRemoteClient.Service.GetAsStringAsync("https://furion.net/"); ``` ### Custom Configuration of the HTTP Remote Request Service If you need to customize the configuration of the `HTTP` remote request service, you can do so by calling the `HttpRemoteClient.Configure(services => {})` method: ```cs showLineNumbers {1,7} HttpRemoteClient.Configure(services => { // Example: configure the default HttpClient services.AddHttpClient(string.Empty).AddProfilerDelegatingHandler(); // To customize the HTTP remote request service, configure it by calling AddHttpRemote(); by default, no registration is required ⚠️ // services.AddHttpRemote(); }); ``` After completing the custom configuration, the `HttpRemoteClient.Service` static property automatically uses the instance built from the latest service build information. ### Sharing the Instance with the Application's Dependency Injection Container Although `HttpRemoteClient` is primarily intended for non-dependency-injection scenarios, it also allows injecting the application's own root service container so that it shares the same `IHttpRemoteService` instance with the existing dependency injection system. This works in any application where an `IServiceProvider` has already been built (including `Console`, `WinForms`, `WPF`, `Worker Service`, and `ASP.NET Core`). #### In Generic Host, `Worker Service`, or Console Applications In scenarios where an `IHost` or `IServiceProvider` has already been built, simply call the `UseHttpRemoteClient` extension method to inject the root container: ```cs showLineNumbers {7,9} // Using Worker Service as an example var builder = Host.CreateApplicationBuilder(args); builder.Services.AddHttpRemote(); var host = builder.Build(); // Inject the root container host.Services.UseHttpRemoteClient(); var result = await HttpRemoteClient.Service.GetAsStringAsync("https://furion.net/"); ``` #### In `ASP.NET Core` Applications A dedicated `UseHttpRemoteClient` extension method is provided, which can be injected in a single line in `Program.cs`: ```cs showLineNumbers {4} var builder = WebApplication.CreateBuilder(args); builder.Services.AddHttpRemote(); var app = builder.Build().UseHttpRemoteClient(); // Inject the root container into HttpRemoteClient app.Run(); ``` From then on, `HttpRemoteClient.Service` is exactly the same instance returned by `app.Services.GetRequiredService()`, and no separate internal container is maintained anymore. > **Notes on the `HttpRemoteClient.Service` Instance** Please note: - **Default behavior**: `HttpRemoteClient.Service` maintains an independent `IServiceProvider` instance internally, isolated from the application host's dependency injection container, so the instance obtained is different. - **After injecting an external container**: If the application's root container is injected via `SetServiceProvider` or `UseHttpRemoteClient`, then `HttpRemoteClient.Service` directly uses the external container's `IHttpRemoteService` instance — exactly the same as the instance in a dependency injection environment — and no separate container is maintained. Regardless of the approach, it is recommended to manually call the `HttpRemoteClient.Dispose()` method to release resources when the application shuts down or no longer needs the `HTTP` remote request service (this operation only releases the internally built container and does not affect an externally injected container): ```cs showLineNumbers HttpRemoteClient.Dispose(); ``` **Please note: once the `Dispose()` method is called, `HttpRemoteClient.Service` can no longer be instantiated again.** --- In addition to using the `Service` property provided by the `HttpRemoteClient` static class, you can also choose to manually build a service container and resolve an `IHttpRemoteService` instance from it, as shown below: ```csharp showLineNumbers {2,5,8,11} // Create a new ServiceCollection instance var services = new ServiceCollection(); // Add the required HTTP remote request service to the ServiceCollection services.AddHttpRemote(); // Build the ServiceProvider instance using var provider = services.BuildServiceProvider(); // Get the IHttpRemoteService instance from the ServiceProvider var httpRemoteService = provider.GetRequiredService(); ``` This approach is suitable for scenarios where you need full control over the dependency injection container's lifetime. If you want `HttpRemoteClient.Service` to also share the service instance with the manually built container, use `provider.UseHttpRemoteClient()` to inject that container instead of creating them separately. --- # 8.17 Parallel Requests (Batch Downloads) > Source: https://http.furion.net/en/docs/faq/parallel/ In scenarios where multiple `HTTP` requests need to be sent simultaneously (such as batch downloads, concurrently calling multiple `API`s, mixing the execution of different types of requests, etc.), you can use the framework's built-in `ParallelUtility` utility class, which provides a more concise `API` and supports controlling the maximum degree of concurrency. ### Concurrently Executing the Same Operation on a Collection Use `ParallelUtility.ForEachAsync` to concurrently execute the same asynchronous operation on each element of a collection: ```cs showLineNumbers {9-12,14} var urls = new[] { "https://furion.net/api/users", "https://furion.net/api/orders", "https://furion.net/api/products" }; // Concurrent requests, default maximum degree of concurrency is 4 var results = await ParallelUtility.ForEachAsync(urls, async (url, token) => { return await _httpRemoteService.GetAsStringAsync(url, cancellationToken: token); }); // results are returned in the original collection order ``` You can also control the maximum degree of concurrency via the `maxDegreeOfParallelism` parameter: ```cs showLineNumbers {4} await ParallelUtility.ForEachAsync(urls, async (url, token) => { await _httpRemoteService.GetAsStringAsync(url, cancellationToken: token); }, maxDegreeOfParallelism: 2); // Execute at most 2 requests concurrently ``` ### Concurrently Executing Multiple Different Operations When you need to simultaneously execute multiple **different types** of operations, use `ParallelUtility.RunAsync`: ```cs showLineNumbers {3-5} // Concurrently execute multiple different requests; all operations run simultaneously await ParallelUtility.RunAsync( token => _httpRemoteService.GetAsStringAsync("https://furion.net/api/users", cancellationToken: token), token => _httpRemoteService.PostAsStringAsync("https://furion.net/api/orders", "\"Furion\"", cancellationToken: token), token => _httpRemoteService.DownloadFileWithConsoleProgressAsync("https://furion.net/logo.png", @"C:\Workspaces\", FileExistsBehavior.Overwrite, cancellationToken: token) ); ``` If you need to obtain the return values, you can use the generic overload: ```cs showLineNumbers {2-4,7} var results = await ParallelUtility.RunAsync( async token => await _httpRemoteService.GetAsStringAsync("https://furion.net/api/users", cancellationToken: token), async token => await _httpRemoteService.GetAsStringAsync("https://furion.net/api/orders", cancellationToken: token), async token => await _httpRemoteService.GetAsStringAsync("https://furion.net/api/products", cancellationToken: token) ); // results[0], results[1], results[2] correspond to the results of each operation in the order passed in ``` > **Synchronous Version** `ParallelUtility` also provides synchronous versions, suitable for non-async contexts. --- # 8.18 Using LoadIntoBufferAsync to Cache Response Content > Source: https://http.furion.net/en/docs/faq/using-loadintobufferasync-to-cache-response-content/ When sending `HTTP` remote requests, the response content is returned as a stream by default and **can only be read once**. If you need to read the response content multiple times in different places (for example, for logging, content validation and business processing at the same time), reading the stream directly will cause subsequent reads to return empty or throw an exception. In this case, you can call the `HttpContent.LoadIntoBufferAsync()` method to **buffer the response content into memory**, thereby enabling subsequent **repeated reads**. ```cs showLineNumbers {4,7-8} var response = await httpRemoteService.GetAsync("https://furion.net/"); // Load the response content into the memory buffer await response.Content.LoadIntoBufferAsync(); // The response content can now be read multiple times var content1 = await response.Content.ReadAsStringAsync(); var content2 = await response.Content.ReadAsStringAsync(); ``` > **Notes** - `LoadIntoBufferAsync` loads the entire response body into memory, which may consume a lot of memory for **large responses** (such as large file downloads); use it with caution. - If the response content is already an in-memory type such as `ByteArrayContent`, there is no need to call this method, as it already supports repeated reads. - This method is already used internally by the `ETag` caching pipeline handler to ensure that the response content can still be read normally by the caller after it has been cached. --- # 8.19 Integrating with APIs Provided by Legacy Java Programs > Source: https://http.furion.net/en/docs/faq/integrating-with-apis-provided-by-legacy-java-programs/ When integrating with APIs provided by legacy `Java` programs, you usually need to enable the feature that automatically sets the `Host` header to ensure the request can be sent properly. For example: ```cs showLineNumbers {2} HttpRequestBuilder.Get("https://furion.net") .AutoSetHostHeader(); // Enable automatic setting of the Host header ``` --- # 8.20 AddHttpRemote Ambiguity Error > Source: https://http.furion.net/en/docs/faq/addhttpremote-ambiguity-error/ If you encounter an ambiguity error with the `AddHttpRemote` method, you can resolve it by adding an empty delegate parameter to it, as shown below: ```cs showLineNumbers services.AddHttpRemote(builder => {}); ``` --- # 8.21 Feedback and Suggestions > Source: https://http.furion.net/en/docs/faq/feedback-and-suggestions/ > **Communicate with Us** You are welcome to submit an [Issue](https://github.com/monksoul/HttpAgent/issues/new) to HttpAgent. --- > **Learn More** To learn more about `HTTP` and `HttpClient`, refer to the following documentation sections: - [ASP.NET Core - Make HTTP requests](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/http-requests) - [HTTP support in .NET](https://learn.microsoft.com/zh-cn/dotnet/fundamentals/networking/http/http-overview) --- # 1.6 Changelog > Source: https://http.furion.net/en/docs/changelog/ ## 📝 Changelog - **New Features** - Added `HTTP` remote request `Mock` simulation testing support 4.9.9.74 ⏱️2026.08.13 [320c865](https://gitee.com/dotnetchina/Furion/commit/320c86569d5d796e3d42093f4c57e121e1800f4f) - Added `HTTP` remote request custom `Logger` support 4.9.9.74 ⏱️2026.08.13 [320c865](https://gitee.com/dotnetchina/Furion/commit/320c86569d5d796e3d42093f4c57e121e1800f4f) - Added `HTTP` remote requests support setting `MCP 2.0` message content 4.9.9.71 ⏱️2026.08.11 [5513b32](https://gitee.com/dotnetchina/Furion/commit/5513b324df29c54ea508552937bde5f526494036) - Added `HTTP` remote requests support sending file content and binary stream content directly 4.9.9.68 ⏱️2026.08.09 [63735d6](https://gitee.com/dotnetchina/Furion/commit/63735d64030c725902a3b7b89352fe14574047b8) - Added `HTTP` remote requests support creating from a `cURL` command string 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Added `HTTP` remote requests support appending to specific types of request content 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Added `HTTP` remote requests support passing `Key: Value` pairs to set request headers 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Added `HTTP` remote requests support enabling standard request header features 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Added `HTTP` remote request `HttpContext` forwarding global configuration option `ConfigureForwardOptions` 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Added the built-in WeChat development platform `Access Token` provider in `HTTP` remote requests 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added support for retrieving raw message lines when sending `Server-Sent Events` in `HTTP` remote requests 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added support for the `HttpRemoteClient` static class to use an external service container in `HTTP` remote requests (resolving the issue where static classes cannot apply external service configuration, and the upgrade issue of legacy string-based extension requests) 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added the `HttpRequestBuilderConfigurator` abstract base class for preconfiguring `HttpRequestBuilder` in `HTTP` remote requests 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added support for appending multipart form content (`WithMultipart(u=>{})`) in the `HttpRequestBuilder` and `HttpFileUploadBuilder` builders of `HTTP` remote requests 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added support for setting the `SOAPAction` method (for `WebService`) in `HttpRequestBuilder` of `HTTP` remote requests 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added support for automatically correcting `GET` and `HEAD` requests that carry request content (automatically converting them to `POST`) when sending `Server-Sent Events` in `HTTP` remote requests 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added `UseHttpRemoteClient(serviceProvider)` related extension methods for `HTTP` remote requests, supporting presetting the `HttpRemoteClient` static class to use an external service container 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added support for `RFC 2047`, `RFC 5987`, and `Latin-1` (`Mojibake`) encoded filename resolution when downloading files in `HTTP` remote requests 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added helper methods for sending `Server-Sent Events` in `HTTP` remote requests (setting response headers and streaming output) 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added the `SetOnRedirect` configurable delegate during redirection in `HTTP` remote requests 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added file upload and download methods with console progress printing in `HTTP` remote requests 4.9.9.55 ⏱️2026.08.02 [8bd413e](https://gitee.com/dotnetchina/Furion/commit/8bd413eea24bd4b08a43affe546588ba694f2e17) - Added the `Action` operator in `HTTP` remote requests, supporting implicit conversion of `HttpRequestBuilder` to `Action` 4.9.9.52 ⏱️2026.08.01 [2c3d42d](https://gitee.com/dotnetchina/Furion/commit/2c3d42d5783e041222340e2230502b85bfaa9974) - Added the `HttpRequestBuilder.Setup` and `HttpBuilder.Setup` static properties in `HTTP` remote requests 4.9.9.52 ⏱️2026.08.01 [2c3d42d](https://gitee.com/dotnetchina/Furion/commit/2c3d42d5783e041222340e2230502b85bfaa9974) - Added **`HTTP` remote request `ETag` caching** 4.9.9.48 ⏱️2026.07.31 [53c6eb8](https://gitee.com/dotnetchina/Furion/commit/53c6eb8850aff618ee97bbfad8f2b0fd5b5bb34d) - Added **`HTTP` remote request quota policy** 4.9.9.46 ⏱️2026.07.29 [22c8edb](https://gitee.com/dotnetchina/Furion/commit/22c8edbe5e249fe10ad011d0dc8982b61278422c) - Added `HTTP` remote requests support `zstd` decompression (`.NET11`) 4.9.9.46 ⏱️2026.07.29 [22c8edb](https://gitee.com/dotnetchina/Furion/commit/22c8edbe5e249fe10ad011d0dc8982b61278422c) - Added the `ContentEquals`, `ContentMatches`, `ContentNotEmpty`, and `HeaderNotExists` assertion methods in `HTTP` remote requests 4.9.9.46 ⏱️2026.07.29 [95f0380](https://gitee.com/dotnetchina/Furion/commit/95f0380bf4be2430aa28ef80405140526828e649) - Added `HTTP` remote request `SSE` and long polling support returning `IAsyncEnumerable` 4.9.9.43 ⏱️2026.07.26 [5fd05a1](https://gitee.com/dotnetchina/Furion/commit/5fd05a1bacf8bf67883b3bb8000164e41cacee32) - Added automatic `Token` refresh for the `Furion` framework in `HTTP` remote requests 4.9.9.39 ⏱️2026.07.21 [6611c06](https://gitee.com/dotnetchina/Furion/commit/6611c0686b963add05ba7119cf0a4ffa41b21a9d) - Added the `JwtTokenUtility` `JWT` parsing utility class in `HTTP` remote requests 4.9.9.39 ⏱️2026.07.21 [6611c06](https://gitee.com/dotnetchina/Furion/commit/6611c0686b963add05ba7119cf0a4ffa41b21a9d) - Added `HTTP` remote requests support configuring `IHttpRequestEventHandler` for specific `HttpClient` instances 4.9.9.39 ⏱️2026.07.21 [6611c06](https://gitee.com/dotnetchina/Furion/commit/6611c0686b963add05ba7119cf0a4ffa41b21a9d) - Added `HTTP` remote requests support configuring format strings for query parameters, request headers, and `Cookie` 4.9.9.38 ⏱️2026.07.21 [b783f3e](https://gitee.com/dotnetchina/Furion/commit/b783f3e00611d6bd84055fc0a1289bc20ccb7128) - Added default logging for the retry policy in `HTTP` remote requests 4.9.9.38 ⏱️2026.07.21 [b783f3e](https://gitee.com/dotnetchina/Furion/commit/b783f3e00611d6bd84055fc0a1289bc20ccb7128) - Added automatic warning log output when an exception occurs while exception suppression is enabled in `HTTP` remote requests 4.9.9.38 ⏱️2026.07.21 [b783f3e](https://gitee.com/dotnetchina/Furion/commit/b783f3e00611d6bd84055fc0a1289bc20ccb7128) - Added dependency-free declarative proxy support in `HTTP` remote requests 4.9.9.37 ⏱️2026.07.20 [f732128](https://gitee.com/dotnetchina/Furion/commit/f7321287388e3998b373d93cf38ce3c6f01a2e9f) - Added `HTTP` remote declarative requests support the `ValueTask` return type 4.9.9.36 ⏱️2026.07.19 [a221418](https://gitee.com/dotnetchina/Furion/commit/a221418fecae702978ce301f3b66dba2d2a17d41) - Added `HTTP` remote request `URL` addresses support `{key?}` and `{**key}` path parameters 4.9.9.36 ⏱️2026.07.19 [a221418](https://gitee.com/dotnetchina/Furion/commit/a221418fecae702978ce301f3b66dba2d2a17d41) - Added the `HttpRemoteResult` content converter in `HTTP` remote requests 4.9.9.34 ⏱️2026.07.18 [3ae3f64](https://gitee.com/dotnetchina/Furion/commit/3ae3f64e8f42fd91e21707b6847550190a50f040) - Added `HTTP` remote requests support `RFC 3986` standard request address concatenation 4.9.9.34 ⏱️2026.07.18 [3ae3f64](https://gitee.com/dotnetchina/Furion/commit/3ae3f64e8f42fd91e21707b6847550190a50f040) - Added `Access Token` automatic management in `HTTP` remote requests 4.9.9.25 [d618a54](https://gitee.com/dotnetchina/Furion/commit/d618a54ed3ef9dc3c42d625e26f6d0360554e157) - Added retry in `HTTP` remote requests 4.9.9.25 [d618a54](https://gitee.com/dotnetchina/Furion/commit/d618a54ed3ef9dc3c42d625e26f6d0360554e157) - Added `HTTP` remote requests support customizing the pipeline handler for sending requests 4.9.9.25 [d618a54](https://gitee.com/dotnetchina/Furion/commit/d618a54ed3ef9dc3c42d625e26f6d0360554e157) - Added `HTTP` remote requests support multi-instance service registration 4.9.9.16 [1284ea8](https://gitee.com/dotnetchina/Furion/commit/1284ea80ffa0e4c2c9ca44bb21650d24fd1ac632) - Added `HTTP` remote request content processors support returning combinations of multiple `HttpContent` instances 4.9.9.16 [1284ea8](https://gitee.com/dotnetchina/Furion/commit/1284ea80ffa0e4c2c9ca44bb21650d24fd1ac632) - Added the `RemoveContent` method to the `HTTP` remote request builder 4.9.9.16 [1284ea8](https://gitee.com/dotnetchina/Furion/commit/1284ea80ffa0e4c2c9ca44bb21650d24fd1ac632) - Added `HTTP` remote requests support batch-adding objects to dispose when a request completes via the `AddDisposables` method 4.9.9.16 [1284ea8](https://gitee.com/dotnetchina/Furion/commit/1284ea80ffa0e4c2c9ca44bb21650d24fd1ac632) - Added methods to get the local `IP` address and `MAC` address in `HTTP` remote requests: `HttpRemoteUtility.GetLocalIPv4()`, `HttpRemoteUtility.GetLocalMacAddress()` 4.9.9.16 [1284ea8](https://gitee.com/dotnetchina/Furion/commit/1284ea80ffa0e4c2c9ca44bb21650d24fd1ac632) - Added `HTTP` remote requests can use `.SetBaseAddress(url)` or `[BaseAddress(url]` as a common prefix 4.9.9.13 ⏱️2026.07.05 [0224fa0](https://gitee.com/dotnetchina/Furion/commit/0224fa0a5bdf21ee5f1488b0697b4f12daf95375) - Added `HTTP` remote requests support adding generic response content converters 4.9.9.10 ⏱️2026.07.03 [9a37cfa](https://gitee.com/dotnetchina/Furion/commit/9a37cfa1f89f1decc0b745d3b73302c15e8c35ac) - Added the `ResultHandler` feature for `JSON` response deserialization wrappers in `HTTP` remote requests 4.9.9.10 ⏱️2026.07.03 [9a37cfa](https://gitee.com/dotnetchina/Furion/commit/9a37cfa1f89f1decc0b745d3b73302c15e8c35ac) - Added `HTTP` remote requests support sending `IBrowserFile` files 4.9.9.9 ⏱️2026.07.02 [fa73c28](https://gitee.com/dotnetchina/Furion/commit/fa73c28dbfff64d275ec85385983bcec64a7df71) - Added `HTTP` declarative requests support setting headers via `[Header("Key: Value")]` 4.9.9.9 ⏱️2026.07.02 [fa73c28](https://gitee.com/dotnetchina/Furion/commit/fa73c28dbfff64d275ec85385983bcec64a7df71) - Added the `SetContent` method of `HTTP` remote requests supports configuring the `disposeResourcesOnRequestCompletion` parameter 4.9.9.9 ⏱️2026.07.02 [fa73c28](https://gitee.com/dotnetchina/Furion/commit/fa73c28dbfff64d275ec85385983bcec64a7df71) - Added the `IFormFile` content processor in `HTTP` remote requests 4.9.9.9 ⏱️2026.07.02 [8a8e44b](https://gitee.com/dotnetchina/Furion/commit/8a8e44b3490a7195911759005b2c5c17d037b20a) - Added `HTTP` remote request `IAsyncEnumerable` response content type support 4.9.9.3 ⏱️2026.06.25 [4b80b9b](https://gitee.com/dotnetchina/Furion/commit/4b80b9b484a9b1ef2cc29a3387a9a1a184a61491) - Added `HTTP` remote requests support saving a stream or byte array to a local file 4.9.8.97 ⏱️2026.06.16 [c8725dc](https://gitee.com/dotnetchina/Furion/commit/c8725dcc890d443aa6e8d0c888b634001bb2b77e) - Added the `FileInfo` request content processor in `HTTP` remote requests 4.9.8.92 ⏱️2026.06.08 [c9e3471](https://gitee.com/dotnetchina/Furion/commit/c9e347114c929ba1070a928ac69d8a284de22485) - Added `HTTP` remote requests support handling double-serialized `JSON` response content 4.9.8.66 ⏱️2026.05.15 [54466a9](https://gitee.com/dotnetchina/Furion/commit/54466a91119cfd15581e65fc6048f4a9357857e0) - Added the `IgnoreQueryParameters` option when forwarding `HttpContext` in `HTTP` remote requests 4.9.8.64 ⏱️2026.05.13 [18d1922](https://gitee.com/dotnetchina/Furion/commit/18d1922b157d31dbec2a55302e8f173023559c70) - Added `HTTP` remote request `JSON Lines` data format support 4.9.8.63 ⏱️2026.05.12 [9abc295](https://gitee.com/dotnetchina/Furion/commit/9abc295a58776f7754cd01888c26c34508733694) - Added `HTTP` remote request query parameter sorting and form field submission sorting support 4.9.8.58 ⏱️2026.05.06 [d247eff](https://gitee.com/dotnetchina/Furion/commit/d247effaf9dd7d6526ba5589ecfd6b83e9f28c9d) - Added `HTTP` remote requests support setting the request `User-Agent` header via the `SetUserAgent` method 4.9.8.58 ⏱️2026.05.06 [d247eff](https://gitee.com/dotnetchina/Furion/commit/d247effaf9dd7d6526ba5589ecfd6b83e9f28c9d) - Added the `UserAgents` static class in `HTTP` remote requests 4.9.8.58 ⏱️2026.05.06 [d247eff](https://gitee.com/dotnetchina/Furion/commit/d247effaf9dd7d6526ba5589ecfd6b83e9f28c9d) - Added `HTTP` declarative requests support disabling data validation 4.9.8.48 ⏱️2026.04.22 [3979720](https://gitee.com/dotnetchina/Furion/commit/39797204ea4a1a6644790f621f811287175d87f6) - Added `UriBuilder` configuration operations in `HTTP` remote requests 4.9.8.45 ⏱️2026.04.19 [56be6c6](https://gitee.com/dotnetchina/Furion/commit/56be6c63d7ec079559cddf45531f8232ead19381) - Added `HTTP` declarative requests support object-oriented inheritance 4.9.8.42 ⏱️2026.04.17 [b00f2b9](https://gitee.com/dotnetchina/Furion/commit/b00f2b9a1ebbe8e2005af6e2eb940a3eff48ebc4) - Added `HTTP` remote requests support setting never time out 4.9.8.21 ⏱️2026.03.09 [92e0283](https://gitee.com/dotnetchina/Furion/commit/92e0283b8aba9f1cc9eeb9540392855095f2f0b5) - Added `HTTP` remote requests support sending form data without `URL` encoding 4.9.8.15 ⏱️2026.02.09 [f0104ef](https://gitee.com/dotnetchina/Furion/commit/f0104ef8154585b2f3d8faec39def9b4fb3797e2) - Added `HTTP` remote declarative requests support `Action` freeze parameters 4.9.7.244 ⏱️2026.01.09 [d9fce11](https://gitee.com/dotnetchina/Furion/commit/d9fce115f4bda37d743fe97e7f3b97e7d3eee9f3) - Added `HTTP` remote requests support the `IHttpRequestBuilderConfigurer` unified configurator for `HttpRequestBuilder` 4.9.7.244 ⏱️2026.01.09 [d9fce11](https://gitee.com/dotnetchina/Furion/commit/d9fce115f4bda37d743fe97e7f3b97e7d3eee9f3) - Added `HTTP` remote requests support configuring `HttpClient` and `HttpRequestMessage` instances when downloading file streams from internet `URL` addresses 4.9.7.235 ⏱️2025.12.27 [a723ae5](https://gitee.com/dotnetchina/Furion/commit/a723ae5cbb93985969c342f7dffb22024e314e5b) - Added `HTTP` remote requests support configuring parameters when setting request headers and `Cookie` 4.9.7.231 ⏱️2025.12.19 [541fadd](https://gitee.com/dotnetchina/Furion/commit/541fadd4cebae94fc3686ffb36a337b193a307c5) - Added `HTTP` remote requests support requesting via a specified network adapter `IP` address 4.9.7.230 ⏱️2025.12.19 [904705d](https://gitee.com/dotnetchina/Furion/commit/904705d20de53c41eed9d798ccbdb50be13a2408) - Added the `HttpBuilder` static class in `HTTP` remote requests, used to simplify the overly long `HttpRequestBuilder` name 4.9.7.222 ⏱️2025.12.08 [c0b6c77](https://gitee.com/dotnetchina/Furion/commit/c0b6c77bdc5d4ea83de9891df8dac002a23404ad) - Added `HTTP` remote request analysis logs support color highlighting 4.9.7.217 ⏱️2025.12.03 [29b9348](https://gitee.com/dotnetchina/Furion/commit/29b93485d9d4f14215dcac687e5601321438aa4c) - Added `HTTP` remote requests support setting `JSON` response deserialization wrappers 4.9.7.214 ⏱️2025.11.26 [f046b4d](https://gitee.com/dotnetchina/Furion/commit/f046b4d423b90a6f6d6aa27cab0f0f671225dff6) [ebe71f9](https://gitee.com/dotnetchina/Furion/commit/ebe71f94f126741153a12e3857ba2536668bd8e6) - Added `HTTP` remote requests support setting a log fallback output delegate when no logging service is configured 4.9.7.213 ⏱️2025.11.26 [17ac155](https://gitee.com/dotnetchina/Furion/commit/17ac155b562c8a18eb2c758777aa4776b2f485eb) - Added `HTTP` remote requests support passing a `JsonSerializerOptions` object when setting `JSON` data 4.9.7.208 ⏱️2025.11.16 [c97b467](https://github.com/monksoul/HttpAgent/commit/c97b467f87a56512d8d196e51c969c05ffa180c0) - Added `HTTP` remote requests support automatically fixing invalid response character encoding 4.9.7.202 ⏱️2025.11.13 [35530e8](https://gitee.com/dotnetchina/Furion/commit/35530e889181beed9672619989a9cbe2edd2c7ca) - Added `HTTP` remote requests support form name naming policies or custom converters 4.9.7.137 ⏱️2025.11.07 [6c175a8](https://gitee.com/dotnetchina/Furion/commit/6c175a8502806ff434898b19160874592efbdca3) - Added **`HTTP` remote request assertion feature** 4.9.7.137 ⏱️2025.11.07 [c044b87](https://gitee.com/dotnetchina/Furion/commit/c044b87cd1a6243637612872df1dd4a3a8cf4100) - Added `HTTP` remote requests enable automatic decompression of `gzip`, `deflate`, `brotli`, and `zstd` response content by default 4.9.7.137 ⏱️2025.11.07 [e9b10ac](https://gitee.com/dotnetchina/Furion/commit/e9b10ac12128e8b79cf320cf4110e87cc827b5fc) - Added the `Profiler(enabled)` alias method `Debugger([enabled])` for the `HTTP` remote request analysis tool 4.9.7.137 ⏱️2025.11.07 [c044b87](https://gitee.com/dotnetchina/Furion/commit/c044b87cd1a6243637612872df1dd4a3a8cf4100) - Added `HTTP` remote requests support adding dynamic `URL` parameters (evaluated at request time) 4.9.7.131 ⏱️2025.10.17 [a162c8d](https://gitee.com/dotnetchina/Furion/commit/a162c8dc2e9aca1e27ed397d8d9f9784e636b559) - Added `HTTP` remote request stress testing supports conveniently disabling `HTTP` caching 4.9.7.131 ⏱️2025.10.17 [a162c8d](https://gitee.com/dotnetchina/Furion/commit/a162c8dc2e9aca1e27ed397d8d9f9784e636b559) - Added `HTTP` remote requests support configuring multi-threaded file downloads 4.9.7.123 ⏱️2025.09.16 [10bddc9](https://gitee.com/dotnetchina/Furion/commit/10bddc926cfcc8590bfc7153b67a9f513d9f81b0) - Added `HTTP` remote requests support converting `XML` strings to typed objects 4.9.7.123 ⏱️2025.09.16 [41746d2](https://gitee.com/dotnetchina/Furion/commit/41746d215cffa5eb3ddee2396b3f62be4c658068) - Added `HTTP` remote request builder instances support `When` conditional construction 4.9.7.100 ⏱️2025.07.22 [651b4d5](https://gitee.com/dotnetchina/Furion/commit/651b4d5a5a1467facc5015017026530b05d523f9) - Added the `With(Action)` method for the `HTTP` remote request extension builder 4.9.7.95 ⏱️2025.07.10 [4615670](https://gitee.com/dotnetchina/Furion/commit/461567045a1dfced8f4a12b2af028f15b653af21) - Added the `[MultipartObject]` attribute in `HTTP` remote declarative requests 4.9.7.94 ⏱️2025.07.09 [7e52e9c](https://gitee.com/dotnetchina/Furion/commit/7e52e9c5b1fccfb3923d6018b2af0d9ac0c8f438) - Added `HTTP` remote requests support the `Unix epoch` date format 4.9.7.77 ⏱️2025.05.31 [ca9c94e](https://gitee.com/dotnetchina/Furion/commit/ca9c94e2d50cede3edcc3911ad6593c520ac6589) - Added `URL` parameter formatters in `HTTP` remote requests 4.9.7.70 ⏱️2025.05.23 [e8b24b3](https://gitee.com/dotnetchina/Furion/commit/e8b24b3f5480e6cdca8a56e1cd01a9cd96603bac) - Added `HTTP` remote requests support configuring `SocketsHttpHandler` to ignore `SSL` certificate validation 4.9.7.63 ⏱️2025.05.16 [042da35](https://gitee.com/dotnetchina/Furion/commit/042da3566110ac0c7abd28319d76154a45ff6ced) - Added `HTTP` remote requests support configuring the callback operation when a request timeout occurs 4.9.7.62 ⏱️2025.05.15 [23a580d](https://gitee.com/dotnetchina/Furion/commit/23a580daff6914a5a186d798c4dbceb6caaad5a7) - Added the `HttpRemoteClient` static class in `HTTP` remote requests 4.9.7.58 ⏱️2025.05.02 [86e9dbe](https://gitee.com/dotnetchina/Furion/commit/86e9dbe197df8efb015e4e22cba4469e715aea27) - Added deconstruction (destructuring expression) support for `HttpRemoteResult` in `HTTP` remote requests 4.9.7.53 ⏱️2025.04.28 [e4dcc10](https://gitee.com/dotnetchina/Furion/commit/e4dcc1028eccd1ffad5471646cb079778ead3ce3) - Added the `IHttpClientBuilder.ConfigureOptions(configure)` extension method in `HTTP` remote requests 4.9.7.51 ⏱️2025.04.26 [33479e2](https://gitee.com/dotnetchina/Furion/commit/33479e212bd09f1867e69e9c428d63b7ece7fa58) - Added printing of the `HttpClient Name` item in the `HTTP` remote request analysis tool 4.9.7.51 ⏱️2025.04.26 [33479e2](https://gitee.com/dotnetchina/Furion/commit/33479e212bd09f1867e69e9c428d63b7ece7fa58) - Added the `WithSuccessStatusCodeHandler` method supports setting success status code callback operations in `HTTP` remote requests 4.9.7.47 ⏱️2025.04.20 [cf7956e](https://gitee.com/dotnetchina/Furion/commit/cf7956e227d978a05c6e0d294766aa45a681f1b9) - Added `HTTP` remote request status code handlers support the `~` symbol to set ranges, such as `200~299` 4.9.7.47 ⏱️2025.04.20 [cf7956e](https://gitee.com/dotnetchina/Furion/commit/cf7956e227d978a05c6e0d294766aa45a681f1b9) - Added the `SetOmitContentType(omit)` method supports removing or keeping the `Content-Type` of request content in `HTTP` remote requests 4.9.7.44 ⏱️2025.04.17 [4d98d60](https://gitee.com/dotnetchina/Furion/commit/4d98d6053f7b05c73b6d60d5284c040538f18789) - Added `HTTP` remote requests support creating `HttpRequestBuilder` instances from `JSON` strings 4.9.7.41 ⏱️2025.04.14 [580dd04](https://gitee.com/dotnetchina/Furion/commit/580dd04362d5c6fb5753402838b8029d0793c2a4) - Added `HTTP` remote requests support using `SuppressExceptions()` and `[SuppressExceptions]` to suppress request exceptions 4.9.7.40 ⏱️2025.04.12 [1a9bc7b](https://gitee.com/dotnetchina/Furion/commit/1a9bc7b90472f81cee2fa80ba1459f07484c08f7) - Added `HTTP` remote requests support setting the `HTTP` version of a single request 4.9.7.40 ⏱️2025.04.12 [1a9bc7b](https://gitee.com/dotnetchina/Furion/commit/1a9bc7b90472f81cee2fa80ba1459f07484c08f7) - Added printing of the `HTTP Version` item in the `HTTP` remote request analysis tool 4.9.7.40 ⏱️2025.04.12 [1a9bc7b](https://gitee.com/dotnetchina/Furion/commit/1a9bc7b90472f81cee2fa80ba1459f07484c08f7) - Added the `Version` property (`HTTP` version) to the `HttpRemoteResult` type in `HTTP` remote requests 4.9.7.40 ⏱️2025.04.12 [1a9bc7b](https://gitee.com/dotnetchina/Furion/commit/1a9bc7b90472f81cee2fa80ba1459f07484c08f7) - Added `HTTP` remote requests support setting the request referrer address 4.9.7.36 ⏱️2025.04.02 [5d4a241](https://gitee.com/dotnetchina/Furion/commit/5d4a241a8dcd63ed2b7fc7a3692b3418c69d3fc5) - Added the `HttpRequestBuilder.AddAuthentication(string, string?)` overload method in `HTTP` remote requests 4.9.7.33 ⏱️2025.03.25 [f8a648a](https://gitee.com/dotnetchina/Furion/commit/f8a648a7377617817ed629da63f1154246eb244f) - Added the `AddFile(IFormFile)` and `AddFiles(IEnumerable)` extension methods for multipart forms in `HTTP` remote requests 4.9.7.31 ⏱️2025.03.24 [6eb54e0](https://gitee.com/dotnetchina/Furion/commit/6eb54e0f6851158149ca0c48a6604839f07bbf40) - Added `HTTP` remote requests support converting `Number` and `Boolean` types to `String` during deserialization 4.9.7.29 ⏱️2025.03.23 [489aa55](https://gitee.com/dotnetchina/Furion/commit/489aa55fbe05ccd889c2b168f7d012918fdb5e1e) - Added `HTTP` remote requests automatically handle Chinese garbled text (mojibake) during serialization 4.9.7.29 ⏱️2025.03.23 [489aa55](https://gitee.com/dotnetchina/Furion/commit/489aa55fbe05ccd889c2b168f7d012918fdb5e1e) - Added `HTTP` remote requests support non-`ISO 8601-1:2019` standard time strings during `JSON` deserialization 4.9.7.25 ⏱️2025.03.14 [10de94b](https://gitee.com/dotnetchina/Furion/commit/10de94babfcc60cb2837714ce9de5c2284e21ced) [3f3d619](https://gitee.com/dotnetchina/Furion/commit/3f3d61965e432c6597039a630e516f59cad08de4) - Added `HTTP` remote requests support adding configuration to all `HttpClient` clients via `IHttpRemoteBuilder.ConfigureHttpClientDefaults(configure)` 4.9.7.22 ⏱️2025.03.04 [cef4ca0](https://gitee.com/dotnetchina/Furion/commit/cef4ca03a727792663eb2d7b4fa8ad9f196cf3ef) - Added `HTTP` remote requests support setting path segments via `WithPathSegment[s]` 4.9.7.21 ⏱️2025.03.03 [7b3335e](https://gitee.com/dotnetchina/Furion/commit/7b3335e8af30509aa1f0465a881693bd3b6f114b) - Added `HTTP` remote requests support enabling the request analysis tool for all `HttpClient` clients via `IHttpRemoteBuilder.AddProfilerDelegatingHandler()` 4.9.7.18 ⏱️2025.03.01 [b6ba52b](https://gitee.com/dotnetchina/Furion/commit/b6ba52bea7f40098a101811c5eb403456139de3c) - Added `HTTP` remote requests support `WebService (SOAP)` 4.9.7.15 ⏱️2025.02.27 [479073a](https://gitee.com/dotnetchina/Furion/commit/479073abf3712bcb9e5566e762289281ea7e6ec1) - Added the `AddProfilerDelegatingHandler(this IHttpClientBuilder builder, bool disableInProduction)` overload method in `HTTP` remote requests 4.9.7.13 ⏱️2025.02.26 [5ef4b13](https://gitee.com/dotnetchina/Furion/commit/5ef4b13c522a824822266dbcf6ad91d8f65e701a) - Added `HTTP` remote request `Server-Sent Events` support arbitrary `HttpMethod` 4.9.7.13 ⏱️2025.02.26 [caa2aca](https://gitee.com/dotnetchina/Furion/commit/caa2acaec7da88d7f9f879ac0fcfe73bd1dc71db) - Added the `Set-Cookie` response header extension method in `HTTP` remote requests 4.9.7.11 ⏱️2025.02.24 [62737cf](https://gitee.com/dotnetchina/Furion/commit/62737cfccfbc130eaf9bcf8e1ffce15bf690e506) - Added `HTTP` remote requests support setting the trigger delegate for the request analysis tool 4.9.7.10 ⏱️2025.02.22 [82b4d81](https://gitee.com/dotnetchina/Furion/commit/82b4d81ae60f1918f06cc28b780902f7096c4fa4) - Added the `ConfigureOptions` overload method that supports resolving services in `HTTP` remote requests 4.9.7.9 ⏱️2025.02.20 [dabbc47](https://gitee.com/dotnetchina/Furion/commit/dabbc47d78a1bfab82d367ef359ddf10d94c298d) - Added the `FallbackBaseAddress` property to the `HttpRemoteOptions` option, supporting fallback base address settings in `HTTP` remote requests 4.9.7.9 ⏱️2025.02.20 [dabbc47](https://gitee.com/dotnetchina/Furion/commit/dabbc47d78a1bfab82d367ef359ddf10d94c298d) - Added the `Server` property to the `HttpRemoteResult` type in `HTTP` remote requests 4.9.7.9 ⏱️2025.02.20 [5b1c181](https://gitee.com/dotnetchina/Furion/commit/5b1c18130cb1324ec6ad4b723cd47d101f33a402) - Added the `HttpRequestMessage` clone extension method in `HTTP` remote requests 4.9.7.8 ⏱️2025.02.18 [abd61c8](https://gitee.com/dotnetchina/Furion/commit/abd61c888a7032e64b63943a35765a8d6eb8c46c) - Added the `[Forward]` forwarding attribute in `HTTP` remote requests 4.9.7 ⏱️2025.01.23 [023166b](https://gitee.com/dotnetchina/Furion/commit/023166b0439e5c43c7f3f58bd88fef3be8f98473) - Added configuration parameter support in `HTTP` remote requests 4.9.7 ⏱️2025.01.23 [023166b](https://gitee.com/dotnetchina/Furion/commit/023166b0439e5c43c7f3f58bd88fef3be8f98473) - Added `HTTP` remote request forwarding supports ignoring request or response headers 4.9.7 ⏱️2025.01.23 [023166b](https://gitee.com/dotnetchina/Furion/commit/023166b0439e5c43c7f3f58bd88fef3be8f98473) - Added `HTTP` remote request redirection supports relative paths 4.9.6.21 ⏱️2024.12.28 [17df0c4](https://gitee.com/dotnetchina/Furion/commit/17df0c473a7c91d1989e2319109a24d7404e9d65) - Added a built-in automatic redirection processing flow in `HTTP` remote requests 4.9.6.20 ⏱️2024.12.27 [4998e13](https://gitee.com/dotnetchina/Furion/commit/4998e139dec691a154bfbd52463c5fd5e33f6141) - Added the `AllowAutoRedirect` and `MaximumAutomaticRedirections` options in the `HttpRemoteOptions` of `HTTP` remote requests 4.9.6.20 ⏱️2024.12.27 [4998e13](https://gitee.com/dotnetchina/Furion/commit/4998e139dec691a154bfbd52463c5fd5e33f6141) - Added the `WithCookie(cookieHeaderValue)` overload method in `HTTP` remote requests 4.9.6.18 ⏱️2024.12.25 [80394dc](https://gitee.com/dotnetchina/Furion/commit/80394dceb9d56bfbffd5612d14c350450ff8c93f) - Added `HTTP` remote requests support server interfaces of `HTTP/1.0` and `HTTP/1.1` with no configuration by default 4.9.6.16 ⏱️2024.12.17 [61afe9a](https://gitee.com/dotnetchina/Furion/commit/61afe9a28cad036ac51b3a457f865cad36711837) - Added `HTTP` remote requests support setting the request base address 4.9.6.15 ⏱️2024.12.10 [187a178](https://gitee.com/dotnetchina/Furion/commit/187a1787cfbc202e69fcd1132a924aad19b3380b) - Added `HTTP` remote requests support preset operations when adding form item content 4.9.6.12 ⏱️2024.12.06 [e610e32](https://gitee.com/dotnetchina/Furion/commit/e610e3233c201eda2397e5f9bc8b3cc7e6ee6375) - Added `HTTP` remote requests support printing the request analysis tool content in non-dependency-injection environments 4.9.6.12 ⏱️2024.12.06 [e610e32](https://gitee.com/dotnetchina/Furion/commit/e610e3233c201eda2397e5f9bc8b3cc7e6ee6375) - Added `HTTP` remote requests support declaratively setting `HttpRequestMessage` request property attributes 4.9.6.11 ⏱️2024.12.04 [8306cf0](https://gitee.com/dotnetchina/Furion/commit/8306cf018bac468d091431efd48e0f9d934190ca) - Added `HTTP` remote requests support configuring the delegate to disable the request analysis tool 4.9.6.7 ⏱️2024.12.02 [250ea66](https://gitee.com/dotnetchina/Furion/commit/250ea66c6c9fff98480c79a26e4a5ef629b99153) - Added `HTTP` remote requests support enabling performance optimization 4.9.6.6 ⏱️2024.12.01 [b7ad81b](https://gitee.com/dotnetchina/Furion/commit/b7ad81bd4b575f1cf9f141f581eb3e7027f741af) - Added `HTTP` remote requests support setting the automatic `Host` header 4.9.6.6 ⏱️2024.12.01 [b7ad81b](https://gitee.com/dotnetchina/Furion/commit/b7ad81bd4b575f1cf9f141f581eb3e7027f741af) - Added `DigestCredentials` digest authentication support in `HTTP` remote requests 4.9.6.5 ⏱️2024.12.01 [3298c02](https://gitee.com/dotnetchina/Furion/commit/3298c027a6df5c400c2885662ce00dc01a185e62) - Added the `FileTypeMapper` file `MIME` type mapping class in `HTTP` remote requests 4.9.6.4 ⏱️2024.11.29 [6782110](https://gitee.com/dotnetchina/Furion/commit/6782110d073a6193c431023b8c40c7ad4fb1129e) - Added `HTTP` remote requests support streams with application rate limiting 4.9.6.3 ⏱️2024.11.28 [f281c32](https://gitee.com/dotnetchina/Furion/commit/f281c32a877dedfd006271768b9054d133a65c29) - Added `HTTP` remote requests support server programs that require `Content-Type` validation 4.9.6.3 ⏱️2024.11.28 [f281c32](https://gitee.com/dotnetchina/Furion/commit/f281c32a877dedfd006271768b9054d133a65c29) - Added `HTTP` remote requests support configuring the request analysis tool log level 4.9.6.3 ⏱️2024.11.28 [f281c32](https://gitee.com/dotnetchina/Furion/commit/f281c32a877dedfd006271768b9054d133a65c29) - Added `HTTP` remote requests support global `HttpRemoteOptions` configuration 4.9.6.2 ⏱️2024.11.28 [b60c996](https://gitee.com/dotnetchina/Furion/commit/b60c99699d8de8000be19077d43a08858f28f874) - Added `HTTP` remote requests support configuring whether to ignore null values in query parameters (`ignoreNullValues`) 4.9.6.2 ⏱️2024.11.28 [b60c996](https://gitee.com/dotnetchina/Furion/commit/b60c99699d8de8000be19077d43a08858f28f874) - Added `HTTP` remote request `MultipartFile` supports adding file types 4.9.6.1 ⏱️2024.11.27 [590cd5e](https://gitee.com/dotnetchina/Furion/commit/590cd5e93b15e9573a299710c81b8e4821e749d7) - Added `HTTP` remote request `WithStatusCodeHandler` supports status codes with comparison symbols 4.9.6.1 ⏱️2024.11.27 [590cd5e](https://gitee.com/dotnetchina/Furion/commit/590cd5e93b15e9573a299710c81b8e4821e749d7) - Added `HTTP` remote request `AddHttpDeclarativeExtractorsFromAssemblies` for batch registration of `HTTP` declarative extractors 4.9.6.1 ⏱️2024.11.27 [590cd5e](https://gitee.com/dotnetchina/Furion/commit/590cd5e93b15e9573a299710c81b8e4821e749d7) - Added `HTTP` remote request support for setting `MCP 2.0` message content 4.9.9.71 ⏱️2026.08.11 [5513b32](https://gitee.com/dotnetchina/Furion/commit/5513b324df29c54ea508552937bde5f526494036) - Added `HTTP` remote request support for directly sending file content and binary stream content 4.9.9.68 ⏱️2026.08.09 [63735d6](https://gitee.com/dotnetchina/Furion/commit/63735d64030c725902a3b7b89352fe14574047b8) - Added `HTTP` remote request support for creating from a `cURL` command string 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Added `HTTP` remote request support for appending to specific types of request content 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Added `HTTP` remote request support for passing `Key: Value` pairs to set request headers 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Added `HTTP` remote request support for enabling the standard request headers feature 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Added `HTTP` remote request built-in `Access Token` provider for the WeChat development platform 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added `HTTP` remote request support for getting the raw message line when sending `Server-Sent Events` 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added `HTTP` remote request `HttpRemoteClient` static class support for using an external service container (solving the issue that a static class cannot apply external service configuration, and the issue of upgrading old-version string extension requests) 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added `HTTP` remote request abstract base class `HttpRequestBuilderConfigurator` for preconfiguring `HttpRequestBuilder` 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added `HTTP` remote request `HttpRequestBuilder` and `HttpFileUploadBuilder` builders support for appending multipart form content (`WithMultipart(u=>{})`) 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added `HTTP` remote request `HttpRequestBuilder` support for setting the `SOAPAction` method (for `WebService`) 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added `HTTP` remote request support for automatically correcting `GET` and `HEAD` requests with request content when sending `Server-Sent Events` (automatically converted to `POST`) 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added `HTTP` remote request `UseHttpRemoteClient(serviceProvider)` related extension methods, supporting presetting the `HttpRemoteClient` static class to use an external service container 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added `HTTP` remote request support for parsing file names encoded with `RFC 2047`, `RFC 5987`, and `Latin-1` (`Mojibake`) when downloading files 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added `HTTP` remote request related helper methods for sending `Server-Sent Events` (setting response headers and streaming output) 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added `HTTP` remote request configurable delegate `SetOnRedirect` during redirects 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added `HTTP` remote request file upload and download methods with console progress printing 4.9.9.55 ⏱️2026.08.02 [8bd413e](https://gitee.com/dotnetchina/Furion/commit/8bd413eea24bd4b08a43affe546588ba694f2e17) - Added `HTTP` remote request `Action` operator, supporting implicit conversion of `HttpRequestBuilder` to `Action` 4.9.9.52 ⏱️2026.08.01 [2c3d42d](https://gitee.com/dotnetchina/Furion/commit/2c3d42d5783e041222340e2230502b85bfaa9974) - Added `HTTP` remote request `HttpRequestBuilder.Setup` and `HttpBuilder.Setup` static properties 4.9.9.52 ⏱️2026.08.01 [2c3d42d](https://gitee.com/dotnetchina/Furion/commit/2c3d42d5783e041222340e2230502b85bfaa9974) - Added **`HTTP` remote request `ETag` caching feature** 4.9.9.48 ⏱️2026.07.31 [53c6eb8](https://gitee.com/dotnetchina/Furion/commit/53c6eb8850aff618ee97bbfad8f2b0fd5b5bb34d) - Added **`HTTP` remote request quota policy feature** 4.9.9.46 ⏱️2026.07.29 [22c8edb](https://gitee.com/dotnetchina/Furion/commit/22c8edbe5e249fe10ad011d0dc8982b61278422c) - Added `HTTP` remote request support for `zstd` decompression (`.NET11`) 4.9.9.46 ⏱️2026.07.29 [22c8edb](https://gitee.com/dotnetchina/Furion/commit/22c8edbe5e249fe10ad011d0dc8982b61278422c) - Added `HTTP` remote request `ContentEquals`, `ContentMatches`, `ContentNotEmpty`, and `HeaderNotExists` assertion methods 4.9.9.46 ⏱️2026.07.29 [95f0380](https://gitee.com/dotnetchina/Furion/commit/95f0380bf4be2430aa28ef80405140526828e649) - Added `HTTP` remote request `SSE` and long polling support for returning `IAsyncEnumerable` 4.9.9.43 ⏱️2026.07.26 [5fd05a1](https://gitee.com/dotnetchina/Furion/commit/5fd05a1bacf8bf67883b3bb8000164e41cacee32) - Added `HTTP` remote request `Furion` framework automatic `Token` refresh feature 4.9.9.39 ⏱️2026.07.21 [6611c06](https://gitee.com/dotnetchina/Furion/commit/6611c0686b963add05ba7119cf0a4ffa41b21a9d) - Added `HTTP` remote request `JWT` parsing utility class `JwtTokenUtility` 4.9.9.39 ⏱️2026.07.21 [6611c06](https://gitee.com/dotnetchina/Furion/commit/6611c0686b963add05ba7119cf0a4ffa41b21a9d) - Added `HTTP` remote request support for configuring `IHttpRequestEventHandler` for a specific `HttpClient` 4.9.9.39 ⏱️2026.07.21 [6611c06](https://gitee.com/dotnetchina/Furion/commit/6611c0686b963add05ba7119cf0a4ffa41b21a9d) - Added `HTTP` remote request support for configuring format strings for query parameters, request headers, and `Cookie` 4.9.9.38 ⏱️2026.07.21 [b783f3e](https://gitee.com/dotnetchina/Furion/commit/b783f3e00611d6bd84055fc0a1289bc20ccb7128) - Added `HTTP` remote request retry policy logs by default 4.9.9.38 ⏱️2026.07.21 [b783f3e](https://gitee.com/dotnetchina/Furion/commit/b783f3e00611d6bd84055fc0a1289bc20ccb7128) - Added `HTTP` remote request automatically outputs a warning log when an exception occurs while exception suppression is enabled 4.9.9.38 ⏱️2026.07.21 [b783f3e](https://gitee.com/dotnetchina/Furion/commit/b783f3e00611d6bd84055fc0a1289bc20ccb7128) - Added `HTTP` remote request dependency-free declarative proxy support 4.9.9.37 ⏱️2026.07.20 [f732128](https://gitee.com/dotnetchina/Furion/commit/f7321287388e3998b373d93cf38ce3c6f01a2e9f) - Added `HTTP` remote declarative request support for the `ValueTask` return type 4.9.9.36 ⏱️2026.07.19 [a221418](https://gitee.com/dotnetchina/Furion/commit/a221418fecae702978ce301f3b66dba2d2a17d41) - Added `HTTP` remote request `URL` address support for `{key?}` and `{**key}` path parameters 4.9.9.36 ⏱️2026.07.19 [a221418](https://gitee.com/dotnetchina/Furion/commit/a221418fecae702978ce301f3b66dba2d2a17d41) - Added `HTTP` remote request `HttpRemoteResult` content converter 4.9.9.34 ⏱️2026.07.18 [3ae3f64](https://gitee.com/dotnetchina/Furion/commit/3ae3f64e8f42fd91e21707b6847550190a50f040) - Added `HTTP` remote request support for `RFC 3986`-standard request address concatenation 4.9.9.34 ⏱️2026.07.18 [3ae3f64](https://gitee.com/dotnetchina/Furion/commit/3ae3f64e8f42fd91e21707b6847550190a50f040) - Added `HTTP` remote request `Access Token` automatic management feature 4.9.9.25 [d618a54](https://gitee.com/dotnetchina/Furion/commit/d618a54ed3ef9dc3c42d625e26f6d0360554e157) - Added `HTTP` remote request retry feature 4.9.9.25 [d618a54](https://gitee.com/dotnetchina/Furion/commit/d618a54ed3ef9dc3c42d625e26f6d0360554e157) - Added `HTTP` remote request support for custom pipeline handlers for sending requests 4.9.9.25 [d618a54](https://gitee.com/dotnetchina/Furion/commit/d618a54ed3ef9dc3c42d625e26f6d0360554e157) - Added `HTTP` remote request support for multi-instance service registration 4.9.9.16 [1284ea8](https://gitee.com/dotnetchina/Furion/commit/1284ea80ffa0e4c2c9ca44bb21650d24fd1ac632) - Added `HTTP` remote request content processor support for returning multiple `HttpContent` combinations 4.9.9.16 [1284ea8](https://gitee.com/dotnetchina/Furion/commit/1284ea80ffa0e4c2c9ca44bb21650d24fd1ac632) - Added `HTTP` remote request builder `RemoveContent` method 4.9.9.16 [1284ea8](https://gitee.com/dotnetchina/Furion/commit/1284ea80ffa0e4c2c9ca44bb21650d24fd1ac632) - Added `HTTP` remote request support for bulk adding objects to be disposed when the request completes: `AddDisposables` 4.9.9.16 [1284ea8](https://gitee.com/dotnetchina/Furion/commit/1284ea80ffa0e4c2c9ca44bb21650d24fd1ac632) - Added `HTTP` remote request methods for getting the local `IP` address and `MAC` address: `HttpRemoteUtility.GetLocalIPv4()`, `HttpRemoteUtility.GetLocalMacAddress()` 4.9.9.16 [1284ea8](https://gitee.com/dotnetchina/Furion/commit/1284ea80ffa0e4c2c9ca44bb21650d24fd1ac632) - Added `HTTP` remote request can use `.SetBaseAddress(url)` or `[BaseAddress(url]` as a common prefix feature 4.9.9.13 ⏱️2026.07.05 [0224fa0](https://gitee.com/dotnetchina/Furion/commit/0224fa0a5bdf21ee5f1488b0697b4f12daf95375) - Added `HTTP` remote request support for adding generic response content converters 4.9.9.10 ⏱️2026.07.03 [9a37cfa](https://gitee.com/dotnetchina/Furion/commit/9a37cfa1f89f1decc0b745d3b73302c15e8c35ac) - Added `HTTP` remote request `JSON` response deserialization wrapper `ResultHandler` feature 4.9.9.10 ⏱️2026.07.03 [9a37cfa](https://gitee.com/dotnetchina/Furion/commit/9a37cfa1f89f1decc0b745d3b73302c15e8c35ac) - Added `HTTP` remote request support for sending `IBrowserFile` files 4.9.9.9 ⏱️2026.07.02 [fa73c28](https://gitee.com/dotnetchina/Furion/commit/fa73c28dbfff64d275ec85385983bcec64a7df71) - Added `HTTP` declarative request support for setting headers via `[Header("key: value")]` 4.9.9.9 ⏱️2026.07.02 [fa73c28](https://gitee.com/dotnetchina/Furion/commit/fa73c28dbfff64d275ec85385983bcec64a7df71) - Added `HTTP` remote request `SetContent` method support for configuring the `disposeResourcesOnRequestCompletion` parameter 4.9.9.9 ⏱️2026.07.02 [fa73c28](https://gitee.com/dotnetchina/Furion/commit/fa73c28dbfff64d275ec85385983bcec64a7df71) - Added `HTTP` remote request `IFormFile` content processor support 4.9.9.9 ⏱️2026.07.02 [8a8e44b](https://gitee.com/dotnetchina/Furion/commit/8a8e44b3490a7195911759005b2c5c17d037b20a) - Added `HTTP` remote request feature for saving a stream or byte array to a local file 4.9.8.97 ⏱️2026.06.16 [c8725dc](https://gitee.com/dotnetchina/Furion/commit/c8725dcc890d443aa6e8d0c888b634001bb2b77e) - Added `HTTP` remote request `FileInfo` request content processor 4.9.8.92 ⏱️2026.06.08 [c9e3471](https://gitee.com/dotnetchina/Furion/commit/c9e347114c929ba1070a928ac69d8a284de22485) - Added `HTTP` remote request support for double serialization `JSON` response content processing 4.9.8.66 ⏱️2026.05.15 [54466a9](https://gitee.com/dotnetchina/Furion/commit/54466a91119cfd15581e65fc6048f4a9357857e0) - Added `HTTP` remote request forwarding `HttpContext` option `IgnoreQueryParameters` support 4.9.8.64 ⏱️2026.05.13 [18d1922](https://gitee.com/dotnetchina/Furion/commit/18d1922b157d31dbec2a55302e8f173023559c70) - Added `HTTP` remote request support for setting the request `User-Agent` header via the `SetUserAgent` method 4.9.8.58 ⏱️2026.05.06 [d247eff](https://gitee.com/dotnetchina/Furion/commit/d247effaf9dd7d6526ba5589ecfd6b83e9f28c9d) - Added `HTTP` remote request `UserAgents` static class 4.9.8.58 ⏱️2026.05.06 [d247eff](https://gitee.com/dotnetchina/Furion/commit/d247effaf9dd7d6526ba5589ecfd6b83e9f28c9d) - Added `HTTP` declarative request support for disabling data validation 4.9.8.48 ⏱️2026.04.22 [3979720](https://gitee.com/dotnetchina/Furion/commit/39797204ea4a1a6644790f621f811287175d87f6) - Added `HTTP` remote request `UriBuilder` configuration operation 4.9.8.45 ⏱️2026.04.19 [56be6c6](https://gitee.com/dotnetchina/Furion/commit/56be6c63d7ec079559cddf45531f8232ead19381) - Added `HTTP` declarative request support for object-oriented inheritance 4.9.8.42 ⏱️2026.04.17 [b00f2b9](https://gitee.com/dotnetchina/Furion/commit/b00f2b9a1ebbe8e2005af6e2eb940a3eff48ebc4) - Added `HTTP` remote request support for setting never-timeout 4.9.8.21 ⏱️2026.03.09 [92e0283](https://gitee.com/dotnetchina/Furion/commit/92e0283b8aba9f1cc9eeb9540392855095f2f0b5) - Added `HTTP` remote request support for sending form data without `URL` encoding 4.9.8.15 ⏱️2026.02.09 [f0104ef](https://gitee.com/dotnetchina/Furion/commit/f0104ef8154585b2f3d8faec39def9b4fb3797e2) - Added `HTTP` remote request declarative request support for `Action` frozen parameters 4.9.7.244 ⏱️2026.01.09 [d9fce11](https://gitee.com/dotnetchina/Furion/commit/d9fce115f4bda37d743fe97e7f3b97e7d3eee9f3) - Added `HTTP` remote request support for the `HttpRequestBuilder` unified configurer `IHttpRequestBuilderConfigurer` 4.9.7.244 ⏱️2026.01.09 [d9fce11](https://gitee.com/dotnetchina/Furion/commit/d9fce115f4bda37d743fe97e7f3b97e7d3eee9f3) - Added `HTTP` remote request support for providing `HttpClient` and `HttpRequestMessage` instances for downloading file streams from internet `URL` addresses 4.9.7.235 ⏱️2025.12.27 [a723ae5](https://gitee.com/dotnetchina/Furion/commit/a723ae5cbb93985969c342f7dffb22024e314e5b) - Added `HTTP` remote request setting request headers and `Cookie` support for configuration parameters 4.9.7.231 ⏱️2025.12.19 [541fadd](https://gitee.com/dotnetchina/Furion/commit/541fadd4cebae94fc3686ffb36a337b193a307c5) - Added `HTTP` remote request support for requests with a specified network adapter `IP` address 4.9.7.230 ⏱️2025.12.19 [904705d](https://gitee.com/dotnetchina/Furion/commit/904705d20de53c41eed9d798ccbdb50be13a2408) - Added `HTTP` remote request `HttpBuilder` static class, used to simplify the issue of the overly long `HttpRequestBuilder` name 4.9.7.222 ⏱️2025.12.08 [c0b6c77](https://gitee.com/dotnetchina/Furion/commit/c0b6c77bdc5d4ea83de9891df8dac002a23404ad) - Added `HTTP` remote request analysis log support for color highlighting 4.9.7.217 ⏱️2025.12.03 [29b9348](https://gitee.com/dotnetchina/Furion/commit/29b93485d9d4f14215dcac687e5601321438aa4c) - Added `HTTP` remote request support for setting the `JSON` response deserialization wrapper 4.9.7.214 ⏱️2025.11.26 [f046b4d](https://gitee.com/dotnetchina/Furion/commit/f046b4d423b90a6f6d6aa27cab0f0f671225dff6) [ebe71f9](https://gitee.com/dotnetchina/Furion/commit/ebe71f94f126741153a12e3857ba2536668bd8e6) - Added `HTTP` remote request support for setting a log fallback output delegate when no logging service is configured 4.9.7.213 ⏱️2025.11.26 [17ac155](https://gitee.com/dotnetchina/Furion/commit/17ac155b562c8a18eb2c758777aa4776b2f485eb) - Added `HTTP` remote request support for passing a `JsonSerializerOptions` object when setting `JSON` data 4.9.7.208 ⏱️2025.11.16 [c97b467](https://github.com/monksoul/HttpAgent/commit/c97b467f87a56512d8d196e51c969c05ffa180c0) - Added `HTTP` remote request support for automatically repairing invalid response character encoding 4.9.7.202 ⏱️2025.11.13 [35530e8](https://gitee.com/dotnetchina/Furion/commit/35530e889181beed9672619989a9cbe2edd2c7ca) - Added `HTTP` remote request support for form name naming strategies or custom converters 4.9.7.137 ⏱️2025.11.07 [6c175a8](https://gitee.com/dotnetchina/Furion/commit/6c175a8502806ff434898b19160874592efbdca3) - Added `HTTP` remote request enables `gzip`, `deflate`, `brotli`, and `zstd` automatic decompression of response content by default 4.9.7.137 ⏱️2025.11.07 [e9b10ac](https://gitee.com/dotnetchina/Furion/commit/e9b10ac12128e8b79cf320cf4110e87cc827b5fc) - Added `HTTP` remote request analysis tool `Profiler(enabled)` alias method: `Debugger([enabled])` 4.9.7.137 ⏱️2025.11.07 [c044b87](https://gitee.com/dotnetchina/Furion/commit/c044b87cd1a6243637612872df1dd4a3a8cf4100) - Added `HTTP` remote request support for adding dynamic `URL` parameters (evaluated at request time) 4.9.7.131 ⏱️2025.10.17 [a162c8d](https://gitee.com/dotnetchina/Furion/commit/a162c8dc2e9aca1e27ed397d8d9f9784e636b559) - Added `HTTP` remote request stress testing support for conveniently disabling `HTTP` caching 4.9.7.131 ⏱️2025.10.17 [a162c8d](https://gitee.com/dotnetchina/Furion/commit/a162c8dc2e9aca1e27ed397d8d9f9784e636b559) - Added `HTTP` remote request support for configuring multi-threaded file downloads 4.9.7.123 ⏱️2025.09.16 [10bddc9](https://gitee.com/dotnetchina/Furion/commit/10bddc926cfcc8590bfc7153b67a9f513d9f81b0) - Added `HTTP` remote request support for converting `XML` strings to typed objects 4.9.7.123 ⏱️2025.09.16 [41746d2](https://gitee.com/dotnetchina/Furion/commit/41746d215cffa5eb3ddee2396b3f62be4c658068) - Added `HTTP` remote request builder instance support for `When` conditional building 4.9.7.100 ⏱️2025.07.22 [651b4d5](https://gitee.com/dotnetchina/Furion/commit/651b4d5a5a1467facc5015017026530b05d523f9) - Added `HTTP` remote request extension feature builder `With(Action)` method 4.9.7.95 ⏱️2025.07.10 [4615670](https://gitee.com/dotnetchina/Furion/commit/461567045a1dfced8f4a12b2af028f15b653af21) - Added `HTTP` remote request declarative `[MultipartObject]` attribute 4.9.7.94 ⏱️2025.07.09 [7e52e9c](https://gitee.com/dotnetchina/Furion/commit/7e52e9c5b1fccfb3923d6018b2af0d9ac0c8f438) - Added `HTTP` remote request support for the `Unix epoch` date format 4.9.7.77 ⏱️2025.05.31 [ca9c94e](https://gitee.com/dotnetchina/Furion/commit/ca9c94e2d50cede3edcc3911ad6593c520ac6589) - Added `HTTP` remote request `URL` parameter formatter 4.9.7.70 ⏱️2025.05.23 [e8b24b3](https://gitee.com/dotnetchina/Furion/commit/e8b24b3f5480e6cdca8a56e1cd01a9cd96603bac) - Added `HTTP` remote request support for configuring `SocketsHttpHandler` to ignore `SSL` certificate validation 4.9.7.63 ⏱️2025.05.16 [042da35](https://gitee.com/dotnetchina/Furion/commit/042da3566110ac0c7abd28319d76154a45ff6ced) - Added `HTTP` remote request support for configuring a callback when a request timeout occurs 4.9.7.62 ⏱️2025.05.15 [23a580d](https://gitee.com/dotnetchina/Furion/commit/23a580daff6914a5a186d798c4dbceb6caaad5a7) - Added `HTTP` remote request `HttpRemoteClient` static class 4.9.7.58 ⏱️2025.05.02 [86e9dbe](https://gitee.com/dotnetchina/Furion/commit/86e9dbe197df8efb015e4e22cba4469e715aea27) - Added `HTTP` remote request `HttpRemoteResult` deconstructor (deconstruction expression) feature support 4.9.7.53 ⏱️2025.04.28 [e4dcc10](https://gitee.com/dotnetchina/Furion/commit/e4dcc1028eccd1ffad5471646cb079778ead3ce3) - Added `HTTP` remote request `IHttpClientBuilder.ConfigureOptions(configure)` extension method 4.9.7.51 ⏱️2025.04.26 [33479e2](https://gitee.com/dotnetchina/Furion/commit/33479e212bd09f1867e69e9c428d63b7ece7fa58) - Added `HTTP` remote request analysis tool printing the `HttpClient Name` item 4.9.7.51 ⏱️2025.04.26 [33479e2](https://gitee.com/dotnetchina/Furion/commit/33479e212bd09f1867e69e9c428d63b7ece7fa58) - Added `HTTP` remote request `WithSuccessStatusCodeHandler` method support for setting a callback for successful request status codes 4.9.7.47 ⏱️2025.04.20 [cf7956e](https://gitee.com/dotnetchina/Furion/commit/cf7956e227d978a05c6e0d294766aa45a681f1b9) - Added `HTTP` remote request status code handler support for setting ranges with the `~` symbol, such as `200~299` 4.9.7.47 ⏱️2025.04.20 [cf7956e](https://gitee.com/dotnetchina/Furion/commit/cf7956e227d978a05c6e0d294766aa45a681f1b9) - Added `HTTP` remote request `SetOmitContentType(omit)` method support for removing or keeping the `Content-Type` of request content 4.9.7.44 ⏱️2025.04.17 [4d98d60](https://gitee.com/dotnetchina/Furion/commit/4d98d6053f7b05c73b6d60d5284c040538f18789) - Added `HTTP` remote request support for creating a `HttpRequestBuilder` instance from a `JSON` string 4.9.7.41 ⏱️2025.04.14 [580dd04](https://gitee.com/dotnetchina/Furion/commit/580dd04362d5c6fb5753402838b8029d0793c2a4) - Added `HTTP` remote request support for using `SuppressExceptions()` and `[SuppressExceptions]` to suppress request exceptions 4.9.7.40 ⏱️2025.04.12 [1a9bc7b](https://gitee.com/dotnetchina/Furion/commit/1a9bc7b90472f81cee2fa80ba1459f07484c08f7) - Added `HTTP` remote request support for setting the `HTTP` version of a single request 4.9.7.40 ⏱️2025.04.12 [1a9bc7b](https://gitee.com/dotnetchina/Furion/commit/1a9bc7b90472f81cee2fa80ba1459f07484c08f7) - Added `HTTP` remote request analysis tool printing the `HTTP Version` item 4.9.7.40 ⏱️2025.04.12 [1a9bc7b](https://gitee.com/dotnetchina/Furion/commit/1a9bc7b90472f81cee2fa80ba1459f07484c08f7) - Added `HTTP` remote request `HttpRemoteResult` type `Version` property (`HTTP` version) 4.9.7.40 ⏱️2025.04.12 [1a9bc7b](https://gitee.com/dotnetchina/Furion/commit/1a9bc7b90472f81cee2fa80ba1459f07484c08f7) - Added `HTTP` remote request support for setting the request referrer address 4.9.7.36 ⏱️2025.04.02 [5d4a241](https://gitee.com/dotnetchina/Furion/commit/5d4a241a8dcd63ed2b7fc7a3692b3418c69d3fc5) - Added `HTTP` remote request `HttpRequestBuilder.AddAuthentication(string, string?)` overload method 4.9.7.33 ⏱️2025.03.25 [f8a648a](https://gitee.com/dotnetchina/Furion/commit/f8a648a7377617817ed629da63f1154246eb244f) - Added `HTTP` remote request multipart form `AddFile(IFormFile)` and `AddFiles(IEnumerable)` extension methods 4.9.7.31 ⏱️2025.03.24 [6eb54e0](https://gitee.com/dotnetchina/Furion/commit/6eb54e0f6851158149ca0c48a6604839f07bbf40) - Added `HTTP` remote request support for converting `Number` and `Boolean` types to `String` during deserialization 4.9.7.29 ⏱️2025.03.23 [489aa55](https://gitee.com/dotnetchina/Furion/commit/489aa55fbe05ccd889c2b168f7d012918fdb5e1e) - Added `HTTP` remote request automatically handles garbled Chinese characters during serialization 4.9.7.29 ⏱️2025.03.23 [489aa55](https://gitee.com/dotnetchina/Furion/commit/489aa55fbe05ccd889c2b168f7d012918fdb5e1e) - Added `HTTP` remote request support for non-`ISO 8601-1:2019` standard time strings during `JSON` deserialization 4.9.7.25 ⏱️2025.03.14 [10de94b](https://gitee.com/dotnetchina/Furion/commit/10de94babfcc60cb2837714ce9de5c2284e21ced) [3f3d619](https://gitee.com/dotnetchina/Furion/commit/3f3d61965e432c6597039a630e516f59cad08de4) - Added `HTTP` remote request support for configuring all `HttpClient` clients with `IHttpRemoteBuilder.ConfigureHttpClientDefaults(configure)` 4.9.7.22 ⏱️2025.03.04 [cef4ca0](https://gitee.com/dotnetchina/Furion/commit/cef4ca03a727792663eb2d7b4fa8ad9f196cf3ef) - Added `HTTP` remote request support for `WithPathSegment[s]` to set path segments 4.9.7.21 ⏱️2025.03.03 [7b3335e](https://gitee.com/dotnetchina/Furion/commit/7b3335e8af30509aa1f0465a881693bd3b6f114b) - Added `HTTP` remote request support for enabling the request analysis tool for all `HttpClient` clients with `IHttpRemoteBuilder.AddProfilerDelegatingHandler()` 4.9.7.18 ⏱️2025.03.01 [b6ba52b](https://gitee.com/dotnetchina/Furion/commit/b6ba52bea7f40098a101811c5eb403456139de3c) - Added `HTTP` remote request support for `WebService (SOAP)` 4.9.7.15 ⏱️2025.02.27 [479073a](https://gitee.com/dotnetchina/Furion/commit/479073abf3712bcb9e5566e762289281ea7e6ec1) - Added `HTTP` remote request `AddProfilerDelegatingHandler(this IHttpClientBuilder builder, bool disableInProduction)` overload method 4.9.7.13 ⏱️2025.02.26 [5ef4b13](https://gitee.com/dotnetchina/Furion/commit/5ef4b13c522a824822266dbcf6ad91d8f65e701a) - Added `HTTP` remote request `Server-Sent Events` support for any `HttpMethod` 4.9.7.13 ⏱️2025.02.26 [caa2aca](https://gitee.com/dotnetchina/Furion/commit/caa2acaec7da88d7f9f879ac0fcfe73bd1dc71db) - Added `HTTP` remote request extension method for getting the `Set-Cookie` response header 4.9.7.11 ⏱️2025.02.24 [62737cf](https://gitee.com/dotnetchina/Furion/commit/62737cfccfbc130eaf9bcf8e1ffce15bf690e506) - Added `HTTP` remote request support for setting the request analysis tool trigger delegate 4.9.7.10 ⏱️2025.02.22 [82b4d81](https://gitee.com/dotnetchina/Furion/commit/82b4d81ae60f1918f06cc28b780902f7096c4fa4) - Added `HTTP` remote request `ConfigureOptions` overload method supporting service resolution 4.9.7.9 ⏱️2025.02.20 [dabbc47](https://gitee.com/dotnetchina/Furion/commit/dabbc47d78a1bfab82d367ef359ddf10d94c298d) - Added `HTTP` remote request `HttpRemoteOptions` option `FallbackBaseAddress` property, supporting fallback request base address settings 4.9.7.9 ⏱️2025.02.20 [dabbc47](https://gitee.com/dotnetchina/Furion/commit/dabbc47d78a1bfab82d367ef359ddf10d94c298d) - Added `HTTP` remote request `HttpRemoteResult` type `Server` property 4.9.7.9 ⏱️2025.02.20 [5b1c181](https://gitee.com/dotnetchina/Furion/commit/5b1c18130cb1324ec6ad4b723cd47d101f33a402) - Added `HTTP` remote request `HttpRequestMessage` clone extension method 4.9.7.8 ⏱️2025.02.18 [abd61c8](https://gitee.com/dotnetchina/Furion/commit/abd61c888a7032e64b63943a35765a8d6eb8c46c) - Added `HTTP` remote request `[Forward]` forwarding attribute support 4.9.7 ⏱️2025.01.23 [023166b](https://gitee.com/dotnetchina/Furion/commit/023166b0439e5c43c7f3f58bd88fef3be8f98473) - Added `HTTP` remote request configuration parameter support 4.9.7 ⏱️2025.01.23 [023166b](https://gitee.com/dotnetchina/Furion/commit/023166b0439e5c43c7f3f58bd88fef3be8f98473) - Added `HTTP` remote request forwarding support for ignoring request or response headers 4.9.7 ⏱️2025.01.23 [023166b](https://gitee.com/dotnetchina/Furion/commit/023166b0439e5c43c7f3f58bd88fef3be8f98473) - Added `HTTP` remote request redirect support for relative paths 4.9.6.21 ⏱️2024.12.28 [17df0c4](https://gitee.com/dotnetchina/Furion/commit/17df0c473a7c91d1989e2319109a24d7404e9d65) - Added `HTTP` remote request built-in automatic redirect handling flow 4.9.6.20 ⏱️2024.12.27 [4998e13](https://gitee.com/dotnetchina/Furion/commit/4998e139dec691a154bfbd52463c5fd5e33f6141) - Added `HTTP` remote request `HttpRemoteOptions` option `AllowAutoRedirect` and `MaximumAutomaticRedirections` configuration 4.9.6.20 ⏱️2024.12.27 [4998e13](https://gitee.com/dotnetchina/Furion/commit/4998e139dec691a154bfbd52463c5fd5e33f6141) - Added `HTTP` remote request `WithCookie(cookieHeaderValue)` overload method 4.9.6.18 ⏱️2024.12.25 [80394dc](https://gitee.com/dotnetchina/Furion/commit/80394dceb9d56bfbffd5612d14c350450ff8c93f) - Added `HTTP` remote request support for `HTTP/1.0` and `HTTP/1.1` server interfaces with no configuration by default 4.9.6.16 ⏱️2024.12.17 [61afe9a](https://gitee.com/dotnetchina/Furion/commit/61afe9a28cad036ac51b3a457f865cad36711837) - Added `HTTP` remote request support for setting the request base address feature 4.9.6.15 ⏱️2024.12.10 [187a178](https://gitee.com/dotnetchina/Furion/commit/187a1787cfbc202e69fcd1132a924aad19b3380b) - Added `HTTP` remote request support for preset operations when adding form item content 4.9.6.12 ⏱️2024.12.06 [e610e32](https://gitee.com/dotnetchina/Furion/commit/e610e3233c201eda2397e5f9bc8b3cc7e6ee6375) - Added `HTTP` remote request support for printing request analysis tool content in non-dependency-injection environments 4.9.6.12 ⏱️2024.12.06 [e610e32](https://gitee.com/dotnetchina/Furion/commit/e610e3233c201eda2397e5f9bc8b3cc7e6ee6375) - Added `HTTP` remote request support for declaratively setting `HttpRequestMessage` request property attributes 4.9.6.11 ⏱️2024.12.04 [8306cf0](https://gitee.com/dotnetchina/Furion/commit/8306cf018bac468d091431efd48e0f9d934190ca) - Added `HTTP` remote request support for configuring the delegate to disable the request analysis tool 4.9.6.7 ⏱️2024.12.02 [250ea66](https://gitee.com/dotnetchina/Furion/commit/250ea66c6c9fff98480c79a26e4a5ef629b99153) - Added `HTTP` remote request support for enabling performance optimization 4.9.6.6 ⏱️2024.12.01 [b7ad81b](https://gitee.com/dotnetchina/Furion/commit/b7ad81bd4b575f1cf9f141f581eb3e7027f741af) - Added `HTTP` remote request support for setting the automatic `Host` header 4.9.6.6 ⏱️2024.12.01 [b7ad81b](https://gitee.com/dotnetchina/Furion/commit/b7ad81bd4b575f1cf9f141f581eb3e7027f741af) - Added `HTTP` remote request `DigestCredentials` digest authentication support 4.9.6.5 ⏱️2024.12.01 [3298c02](https://gitee.com/dotnetchina/Furion/commit/3298c027a6df5c400c2885662ce00dc01a185e62) - Added `HTTP` remote request `FileTypeMapper` file `MIME` type mapping class 4.9.6.4 ⏱️2024.11.29 [6782110](https://gitee.com/dotnetchina/Furion/commit/6782110d073a6193c431023b8c40c7ad4fb1129e) - Added `HTTP` remote request support for streams with application rate limiting 4.9.6.3 ⏱️2024.11.28 [f281c32](https://gitee.com/dotnetchina/Furion/commit/f281c32a877dedfd006271768b9054d133a65c29) - Added `HTTP` remote request support for server programs that specifically require `Content-Type` validation 4.9.6.3 ⏱️2024.11.28 [f281c32](https://gitee.com/dotnetchina/Furion/commit/f281c32a877dedfd006271768b9054d133a65c29) - Added `HTTP` remote request support for configuring the request analysis tool log level 4.9.6.3 ⏱️2024.11.28 [f281c32](https://gitee.com/dotnetchina/Furion/commit/f281c32a877dedfd006271768b9054d133a65c29) - Added `HTTP` remote request support for global `HttpRemoteOptions` configuration 4.9.6.2 ⏱️2024.11.28 [b60c996](https://gitee.com/dotnetchina/Furion/commit/b60c99699d8de8000be19077d43a08858f28f874) - Added `HTTP` remote request support for configuring whether query parameters ignore null values `ignoreNullValues` 4.9.6.2 ⏱️2024.11.28 [b60c996](https://gitee.com/dotnetchina/Furion/commit/b60c99699d8de8000be19077d43a08858f28f874) - Added `HTTP` remote request `MultipartFile` add file type 4.9.6.1 ⏱️2024.11.27 [590cd5e](https://gitee.com/dotnetchina/Furion/commit/590cd5e93b15e9573a299710c81b8e4821e749d7) - Added `HTTP` remote request `WithStatusCodeHandler` support for status codes containing comparison symbols 4.9.6.1 ⏱️2024.11.27 [590cd5e](https://gitee.com/dotnetchina/Furion/commit/590cd5e93b15e9573a299710c81b8e4821e749d7) - Added `HTTP` remote request `AddHttpDeclarativeExtractorsFromAssemblies` batch registration of `HTTP` declarative extractors 4.9.6.1 ⏱️2024.11.27 [590cd5e](https://gitee.com/dotnetchina/Furion/commit/590cd5e93b15e9573a299710c81b8e4821e749d7) - Added `HTTP` remote requests support `Mock` simulation testing 4.9.9.74 ⏱️2026.08.13 [320c865](https://gitee.com/dotnetchina/Furion/commit/320c86569d5d796e3d42093f4c57e121e1800f4f) - Added `HTTP` remote requests support a custom `Logger` 4.9.9.74 ⏱️2026.08.13 [320c865](https://gitee.com/dotnetchina/Furion/commit/320c86569d5d796e3d42093f4c57e121e1800f4f) - Added `HTTP` remote requests support directly sending file content and binary stream content 4.9.9.68 ⏱️2026.08.09 [63735d6](https://gitee.com/dotnetchina/Furion/commit/63735d64030c725902a3b7b89352fe14574047b8) - Added `HTTP` remote requests support enabling standard request headers 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Added `HTTP` remote requests include a built-in `Access Token` provider for the WeChat development platform 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added `HTTP` remote requests support retrieving raw message lines when sending `Server-Sent Events` 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added The `HTTP` remote request `HttpRemoteClient` static class supports using an external service container (resolving the issue that static classes cannot apply external service configuration, and the problem of upgrading legacy string extension requests) 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added `HTTP` remote requests provide the abstract base class `HttpRequestBuilderConfigurator` for pre-configuring `HttpRequestBuilder` 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added In `HTTP` remote requests, the `HttpRequestBuilder` and `HttpFileUploadBuilder` builders support appending multipart form content (`WithMultipart(u=>{})`) 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added In `HTTP` remote requests, `HttpRequestBuilder` supports setting the `SOAPAction` method (for `WebService`) 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added `HTTP` remote requests support automatically correcting `GET` and `HEAD` requests that carry request content when sending `Server-Sent Events` (automatically converted to `POST`) 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added The `UseHttpRemoteClient(serviceProvider)` extension methods for `HTTP` remote requests support presetting the `HttpRemoteClient` static class to use an external service container 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added `HTTP` remote requests support `RFC 2047`, `RFC 5987`, and `Latin-1` (`Mojibake`) encoded filename parsing when downloading files 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added Helper methods related to sending `Server-Sent Events` in `HTTP` remote requests (setting response headers and streaming output) 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added `HTTP` remote requests support configuring the `SetOnRedirect` delegate on redirect 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added `HTTP` remote request upload and download file methods with console progress printing 4.9.9.55 ⏱️2026.08.02 [8bd413e](https://gitee.com/dotnetchina/Furion/commit/8bd413eea24bd4b08a43affe546588ba694f2e17) - Added `HTTP` remote request automatic `Token` refresh for the `Furion` framework 4.9.9.39 ⏱️2026.07.21 [6611c06](https://gitee.com/dotnetchina/Furion/commit/6611c0686b963add05ba7119cf0a4ffa41b21a9d) - Added `HTTP` remote requests support configuring `IHttpRequestEventHandler` for a specific `HttpClient` 4.9.9.39 ⏱️2026.07.21 [6611c06](https://gitee.com/dotnetchina/Furion/commit/6611c0686b963add05ba7119cf0a4ffa41b21a9d) - Added `HTTP` remote requests automatically output warning logs when an exception occurs while exception suppression is enabled 4.9.9.38 ⏱️2026.07.21 [b783f3e](https://gitee.com/dotnetchina/Furion/commit/b783f3e00611d6bd84055fc0a1289bc20ccb7128) - Added `HTTP` remote declarative requests support the `ValueTask` return value type 4.9.9.36 ⏱️2026.07.19 [a221418](https://gitee.com/dotnetchina/Furion/commit/a221418fecae702978ce301f3b66dba2d2a17d41) - Added `HTTP` remote requests support `RFC 3986`-standard request address concatenation 4.9.9.34 ⏱️2026.07.18 [3ae3f64](https://gitee.com/dotnetchina/Furion/commit/3ae3f64e8f42fd91e21707b6847550190a50f040) - Added `HTTP` remote requests support custom pipeline handlers for sending requests 4.9.9.25 [d618a54](https://gitee.com/dotnetchina/Furion/commit/d618a54ed3ef9dc3c42d625e26f6d0360554e157) - Added `HTTP` remote requests support batch-adding objects to be disposed when the request completes via `AddDisposables` 4.9.9.16 [1284ea8](https://gitee.com/dotnetchina/Furion/commit/1284ea80ffa0e4c2c9ca44bb21650d24fd1ac632) - Added `HTTP` remote requests can act as a common prefix via `.SetBaseAddress(url)` or `[BaseAddress(url]` 4.9.9.13 ⏱️2026.07.05 [0224fa0](https://gitee.com/dotnetchina/Furion/commit/0224fa0a5bdf21ee5f1488b0697b4f12daf95375) - Added `HTTP` declarative requests support setting headers via `[Header("key: value")]` 4.9.9.9 ⏱️2026.07.02 [fa73c28](https://gitee.com/dotnetchina/Furion/commit/fa73c28dbfff64d275ec85385983bcec64a7df71) - Added `HTTP` remote request `SetContent` method supports configuring the `disposeResourcesOnRequestCompletion` parameter 4.9.9.9 ⏱️2026.07.02 [fa73c28](https://gitee.com/dotnetchina/Furion/commit/fa73c28dbfff64d275ec85385983bcec64a7df71) - Added `HTTP` remote request feature to save streams or byte arrays to a local file 4.9.8.97 ⏱️2026.06.16 [c8725dc](https://gitee.com/dotnetchina/Furion/commit/c8725dcc890d443aa6e8d0c888b634001bb2b77e) - Added `HTTP` remote requests support double serialization `JSON` response content processing 4.9.8.66 ⏱️2026.05.15 [54466a9](https://gitee.com/dotnetchina/Furion/commit/54466a91119cfd15581e65fc6048f4a9357857e0) - Added `HTTP` remote request `HttpContext` forwarding option `IgnoreQueryParameters` support 4.9.8.64 ⏱️2026.05.13 [18d1922](https://gitee.com/dotnetchina/Furion/commit/18d1922b157d31dbec2a55302e8f173023559c70) - Added `HTTP` remote requests support query parameter sorting and form field submission sorting 4.9.8.58 ⏱️2026.05.06 [d247eff](https://gitee.com/dotnetchina/Furion/commit/d247effaf9dd7d6526ba5589ecfd6b83e9f28c9d) - Added `HTTP` remote request `UriBuilder` configuration operations 4.9.8.45 ⏱️2026.04.19 [56be6c6](https://gitee.com/dotnetchina/Furion/commit/56be6c63d7ec079559cddf45531f8232ead19381) - Added `HTTP` remote requests support setting a never-timeout 4.9.8.21 ⏱️2026.03.09 [92e0283](https://gitee.com/dotnetchina/Furion/commit/92e0283b8aba9f1cc9eeb9540392855095f2f0b5) - Added `HTTP` remote request declarative requests support `Action` frozen parameters 4.9.7.244 ⏱️2026.01.09 [d9fce11](https://gitee.com/dotnetchina/Furion/commit/d9fce115f4bda37d743fe97e7f3b97e7d3eee9f3) - Added `HTTP` remote requests support the `HttpRequestBuilder` unified configurator `IHttpRequestBuilderConfigurer` 4.9.7.244 ⏱️2026.01.09 [d9fce11](https://gitee.com/dotnetchina/Furion/commit/d9fce115f4bda37d743fe97e7f3b97e7d3eee9f3) - Added `HTTP` remote requests support providing `HttpClient` and `HttpRequestMessage` instances for downloading file streams from an internet `URL` address 4.9.7.235 ⏱️2025.12.27 [a723ae5](https://gitee.com/dotnetchina/Furion/commit/a723ae5cbb93985969c342f7dffb22024e314e5b) - Added `HTTP` remote request setting of request headers and `Cookie` supports configuration parameters 4.9.7.231 ⏱️2025.12.19 [541fadd](https://gitee.com/dotnetchina/Furion/commit/541fadd4cebae94fc3686ffb36a337b193a307c5) - Added `HTTP` remote requests support requesting via a specified network interface card `IP` address 4.9.7.230 ⏱️2025.12.19 [904705d](https://gitee.com/dotnetchina/Furion/commit/904705d20de53c41eed9d798ccbdb50be13a2408) - Added `HTTP` remote request `HttpBuilder` static class, used to simplify the overly long `HttpRequestBuilder` name 4.9.7.222 ⏱️2025.12.08 [c0b6c77](https://gitee.com/dotnetchina/Furion/commit/c0b6c77bdc5d4ea83de9891df8dac002a23404ad) - Added `HTTP` remote request Profiler logs support color highlighting 4.9.7.217 ⏱️2025.12.03 [29b9348](https://gitee.com/dotnetchina/Furion/commit/29b93485d9d4f14215dcac687e5601321438aa4c) - Added `HTTP` remote requests support setting a `JSON` response deserialization wrapper 4.9.7.214 ⏱️2025.11.26 [f046b4d](https://gitee.com/dotnetchina/Furion/commit/f046b4d423b90a6f6d6aa27cab0f0f671225dff6) [ebe71f9](https://gitee.com/dotnetchina/Furion/commit/ebe71f94f126741153a12e3857ba2536668bd8e6) - Added `HTTP` remote requests support automatically repairing invalid response character encoding 4.9.7.202 ⏱️2025.11.13 [35530e8](https://gitee.com/dotnetchina/Furion/commit/35530e889181beed9672619989a9cbe2edd2c7ca) - Added `HTTP` remote request Profiler `Profiler(enabled)` alias method: `Debugger([enabled])` 4.9.7.137 ⏱️2025.11.07 [c044b87](https://gitee.com/dotnetchina/Furion/commit/c044b87cd1a6243637612872df1dd4a3a8cf4100) - Added `HTTP` remote requests support converting `XML` strings into typed objects 4.9.7.123 ⏱️2025.09.16 [41746d2](https://gitee.com/dotnetchina/Furion/commit/41746d215cffa5eb3ddee2396b3f62be4c658068) - Added `HTTP` remote request builder instances support `When` conditional building 4.9.7.100 ⏱️2025.07.22 [651b4d5](https://gitee.com/dotnetchina/Furion/commit/651b4d5a5a1467facc5015017026530b05d523f9) - Added `HTTP` remote requests support configuring a callback action when a request timeout occurs 4.9.7.62 ⏱️2025.05.15 [23a580d](https://gitee.com/dotnetchina/Furion/commit/23a580daff6914a5a186d798c4dbceb6caaad5a7) - Added `HTTP` remote request Profiler prints the `HttpClient Name` item 4.9.7.51 ⏱️2025.04.26 [33479e2](https://gitee.com/dotnetchina/Furion/commit/33479e212bd09f1867e69e9c428d63b7ece7fa58) - Added `HTTP` remote request `WithSuccessStatusCodeHandler` method supports setting a callback action for successful request status codes 4.9.7.47 ⏱️2025.04.20 [cf7956e](https://gitee.com/dotnetchina/Furion/commit/cf7956e227d978a05c6e0d294766aa45a681f1b9) - Added `HTTP` remote request status code handlers support setting ranges with the `~` symbol, e.g. `200~299` 4.9.7.47 ⏱️2025.04.20 [cf7956e](https://gitee.com/dotnetchina/Furion/commit/cf7956e227d978a05c6e0d294766aa45a681f1b9) - Added `HTTP` remote request `SetOmitContentType(omit)` method supports removing or keeping the request content `Content-Type` 4.9.7.44 ⏱️2025.04.17 [4d98d60](https://gitee.com/dotnetchina/Furion/commit/4d98d6053f7b05c73b6d60d5284c040538f18789) - Added `HTTP` remote requests support setting the `HTTP` version for a single request 4.9.7.40 ⏱️2025.04.12 [1a9bc7b](https://gitee.com/dotnetchina/Furion/commit/1a9bc7b90472f81cee2fa80ba1459f07484c08f7) - Added `HTTP` remote request Profiler prints the `HTTP Version` item 4.9.7.40 ⏱️2025.04.12 [1a9bc7b](https://gitee.com/dotnetchina/Furion/commit/1a9bc7b90472f81cee2fa80ba1459f07484c08f7) - Added `HTTP` remote requests automatically handle Chinese mojibake issues during serialization 4.9.7.29 ⏱️2025.03.23 [489aa55](https://gitee.com/dotnetchina/Furion/commit/489aa55fbe05ccd889c2b168f7d012918fdb5e1e) - Added `HTTP` remote requests support non-`ISO 8601-1:2019` time strings during `JSON` deserialization 4.9.7.25 ⏱️2025.03.14 [10de94b](https://gitee.com/dotnetchina/Furion/commit/10de94babfcc60cb2837714ce9de5c2284e21ced) [3f3d619](https://gitee.com/dotnetchina/Furion/commit/3f3d61965e432c6597039a630e516f59cad08de4) - Added `HTTP` remote requests support enabling the Profiler for all `HttpClient` clients via `IHttpRemoteBuilder.AddProfilerDelegatingHandler()` 4.9.7.18 ⏱️2025.03.01 [b6ba52b](https://gitee.com/dotnetchina/Furion/commit/b6ba52bea7f40098a101811c5eb403456139de3c) - Added `HTTP` remote request `Server-Sent Events` support any `HttpMethod` 4.9.7.13 ⏱️2025.02.26 [caa2aca](https://gitee.com/dotnetchina/Furion/commit/caa2acaec7da88d7f9f879ac0fcfe73bd1dc71db) - Added `HTTP` remote requests support setting the Profiler trigger delegate 4.9.7.10 ⏱️2025.02.22 [82b4d81](https://gitee.com/dotnetchina/Furion/commit/82b4d81ae60f1918f06cc28b780902f7096c4fa4) - Added `HTTP` remote request `ConfigureOptions` overload method that supports resolving services 4.9.7.9 ⏱️2025.02.20 [dabbc47](https://gitee.com/dotnetchina/Furion/commit/dabbc47d78a1bfab82d367ef359ddf10d94c298d) - Added `HTTP` remote request configuration parameters support 4.9.7 ⏱️2025.01.23 [023166b](https://gitee.com/dotnetchina/Furion/commit/023166b0439e5c43c7f3f58bd88fef3be8f98473) - Added `HTTP` remote request redirect supports relative paths 4.9.6.21 ⏱️2024.12.28 [17df0c4](https://gitee.com/dotnetchina/Furion/commit/17df0c473a7c91d1989e2319109a24d7404e9d65) - Added `HTTP` remote requests have a built-in automatic redirect handling pipeline 4.9.6.20 ⏱️2024.12.27 [4998e13](https://gitee.com/dotnetchina/Furion/commit/4998e139dec691a154bfbd52463c5fd5e33f6141) - Added `HTTP` remote request `HttpRemoteOptions` options `AllowAutoRedirect` and `MaximumAutomaticRedirections` configuration 4.9.6.20 ⏱️2024.12.27 [4998e13](https://gitee.com/dotnetchina/Furion/commit/4998e139dec691a154bfbd52463c5fd5e33f6141) - Added `HTTP` remote requests support `HTTP/1.0` and `HTTP/1.1` server endpoints by default with no configuration 4.9.6.16 ⏱️2024.12.17 [61afe9a](https://gitee.com/dotnetchina/Furion/commit/61afe9a28cad036ac51b3a457f865cad36711837) - Added `HTTP` remote requests support printing Profiler content in non-dependency-injection environments 4.9.6.12 ⏱️2024.12.06 [e610e32](https://gitee.com/dotnetchina/Furion/commit/e610e3233c201eda2397e5f9bc8b3cc7e6ee6375) - Added `HTTP` remote requests support configuring a delegate to disable the Profiler 4.9.6.7 ⏱️2024.12.02 [250ea66](https://gitee.com/dotnetchina/Furion/commit/250ea66c6c9fff98480c79a26e4a5ef629b99153) - Added `HTTP` remote requests support server programs that specifically require validating `Content-Type` 4.9.6.3 ⏱️2024.11.28 [f281c32](https://gitee.com/dotnetchina/Furion/commit/f281c32a877dedfd006271768b9054d133a65c29) - Added `HTTP` remote requests support configuring the Profiler log level 4.9.6.3 ⏱️2024.11.28 [f281c32](https://gitee.com/dotnetchina/Furion/commit/f281c32a877dedfd006271768b9054d133a65c29) - Added `HTTP` remote requests support configuring whether query parameters ignore null values via `ignoreNullValues` 4.9.6.2 ⏱️2024.11.28 [b60c996](https://gitee.com/dotnetchina/Furion/commit/b60c99699d8de8000be19077d43a08858f28f874) - Added `HTTP` remote request `MultipartFile` added file type 4.9.6.1 ⏱️2024.11.27 [590cd5e](https://gitee.com/dotnetchina/Furion/commit/590cd5e93b15e9573a299710c81b8e4821e749d7) - Added `HTTP` remote request `WithStatusCodeHandler` supports comparison-symbol-style status codes 4.9.6.1 ⏱️2024.11.27 [590cd5e](https://gitee.com/dotnetchina/Furion/commit/590cd5e93b15e9573a299710c81b8e4821e749d7) - Added `HTTP` remote request `AddHttpDeclarativeExtractorsFromAssemblies` batch-registers `HTTP` declarative extractors 4.9.6.1 ⏱️2024.11.27 [590cd5e](https://gitee.com/dotnetchina/Furion/commit/590cd5e93b15e9573a299710c81b8e4821e749d7) - **Breaking Changes** - Changed the `HTTP` remote declarative request context name from `HttpDeclarativeExtractorContext` to `HttpDeclarativeParsingContext` 4.9.9.70 ⏱️2026.08.11 [cfbc6917](https://gitee.com/dotnetchina/Furion/commit/cfbc69174eea0621cedc262e6de8b485b00a579f) - Changed the default `CompletionOption` option for `HTTP` remote request `HttpContext` forwarding to `ResponseHeadersRead` 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Changed the method name for setting `Bearer` authorization in `HTTP` remote requests: `AddJwtBearerAuthentication` → `AddBearerAuthentication` 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Changed the method name for setting the `URL` builder in `HTTP` remote requests: `SetUriBuilder` → `SetOnUriBuilding` 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Changed the `OnPostReceiveResponse` event method of `HTTP` remote requests to an async method (`OnPostReceiveResponseAsync`) 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Changed the `WebSocket` client events of `HTTP` remote requests to async events 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Changed the binary content size limit printed by the `HTTP` remote request analysis tool from `1KB` to `0.5KB` to avoid flooding the console output 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Changed the `WithRequest` method name to `With` in `HTTP` remote requests 4.9.9.52 ⏱️2026.08.01 [2c3d42d](https://gitee.com/dotnetchina/Furion/commit/2c3d42d5783e041222340e2230502b85bfaa9974) - Changed the `IUrlParameterFormatter` interface method for `URL` parameter formatters in `HTTP` remote requests 4.9.9.40 ⏱️2026.07.23 [c380d55](https://gitee.com/dotnetchina/Furion/commit/c380d552df469597c33f4e2a6b9b085079fef3c0) - Changed the `URL` parameter sorting delegate signature in `HTTP` remote requests 4.9.9.40 ⏱️2026.07.23 [c380d55](https://gitee.com/dotnetchina/Furion/commit/c380d552df469597c33f4e2a6b9b085079fef3c0) - Changed the content converter method parameter design in `HTTP` remote requests 4.9.9.34 ⏱️2026.07.18 [3ae3f64](https://gitee.com/dotnetchina/Furion/commit/3ae3f64e8f42fd91e21707b6847550190a50f040) - Added **`HTTP` remote request `QUERY` request support** 4.9.9.8 ⏱️2026.07.01 [1195ffa](https://gitee.com/dotnetchina/Furion/commit/1195ffa77796a1a334dffda01f217036ba08e1a0) - Changed **`HTTP` remote request `[Query]` renamed to `[QueryParam]`** 4.9.9.8 ⏱️2026.07.01 [b9df71b](https://gitee.com/dotnetchina/Furion/commit/b9df71b692bfbefb3ff2897ef08a103150e2bd23) ## View changes — [Query] renamed to [QueryParam] Yesterday, the IETF (Internet Engineering Task Force) officially released `RFC 10008`, adding a new member to the `HTTP` protocol family — the `QUERY` method. This is an `HTTP` verb that is as safe and idempotent as `GET`, but supports carrying request content. https://www.rfc-editor.org/info/rfc10008 The framework has already provided adaptation support at the earliest opportunity. **Notably, the original `[Query]` attribute has been renamed to `[QueryParam]`, and `[Query]` is now used as the HTTP method (request verb).** - - Changed **the `HTTP` remote request content processor `IHttpContentProcessor` interface method signature** 4.9.8.90 ⏱️2026.06.05 [8c5df99](https://gitee.com/dotnetchina/Furion/commit/8c5df990b842b001ab111f27109d7d7608df0aa9) - - Changed the content converter factory of `HTTP` remote request objects (**including interface signature changes**), improving extension flexibility and code maintainability 4.9.7.129 ⏱️2025.10.09 [cf83a79](https://gitee.com/dotnetchina/Furion/commit/cf83a790821c0209031c0fc3a491ac9061c32efd) - - Changed the `HTTP` remote request extension feature interface method signature 4.9.7.95 ⏱️2025.07.10 [4615670](https://gitee.com/dotnetchina/Furion/commit/461567045a1dfced8f4a12b2af028f15b653af21) - - Changed the `[Version]` declarative attribute name for setting the `HTTP` version in `HTTP` remote requests to `[HttpVersion]` 4.9.7.41 ⏱️2025.04.14 [b054693](https://gitee.com/dotnetchina/Furion/commit/b05469379da93f81e61cc1fe7216f2300be7d742) - Adjusted `HTTP` remote request declarative request context name `HttpDeclarativeExtractorContext` -> `HttpDeclarativeParsingContext` 4.9.9.70 ⏱️2026.08.11 [cfbc6917](https://gitee.com/dotnetchina/Furion/commit/cfbc69174eea0621cedc262e6de8b485b00a579f) - Adjusted `HTTP` remote request `HttpContext` forwarding default `CompletionOption` option is now `ResponseHeadersRead` 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Adjusted `HTTP` remote request method name for setting `Bearer` authorization: `AddJwtBearerAuthentication` → `AddBearerAuthentication` 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Adjusted `HTTP` remote request method name for setting the `URL` builder: `SetUriBuilder` → `SetOnUriBuilding` 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Adjusted `HTTP` remote request `OnPostReceiveResponse` event method changed to an asynchronous method (`OnPostReceiveResponseAsync`) 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Adjusted `HTTP` remote request `WebSocket` client events are now asynchronous events 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Adjusted `HTTP` remote request analysis tool size limit for printing binary content: `1KB` → `0.5KB`, to avoid flooding the console output 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Adjusted `HTTP` remote request `WithRequest` method renamed to `With` 4.9.9.52 ⏱️2026.08.01 [2c3d42d](https://gitee.com/dotnetchina/Furion/commit/2c3d42d5783e041222340e2230502b85bfaa9974) - Adjusted `HTTP` remote request `URL` parameter formatter `IUrlParameterFormatter` interface methods 4.9.9.40 ⏱️2026.07.23 [c380d55](https://gitee.com/dotnetchina/Furion/commit/c380d552df469597c33f4e2a6b9b085079fef3c0) - Adjusted `HTTP` remote request `URL` parameter sorting delegate signature 4.9.9.40 ⏱️2026.07.23 [c380d55](https://gitee.com/dotnetchina/Furion/commit/c380d552df469597c33f4e2a6b9b085079fef3c0) - Adjusted `HTTP` remote request content converter method parameter design 4.9.9.34 ⏱️2026.07.18 [3ae3f64](https://gitee.com/dotnetchina/Furion/commit/3ae3f64e8f42fd91e21707b6847550190a50f040) - Adjusted **`HTTP` remote request `[Query]` renamed to `[QueryParam]`** 4.9.9.8 ⏱️2026.07.01 [b9df71b](https://gitee.com/dotnetchina/Furion/commit/b9df71b692bfbefb3ff2897ef08a103150e2bd23) Yesterday, the IETF (Internet Engineering Task Force) officially released `RFC 10008`, adding a new member to the `HTTP` protocol family — the `QUERY` method. It is an `HTTP` verb that is as safe and idempotent as `GET`, but supports carrying request content. The framework has already provided support for it at the earliest opportunity. **Note that the original `[Query]` attribute has been renamed to `[QueryParam]`; `[Query]` is now used as an HTTP method (request verb).** - - Adjusted **`HTTP` remote request content processor `IHttpContentProcessor` interface method signature** 4.9.8.90 ⏱️2026.06.05 [8c5df99](https://gitee.com/dotnetchina/Furion/commit/8c5df990b842b001ab111f27109d7d7608df0aa9) - - Adjusted `HTTP` remote request object content converter factory (**including interface signature changes**), improving extensibility and code maintainability 4.9.7.129 ⏱️2025.10.09 [cf83a79](https://gitee.com/dotnetchina/Furion/commit/cf83a790821c0209031c0fc3a491ac9061c32efd) - - Adjusted `HTTP` remote request extension feature interface method signatures 4.9.7.95 ⏱️2025.07.10 [4615670](https://gitee.com/dotnetchina/Furion/commit/461567045a1dfced8f4a12b2af028f15b653af21) - - Adjusted `HTTP` remote request declarative attribute for setting the `HTTP` version: the `[Version]` name is adjusted to `[HttpVersion]` 4.9.7.41 ⏱️2025.04.14 [b054693](https://gitee.com/dotnetchina/Furion/commit/b05469379da93f81e61cc1fe7216f2300be7d742) - Adjusted `HTTP` remote request `OnPostReceiveResponse` event method changed to an async method (`OnPostReceiveResponseAsync`) 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Adjusted `HTTP` remote request `WebSocket` client events changed to async events 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Adjusted `HTTP` remote request Profiler binary content printing size limit: `1KB` → `0.5KB`, to avoid flooding the console output 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Added **`HTTP` remote request `QUERY` method support** 4.9.9.8 ⏱️2026.07.01 [1195ffa](https://gitee.com/dotnetchina/Furion/commit/1195ffa77796a1a334dffda01f217036ba08e1a0) Yesterday, the IETF (Internet Engineering Task Force) officially released `RFC 10008`, adding a new member to the `HTTP` protocol family — the `QUERY` method. This is an `HTTP` verb that is just as safe and idempotent as `GET`, but supports carrying request content. **It is worth noting that the original `[Query]` attribute has been renamed to `[QueryParam]`; the current `[Query]` is now used as an HTTP method (request verb).** - - Adjusted The content converter factory for `HTTP` remote request objects (**including interface signature changes**), improving extension flexibility and code maintainability 4.9.7.129 ⏱️2025.10.09 [cf83a79](https://gitee.com/dotnetchina/Furion/commit/cf83a790821c0209031c0fc3a491ac9061c32efd) - - Adjusted The `HTTP` remote request declarative attribute `[Version]` for setting the `HTTP` version renamed to `[HttpVersion]` 4.9.7.41 ⏱️2025.04.14 [b054693](https://gitee.com/dotnetchina/Furion/commit/b05469379da93f81e61cc1fe7216f2300be7d742) - **Bug Fixes** - Fixed Fixed the issue where `HTTP` remote request `ETag` caching did not respect `Cache-Control: no-store/private` 4.9.9.82 ⏱️2026.08.18 [1de84bb](https://gitee.com/dotnetchina/Furion/commit/1de84bb5ec90162b83a36fc9e33c9bb395b89365) - Fixed Fixed the issue where reading a response after `HTTP` remote request `ETag` caching made the content unable to be read repeatedly 4.9.9.82 ⏱️2026.08.18 [1de84bb](https://gitee.com/dotnetchina/Furion/commit/1de84bb5ec90162b83a36fc9e33c9bb395b89365) - Fixed Fixed the missing data integrity verification issue in `HTTP` remote request multi-threaded chunked downloads 4.9.9.82 ⏱️2026.08.18 [1de84bb](https://gitee.com/dotnetchina/Furion/commit/1de84bb5ec90162b83a36fc9e33c9bb395b89365) - Fixed Fixed the resource leak and missing backpressure issue in `HTTP` remote request long-polling channels 4.9.9.82 ⏱️2026.08.18 [1de84bb](https://gitee.com/dotnetchina/Furion/commit/1de84bb5ec90162b83a36fc9e33c9bb395b89365) - Fixed Fixed the issue where the `OnTimeout` callback was incorrectly triggered when the `HTTP` remote request timeout handler cancelled proactively 4.9.9.82 ⏱️2026.08.18 [1de84bb](https://gitee.com/dotnetchina/Furion/commit/1de84bb5ec90162b83a36fc9e33c9bb395b89365) - Fixed the issue where request headers could not be automatically categorized when setting request headers in `HTTP` remote requests, such as being unable to identify which are request content headers 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Fixed the issue where request content type could not be automatically inferred from request headers in `HTTP` remote requests 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Fixed the issue where `Basic` authentication with an empty password was not supported in `HTTP` remote requests 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Fixed the issue where the `URL`-encoded form processor did not support string content in `HTTP` remote requests 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Fixed the issue where the `Content-Length` header was not excluded when forwarding `HttpContext` in `HTTP` remote requests, causing forwarding errors 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Fixed the issue where automatic decompression did not occur when forwarding `HttpContext` to `IActionResult` results in `HTTP` remote requests 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Fixed the issue where the `HTTP` remote request file downloader failed downloads due to network fluctuations (additionally adding log tracking output) 4.9.9.66 ⏱️2026.08.08 [152fa8e](https://gitee.com/dotnetchina/Furion/commit/152fa8e40207a53f84980830a7a55685c839201a) - Fixed the occasional deadlock issue after configuring download/upload thread counts in `HTTP` remote requests 4.9.9.65 ⏱️2026.08.07 [632ad1e](https://gitee.com/dotnetchina/Furion/commit/632ad1e13a294d390136f7853a65ce8502900759) - Fixed the blank page (no output) issue when forwarding websites via `HttpContext` in `HTTP` remote requests 4.9.9.65 ⏱️2026.08.07 [632ad1e](https://gitee.com/dotnetchina/Furion/commit/632ad1e13a294d390136f7853a65ce8502900759) - Fixed the memory overflow issue in long polling and `SSE` scenarios in `HTTP` remote requests 4.9.9.65 ⏱️2026.08.07 [632ad1e](https://gitee.com/dotnetchina/Furion/commit/632ad1e13a294d390136f7853a65ce8502900759) - Fixed the calculation bias issue in `HTTP` remote request stress testing results 4.9.9.65 ⏱️2026.08.07 [632ad1e](https://gitee.com/dotnetchina/Furion/commit/632ad1e13a294d390136f7853a65ce8502900759) - Fixed the issue where the automatic refresh configuration of the built-in `Furion` and `WeChat` `Access Token` managers failed in `HTTP` remote requests 4.9.9.65 ⏱️2026.08.07 [632ad1e](https://gitee.com/dotnetchina/Furion/commit/632ad1e13a294d390136f7853a65ce8502900759) - Fixed the issue where the built-in `Furion` framework `Access Token` provider did not refresh after expiration in `HTTP` remote requests 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Fixed the issue where the assertion context prevented external re-reading after reading response content in `HTTP` remote requests 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Fixed the multicast delegate handling error (synchronous and asynchronous) in `HTTP` remote requests 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Fixed the issue where, when appending parameters, configuring `replace: true` still failed to replace the original `URL` address in `HTTP` remote requests 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Fixed the issue where unnecessary original parameters were carried during redirection, causing request failures in `HTTP` remote requests 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Fixed the console progress bar disorder when downloading multiple files simultaneously in `HTTP` remote requests 4.9.9.59 ⏱️2026.08.03 [ecc25f9](https://gitee.com/dotnetchina/Furion/commit/ecc25f946d27c226234798277cca1cd56a5d8ca0) [029e51a](https://gitee.com/dotnetchina/Furion/commit/029e51aa27128bf6a22b66770a4fb1039cb1aa9c) - Fixed the issue where `File-Based Apps` application types were not supported in `HTTP` remote requests 4.9.9.53 ⏱️2026.08.02 [aae2318](https://gitee.com/dotnetchina/Furion/commit/aae23182e161e84419fa854b0c68e8b722056167) - Fixed the `Boundary` parsing error of content type when forwarding forms in `HTTP` remote requests 4.9.9.50 ⏱️2026.07.31 [8a93868](https://gitee.com/dotnetchina/Furion/commit/8a93868afedaf56f879fcfdd256e90540ebb556d) - Fixed the error handling format and algorithm issues of `Digest` digest authentication in `HTTP` remote requests 4.9.9.47 ⏱️2026.07.30 [312767f](https://gitee.com/dotnetchina/Furion/commit/312767f1a51f4c681fd6f8d6957d0ddaf9bd4885) - Fixed the stream-disposed exception when the `HTTP` remote request analysis log prints response content of unknown size exceeding `5MB` 4.9.9.46 ⏱️2026.07.29 [22c8edb](https://gitee.com/dotnetchina/Furion/commit/22c8edbe5e249fe10ad011d0dc8982b61278422c) - Fixed the data truncation issue when the `WebSocket` client receives large messages in `HTTP` remote requests 4.9.9.46 ⏱️2026.07.29 [fc1634d](https://gitee.com/dotnetchina/Furion/commit/fc1634d52182db315f7a2c38c9f1f33f25be100e) - Fixed the deadlock issue when the `HTTP` remote request analysis log prints `SSE` or streaming response content 4.9.9.43 ⏱️2026.07.26 [5fd05a1](https://gitee.com/dotnetchina/Furion/commit/5fd05a1bacf8bf67883b3bb8000164e41cacee32) [09cb4f1](https://gitee.com/dotnetchina/Furion/commit/09cb4f130ccacd7c027cf06e752716013e19697c) - Fixed the data error handling issue in `SSE` and long polling in `HTTP` remote requests 4.9.9.43 ⏱️2026.07.26 [5fd05a1](https://gitee.com/dotnetchina/Furion/commit/5fd05a1bacf8bf67883b3bb8000164e41cacee32) - Fixed the issue where the `WebSocket` client did not release event resources after disposal in `HTTP` remote requests 4.9.9.34 [3ae3f64](https://gitee.com/dotnetchina/Furion/commit/3ae3f64e8f42fd91e21707b6847550190a50f040) - Fixed the `SSRF` attack security issue in `HTTP` remote request forwarding 4.9.9.30 [020aee7](https://gitee.com/dotnetchina/Furion/commit/020aee78942c2f1d0630a27e483cf00cf83fbc90) [b30721d](https://gitee.com/dotnetchina/Furion/commit/b30721d9c30bd41376abeaa0dbca18ba59a0fc6b) - Fixed the issue where console progress printing might throw exceptions during `HTTP` remote file upload/download 4.9.9.20 [acfd9dd](https://gitee.com/dotnetchina/Furion/commit/acfd9ddc1c3c97f0a25ac526e880e3242303537f) - Fixed the connection pool exhaustion issue in extreme cases in `HTTP` remote requests 4.9.9.18 [9b7b430](https://gitee.com/dotnetchina/Furion/commit/9b7b430155b975dbd0e707575e7619c6251eeb98) - Fixed the decompression exception when using `HTTP` remote requests in `Blazor WebAssembly` applications 4.9.9.13 ⏱️2026.07.05 [0224fa0](https://gitee.com/dotnetchina/Furion/commit/0224fa0a5bdf21ee5f1488b0697b4f12daf95375) - Fixed the blank line printing issue in some special scenarios in `HTTP` remote request analysis logs 4.9.9.13 ⏱️2026.07.05 [0224fa0](https://gitee.com/dotnetchina/Furion/commit/0224fa0a5bdf21ee5f1488b0697b4f12daf95375) - Fixed the issue where declarative interfaces did not support closed generic interface definitions in `HTTP` remote requests 4.9.9.12 ⏱️2026.07.05 [aa47822](https://gitee.com/dotnetchina/Furion/commit/aa478228719b1ea1c1e3d23559ec089175ed8068) - Fixed the issue where `HttpResponseMessage` was not released in `HTTP` remote requests 4.9.9.10 ⏱️2026.07.03 [9a37cfa](https://gitee.com/dotnetchina/Furion/commit/9a37cfa1f89f1decc0b745d3b73302c15e8c35ac) - Fixed the issue where `SetJsonContent` could not apply global `HttpClient` configuration in `HTTP` remote requests 4.9.8.90 ⏱️2026.06.05 [8c5df99](https://gitee.com/dotnetchina/Furion/commit/8c5df990b842b001ab111f27109d7d7608df0aa9) - Fixed the potential memory overflow issue when forwarding `HttpContext` in `HTTP` remote requests 4.9.8.90 ⏱️2026.06.05 [8c5df99](https://gitee.com/dotnetchina/Furion/commit/8c5df990b842b001ab111f27109d7d7608df0aa9) - Fixed the thread pool issue in the file downloader of `HTTP` remote requests 4.9.8.90 ⏱️2026.06.05 [8c5df99](https://gitee.com/dotnetchina/Furion/commit/8c5df990b842b001ab111f27109d7d7608df0aa9) - Fixed the issue where the analysis tool could not print compressed content (such as `gzip`) in `HTTP` remote requests 4.9.8.64 ⏱️2026.05.13 [18d1922](https://gitee.com/dotnetchina/Furion/commit/18d1922b157d31dbec2a55302e8f173023559c70) - Fixed the memory issue and excessive `QPS` calculation error in `HTTP` remote request stress testing 4.9.8.57 ⏱️2026.05.01 [ab3b093](https://gitee.com/dotnetchina/Furion/commit/ab3b093ae7569ae8a03853e989ed870e7c7d60cf) - Fixed the deadlock issue when enabling request analysis logs in synchronous requests in `Blazor` applications for `HTTP` remote requests 4.9.8.46 ⏱️2026.04.19 [bfa8579](https://gitee.com/dotnetchina/Furion/commit/bfa8579b6f90d158aca7ccaa68bd5d7a79a3c5b7) - Fixed the issue where child attributes were not recursively searched when retrieving the proxy interface attribute list in `HTTP` remote requests 4.9.8.44 ⏱️2026.04.18 [7b0098d](https://gitee.com/dotnetchina/Furion/commit/7b0098d7ef59100a617678966ce46c679eaa2a30) - Fixed the exception when adding generic-type declarative interfaces in `HTTP` remote requests 4.9.8.42 ⏱️2026.04.17 [b00f2b9](https://gitee.com/dotnetchina/Furion/commit/b00f2b9a1ebbe8e2005af6e2eb940a3eff48ebc4) - Fixed the issue where downloading files failed if the server did not set `Content-Length` in `HTTP` remote requests 4.9.8.36 ⏱️2026.04.09 [d904e8d](https://gitee.com/dotnetchina/Furion/commit/d904e8de21210ec9b218594685b1da01be9be1f6) - Fixed the issue where `Accept-Language` could not be forwarded when forwarding `HttpContext` in `HTTP` remote requests 4.9.8.31 ⏱️2026.03.31 [#IHTVU9](https://gitee.com/dotnetchina/Furion/issues/IHTVU9) [1f67681](https://gitee.com/dotnetchina/Furion/commit/1f676815435ce65969b5a34ce051542321f07204) - Fixed the exception when the analysis tool prints files exceeding `2GB` in `HTTP` remote requests 4.9.8.2 ⏱️2026.01.24 [600d02a](https://gitee.com/dotnetchina/Furion/commit/600d02a0b60ce2b0cb594aa3bf2ba1e81be49ed3) - Fixed the issue where path segments were not removed when handling redirection in `HTTP` remote requests 4.9.8.1 ⏱️2026.01.22 [288facb](https://gitee.com/dotnetchina/Furion/commit/288facb79c71a71151ae9ec0bac650872fb1589e) - Fixed the issue where setting the base address did not support path parameters and configuration parameters in `HTTP` remote requests 4.9.7.232 ⏱️2025.12.22 [5252bbd](https://gitee.com/dotnetchina/Furion/commit/5252bbd3f5932ccaf336cbcd936880252aef1b67) - Fixed the duplicate printing issue in the analysis tool of `HTTP` remote requests 4.9.7.219 ⏱️2025.12.03 [82091b4](https://gitee.com/dotnetchina/Furion/commit/82091b43cd1f9b09eb7f0068630c34800124ccf4) - Fixed the issue where the analysis log printed incomplete form data in `HTTP` remote requests 4.9.7.217 ⏱️2025.12.03 [5bce378](https://gitee.com/dotnetchina/Furion/commit/5bce37839a0eb933e41c3d1bc8679488599badfe) - Fixed the issue where the analysis log did not print `HttpClient` default configuration request headers in `HTTP` remote requests 4.9.7.217 ⏱️2025.12.03 [fd0eedc](https://gitee.com/dotnetchina/Furion/commit/fd0eedc372ba42f341c7d0d50a1d4f578ba5056d) - Fixed the issue where cloning `HttpRequestMessage` lost the `Options` property in `HTTP` remote requests 4.9.7.215 ⏱️2025.11.26 [bf38601](https://gitee.com/dotnetchina/Furion/commit/bf38601a2ab8a2da11b26498c2c62269f50842fd) - Fixed the null reference exception when the upstream server response did not carry a `Content-Type` header in `HTTP` remote requests 4.9.7.210 ⏱️2025.11.18 [48eae77](https://gitee.com/dotnetchina/Furion/commit/48eae773bbc01bb7da2e277b88c05007eb5e991a) - Fixed the issue where the response body was lost for some status codes when forwarding `HttpContext` content in `HTTP` remote requests 4.9.7.210 ⏱️2025.11.18 [48eae77](https://gitee.com/dotnetchina/Furion/commit/48eae773bbc01bb7da2e277b88c05007eb5e991a) - Fixed the multi-threaded deadlock issue in the `HttpRemoteClient` static class of `HTTP` remote requests 4.9.7.137 ⏱️2025.11.07 [c044b87](https://gitee.com/dotnetchina/Furion/commit/c044b87cd1a6243637612872df1dd4a3a8cf4100) - Fixed the Chinese garbled text (mojibake) issue when parsing the `Content-Disposition` header filename in the response of `HTTP` remote requests 4.9.7.124 ⏱️2025.09.16 [183cb5e](https://gitee.com/dotnetchina/Furion/commit/183cb5e84ac894f9cb5580498cc1709c26a910d4) - Fixed the issue where the console progress bar could not adapt when uploading/downloading files in `HTTP` remote requests 4.9.7.116 ⏱️2025.09.02 [47250ef](https://gitee.com/dotnetchina/Furion/commit/47250ef3103d72d861df1f073331ec7edf45d6cd) - Fixed the concurrency thread safety issue in `HTTP` remote declarative requests 4.9.7.115 ⏱️2025.08.31 [0a5e57f](https://gitee.com/dotnetchina/Furion/commit/0a5e57fae035c2c0a614a67451743f3444ff3178) [#ICVKHB](https://github.com/monksoul/HttpAgent/issues/ICVKHB) - Fixed the issue where the filename had surrounding double quotes when parsing response headers during file download in `HTTP` remote requests 4.9.7.113 ⏱️2025.08.29 [5e92eab](https://gitee.com/dotnetchina/Furion/commit/5e92eabe4a95cd1af8de90e01e1e9f3eeed5aa5e) - Fixed the issue where `Content-Type` was lost when forwarding `HttpContext` in `HTTP` remote requests 4.9.7.109 ⏱️2025.08.14 [9aaf17c](https://gitee.com/dotnetchina/Furion/commit/9aaf17c568f5b470858056849310d4808c103ab7) - Fixed the issue where disabling cache was ineffective when forwarding `HttpContext` in `HTTP` remote requests 4.9.7.104 ⏱️2025.07.24 [3a386fa](https://gitee.com/dotnetchina/Furion/commit/3a386fa5345c56f6b29903e73a371a2bb3888fdf) - Fixed the issue where `MultipartFile`-type properties could not be sent via forms in `HTTP` remote requests 4.9.7.93 ⏱️2025.07.05 [30c853d](https://gitee.com/dotnetchina/Furion/commit/30c853d2597feeb81cca6df288021b634d686631) - Fixed the issue where uploading files without a configured filename caused the server to fail to receive the file (if no filename is specified, the filename defaults to `Unnamed_xxxxxxxxx`) in `HTTP` remote requests 4.9.7.93 ⏱️2025.07.05 [30c853d](https://gitee.com/dotnetchina/Furion/commit/30c853d2597feeb81cca6df288021b634d686631) - Fixed the issue where printing binary content containing backspace characters could produce incomplete output in the `HTTP` remote request analysis tool 4.9.7.93 ⏱️2025.07.05 [30c853d](https://gitee.com/dotnetchina/Furion/commit/30c853d2597feeb81cca6df288021b634d686631) - Fixed the timeout configuration issue in `HTTP` remote requests and clarified the exception type thrown after timeout 4.9.7.90 ⏱️2025.06.25 [679319d](https://gitee.com/dotnetchina/Furion/commit/679319ddbde84af6977114a5823f43fd3c006b96) - Fixed the issue where `HttpContent(Body)` could not be tampered with when converting `HttpContext` in `HTTP` remote requests 4.9.7.89 ⏱️2025.06.20 [ca7bfb5](https://gitee.com/dotnetchina/Furion/commit/ca7bfb5e1a3da863c84e03fbb8ce8e13aee7d101) - Fixed the issue where the analysis tool did not support `Blazor WebAssembly` applications in `HTTP` remote requests 4.9.7.69 ⏱️2025.05.22 [c257ed0](https://gitee.com/dotnetchina/Furion/commit/c257ed031236666d1e4bcb458c16f02c501d2175) - Fixed the format disorder issue when manually printing in the `HTTP` remote request analysis tool 4.9.7.52 ⏱️2025.04.27 [14261e4](https://gitee.com/dotnetchina/Furion/commit/14261e4fb2ff77c88b805ae8ac771bebfef51885) - Fixed **the memory overflow (`OOM`) issue in `HTTP` remote request deserialization caused by version `v4.9.7.49`** 4.9.7.50 ⏱️2025.04.25 [4cf7375](https://gitee.com/dotnetchina/Furion/commit/4cf7375508ac134f95e7e500c9aac7cffa19d115) [406ff44](https://gitee.com/dotnetchina/Furion/commit/406ff4495fb93a0a33827c63802c7d1f85bd4a1d) - Fixed the issue where a trailing `/` at the end of the request path was automatically removed in `HTTP` remote requests 4.9.7.45 ⏱️2025.04.17 [5b18955](https://gitee.com/dotnetchina/Furion/commit/5b18955367d208222a668a5ae1c76702e77e6e3e) - Fixed the issue where `User-Agent` could not be removed via `RemoveHeaders` in `HTTP` remote requests 4.9.7.44 ⏱️2025.04.17 [4d98d60](https://gitee.com/dotnetchina/Furion/commit/4d98d6053f7b05c73b6d60d5284c040538f18789) - Fixed the exception when forcing `IPv4` and the request address is an `IP` address in `HTTP` remote requests 4.9.7.28 ⏱️2025.03.23 [1d57a07](https://gitee.com/dotnetchina/Furion/commit/1d57a0732531a69ea1ed6774add60b24e86f8b7d) - Fixed the parsing failure when a parameter value contains multiple `=` while parsing `URL` parameters in `HTTP` remote requests 4.9.7.24 ⏱️2025.03.13 [5c9270f](https://gitee.com/dotnetchina/Furion/commit/5c9270f39721d5b25c08d081f27377f7bdb4bee5) - Fixed the issue where removing query parameters was ineffective when no query parameters were set in `HTTP` remote requests 4.9.7.21 ⏱️2025.03.03 [7b3335e](https://gitee.com/dotnetchina/Furion/commit/7b3335e8af30509aa1f0465a881693bd3b6f114b) - Fixed the `CancellationToken` error handling issue in file upload/download, long polling, and `Server-Sent Events` in `HTTP` remote requests 4.9.7.16 ⏱️2025.02.28 [21c1f06](https://gitee.com/dotnetchina/Furion/commit/21c1f06cfed1f892eb5ee7bf91989103a3e922d5) - Fixed the null reference exception when configuring the client base address in `HTTP` remote requests 4.9.7.16 ⏱️2025.02.28 [21c1f06](https://gitee.com/dotnetchina/Furion/commit/21c1f06cfed1f892eb5ee7bf91989103a3e922d5) - Fixed the issue where the analysis tool did not print requests that were not actually successful but ensured the request was successful in `HTTP` remote requests 4.9.7.10 ⏱️2025.02.22 [82b4d81](https://gitee.com/dotnetchina/Furion/commit/82b4d81ae60f1918f06cc28b780902f7096c4fa4) - Fixed the issue of incorrectly handling the request method and request content during redirection in `HTTP` remote requests 4.9.7.2 ⏱️2025.01.26 [c326cf3](https://gitee.com/dotnetchina/Furion/commit/c326cf3c536f29dd29198477990708590b2aeeed) - Fixed the corrupted file issue when forwarding `HttpContext` files in `HTTP` remote requests 4.9.7.1 ⏱️2025.01.23 [e90a08c](https://gitee.com/dotnetchina/Furion/commit/e90a08cb76419d6a9130db89774130a8c13e27b4) - Fixed the issue where query parameters might be repeatedly concatenated when encountering redirection in `HTTP` remote requests 4.9.7 ⏱️2025.01.23 [0e64da5](https://gitee.com/dotnetchina/Furion/commit/0e64da5fe468a0c925bc9bb21985dd119fe9834c) - Fixed Fixed the issue where `HTTP` remote requests could not automatically categorize request headers, such as failing to identify which are request content headers 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Fixed Fixed the issue where the `HTTP` remote request content type could not be automatically inferred from request headers 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Fixed Fixed the issue where `HTTP` remote requests did not support `Basic` authentication with an empty password 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Fixed Fixed the issue where the `HTTP` remote request `URL`-encoded form processor did not support string content 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Fixed Fixed the issue where the `Content-Length` header was not excluded when forwarding `HttpContext` in `HTTP` remote requests, causing forwarding exceptions 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Fixed Fixed the issue where `HttpContext` forwarded to `IActionResult` results in `HTTP` remote requests could not be automatically decompressed 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Fixed Fixed the issue where the `HTTP` remote request file downloader failed due to network fluctuations (additional log tracking output added) 4.9.9.66 ⏱️2026.08.08 [152fa8e](https://gitee.com/dotnetchina/Furion/commit/152fa8e40207a53f84980830a7a55685c839201a) - Fixed Fixed the occasional freeze issue after configuring the number of download/upload threads in `HTTP` remote requests 4.9.9.65 ⏱️2026.08.07 [632ad1e](https://gitee.com/dotnetchina/Furion/commit/632ad1e13a294d390136f7853a65ce8502900759) - Fixed Fixed the blank page (no output) issue when forwarding websites through `HttpContext` in `HTTP` remote requests 4.9.9.65 ⏱️2026.08.07 [632ad1e](https://gitee.com/dotnetchina/Furion/commit/632ad1e13a294d390136f7853a65ce8502900759) - Fixed Fixed the memory overflow issue in `HTTP` remote request long polling and `SSE` scenarios 4.9.9.65 ⏱️2026.08.07 [632ad1e](https://gitee.com/dotnetchina/Furion/commit/632ad1e13a294d390136f7853a65ce8502900759) - Fixed Fixed the issue of calculation deviation in `HTTP` remote request stress testing results 4.9.9.65 ⏱️2026.08.07 [632ad1e](https://gitee.com/dotnetchina/Furion/commit/632ad1e13a294d390136f7853a65ce8502900759) - Fixed Fixed the issue where the automatic refresh configuration of the built-in `Furion` and `WeChat` `Access Token` managers in `HTTP` remote requests became ineffective 4.9.9.65 ⏱️2026.08.07 [632ad1e](https://gitee.com/dotnetchina/Furion/commit/632ad1e13a294d390136f7853a65ce8502900759) - Fixed Fixed the issue where the built-in `Furion` framework `Access Token` provider in `HTTP` remote requests did not refresh after expiration 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Fixed Fixed the issue where the assertion context in `HTTP` remote requests prevented external re-reading after reading the response content 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Fixed Fixed the multicast delegate handling error in `HTTP` remote requests (synchronous and asynchronous) 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Fixed Fixed the issue where appending parameters in `HTTP` remote requests failed to replace the original `URL` address even when `replace: true` was configured 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Fixed Fixed the issue where unnecessary original parameters were carried during redirects in `HTTP` remote requests, causing request failures 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Fixed Fixed the console progress bar disorder when downloading multiple files simultaneously in `HTTP` remote requests 4.9.9.59 ⏱️2026.08.03 [ecc25f9](https://gitee.com/dotnetchina/Furion/commit/ecc25f946d27c226234798277cca1cd56a5d8ca0) [029e51a](https://gitee.com/dotnetchina/Furion/commit/029e51aa27128bf6a22b66770a4fb1039cb1aa9c) - Fixed Fixed the issue where `HTTP` remote requests did not support the `File-Based Apps` application type 4.9.9.53 ⏱️2026.08.02 [aae2318](https://gitee.com/dotnetchina/Furion/commit/aae23182e161e84419fa854b0c68e8b722056167) - Fixed Fixed the `Boundary` parsing error of the content type when forwarding forms in `HTTP` remote requests 4.9.9.50 ⏱️2026.07.31 [8a93868](https://gitee.com/dotnetchina/Furion/commit/8a93868afedaf56f879fcfdd256e90540ebb556d) - Fixed Fixed the error handling format and algorithm issues of `Digest` authentication in `HTTP` remote requests 4.9.9.47 ⏱️2026.07.30 [312767f](https://gitee.com/dotnetchina/Furion/commit/312767f1a51f4c681fd6f8d6957d0ddaf9bd4885) - Fixed Fixed the stream-disposed exception when the `HTTP` remote request analysis log printed response content of unknown size exceeding `5MB` 4.9.9.46 ⏱️2026.07.29 [22c8edb](https://gitee.com/dotnetchina/Furion/commit/22c8edbe5e249fe10ad011d0dc8982b61278422c) - Fixed Fixed the data truncation issue when the `HTTP` remote request `WebSocket` client received large messages 4.9.9.46 ⏱️2026.07.29 [fc1634d](https://gitee.com/dotnetchina/Furion/commit/fc1634d52182db315f7a2c38c9f1f33f25be100e) - Fixed Fixed the deadlock issue when the `HTTP` remote request analysis log printed `SSE` or streaming response content 4.9.9.43 ⏱️2026.07.26 [5fd05a1](https://gitee.com/dotnetchina/Furion/commit/5fd05a1bacf8bf67883b3bb8000164e41cacee32) [09cb4f1](https://gitee.com/dotnetchina/Furion/commit/09cb4f130ccacd7c027cf06e752716013e19697c) - Fixed Fixed the error handling data issue in `HTTP` remote request `SSE` and long polling 4.9.9.43 ⏱️2026.07.26 [5fd05a1](https://gitee.com/dotnetchina/Furion/commit/5fd05a1bacf8bf67883b3bb8000164e41cacee32) - Fixed Fixed the issue where the `HTTP` remote request `WebSocket` client did not release event resources after disposal 4.9.9.34 [3ae3f64](https://gitee.com/dotnetchina/Furion/commit/3ae3f64e8f42fd91e21707b6847550190a50f040) - Fixed Fixed the `SSRF` attack security issue in `HTTP` remote request forwarding 4.9.9.30 [020aee7](https://gitee.com/dotnetchina/Furion/commit/020aee78942c2f1d0630a27e483cf00cf83fbc90) [b30721d](https://gitee.com/dotnetchina/Furion/commit/b30721d9c30bd41376abeaa0dbca18ba59a0fc6b) - Fixed Fixed the issue where console progress printing might throw exceptions during `HTTP` remote file upload/download 4.9.9.20 [acfd9dd](https://gitee.com/dotnetchina/Furion/commit/acfd9ddc1c3c97f0a25ac526e880e3242303537f) - Fixed Fixed the connection pool exhaustion issue in `HTTP` remote requests under extreme conditions 4.9.9.18 [9b7b430](https://gitee.com/dotnetchina/Furion/commit/9b7b430155b975dbd0e707575e7619c6251eeb98) - Fixed Fixed the decompression exception issue when using `HTTP` remote requests in `Blazor WebAssembly` applications 4.9.9.13 ⏱️2026.07.05 [0224fa0](https://gitee.com/dotnetchina/Furion/commit/0224fa0a5bdf21ee5f1488b0697b4f12daf95375) - Fixed Fixed the blank line printing issue in `HTTP` remote request analysis logs under some special scenarios 4.9.9.13 ⏱️2026.07.05 [0224fa0](https://gitee.com/dotnetchina/Furion/commit/0224fa0a5bdf21ee5f1488b0697b4f12daf95375) - Fixed Fixed the issue where `HTTP` remote request declarative interfaces did not support closed generic interface definitions 4.9.9.12 ⏱️2026.07.05 [aa47822](https://gitee.com/dotnetchina/Furion/commit/aa478228719b1ea1c1e3d23559ec089175ed8068) - Fixed Fixed the issue of undisposed `HttpResponseMessage` in `HTTP` remote requests 4.9.9.10 ⏱️2026.07.03 [9a37cfa](https://gitee.com/dotnetchina/Furion/commit/9a37cfa1f89f1decc0b745d3b73302c15e8c35ac) - Fixed Fixed the issue where `SetJsonContent` in `HTTP` remote requests could not apply global `HttpClient` configuration 4.9.8.90 ⏱️2026.06.05 [8c5df99](https://gitee.com/dotnetchina/Furion/commit/8c5df990b842b001ab111f27109d7d7608df0aa9) - Fixed Fixed the potential memory overflow issue when forwarding `HttpContext` in `HTTP` remote requests 4.9.8.90 ⏱️2026.06.05 [8c5df99](https://gitee.com/dotnetchina/Furion/commit/8c5df990b842b001ab111f27109d7d7608df0aa9) - Fixed Fixed the thread pool issue in the `HTTP` remote request file downloader 4.9.8.90 ⏱️2026.06.05 [8c5df99](https://gitee.com/dotnetchina/Furion/commit/8c5df990b842b001ab111f27109d7d7608df0aa9) - Fixed Fixed the issue where the `HTTP` remote request analysis tool could not print compressed content (such as `gzip`) 4.9.8.64 ⏱️2026.05.13 [18d1922](https://gitee.com/dotnetchina/Furion/commit/18d1922b157d31dbec2a55302e8f173023559c70) - Fixed Fixed the memory issue and excessive `QPS` calculation error in `HTTP` remote request stress testing 4.9.8.57 ⏱️2026.05.01 [ab3b093](https://gitee.com/dotnetchina/Furion/commit/ab3b093ae7569ae8a03853e989ed870e7c7d60cf) - Fixed Fixed the deadlock issue when the request analysis log was enabled in synchronous requests in `Blazor` applications 4.9.8.46 ⏱️2026.04.19 [bfa8579](https://gitee.com/dotnetchina/Furion/commit/bfa8579b6f90d158aca7ccaa68bd5d7a79a3c5b7) - Fixed Fixed the issue where child attributes were not recursively searched when getting the proxy interface attribute list in `HTTP` remote requests 4.9.8.44 ⏱️2026.04.18 [7b0098d](https://gitee.com/dotnetchina/Furion/commit/7b0098d7ef59100a617678966ce46c679eaa2a30) - Fixed Fixed the exception issue when adding generic declarative interfaces in `HTTP` remote requests 4.9.8.42 ⏱️2026.04.17 [b00f2b9](https://gitee.com/dotnetchina/Furion/commit/b00f2b9a1ebbe8e2005af6e2eb940a3eff48ebc4) - Fixed Fixed the download failure issue when the server did not set `Content-Length` while downloading files in `HTTP` remote requests 4.9.8.36 ⏱️2026.04.09 [d904e8d](https://gitee.com/dotnetchina/Furion/commit/d904e8de21210ec9b218594685b1da01be9be1f6) - Fixed Fixed the issue where `Accept-Language` could not be forwarded when forwarding `HttpContext` in `HTTP` remote requests 4.9.8.31 ⏱️2026.03.31 [#IHTVU9](https://gitee.com/dotnetchina/Furion/issues/IHTVU9) [1f67681](https://gitee.com/dotnetchina/Furion/commit/1f676815435ce65969b5a34ce051542321f07204) - Fixed Fixed the exception issue when the `HTTP` remote request analysis tool printed files exceeding `2GB` 4.9.8.2 ⏱️2026.01.24 [600d02a](https://gitee.com/dotnetchina/Furion/commit/600d02a0b60ce2b0cb594aa3bf2ba1e81be49ed3) - Fixed Fixed the issue where path segments were not removed when handling redirects in `HTTP` remote requests 4.9.8.1 ⏱️2026.01.22 [288facb](https://gitee.com/dotnetchina/Furion/commit/288facb79c71a71151ae9ec0bac650872fb1589e) - Fixed Fixed the issue where setting the base address in `HTTP` remote requests did not support path parameters and configuration parameters 4.9.7.232 ⏱️2025.12.22 [5252bbd](https://gitee.com/dotnetchina/Furion/commit/5252bbd3f5932ccaf336cbcd936880252aef1b67) - Fixed Fixed the duplicate printing issue in the `HTTP` remote request analysis tool 4.9.7.219 ⏱️2025.12.03 [82091b4](https://gitee.com/dotnetchina/Furion/commit/82091b43cd1f9b09eb7f0068630c34800124ccf4) - Fixed Fixed the issue where the `HTTP` remote request analysis log did not print form data completely 4.9.7.217 ⏱️2025.12.03 [5bce378](https://gitee.com/dotnetchina/Furion/commit/5bce37839a0eb933e41c3d1bc8679488599badfe) - Fixed Fixed the issue where the `HTTP` remote request analysis log did not print the `HttpClient` default configured request headers 4.9.7.217 ⏱️2025.12.03 [fd0eedc](https://gitee.com/dotnetchina/Furion/commit/fd0eedc372ba42f341c7d0d50a1d4f578ba5056d) - Fixed Fixed the issue where cloning `HttpRequestMessage` in `HTTP` remote requests lost the `Options` property 4.9.7.215 ⏱️2025.11.26 [bf38601](https://gitee.com/dotnetchina/Furion/commit/bf38601a2ab8a2da11b26498c2c62269f50842fd) - Fixed Fixed the null reference exception when the upstream server response did not carry the `Content-Type` header in `HTTP` remote requests 4.9.7.210 ⏱️2025.11.18 [48eae77](https://gitee.com/dotnetchina/Furion/commit/48eae773bbc01bb7da2e277b88c05007eb5e991a) - Fixed Fixed the issue where the response body was lost for some status codes when forwarding `HttpContext` content in `HTTP` remote requests 4.9.7.210 ⏱️2025.11.18 [48eae77](https://gitee.com/dotnetchina/Furion/commit/48eae773bbc01bb7da2e277b88c05007eb5e991a) - Fixed Fixed the multi-threaded deadlock issue in the `HTTP` remote request static class `HttpRemoteClient` 4.9.7.137 ⏱️2025.11.07 [c044b87](https://gitee.com/dotnetchina/Furion/commit/c044b87cd1a6243637612872df1dd4a3a8cf4100) - Fixed Fixed the garbled Chinese characters issue when parsing the file name in the response `Content-Disposition` header in `HTTP` remote requests 4.9.7.124 ⏱️2025.09.16 [183cb5e](https://gitee.com/dotnetchina/Furion/commit/183cb5e84ac894f9cb5580498cc1709c26a910d4) - Fixed Fixed the issue where the console progress bar could not adapt during file upload and download in `HTTP` remote requests 4.9.7.116 ⏱️2025.09.02 [47250ef](https://gitee.com/dotnetchina/Furion/commit/47250ef3103d72d861df1f073331ec7edf45d6cd) - Fixed Fixed the concurrency thread safety issue in `HTTP` remote declarative requests 4.9.7.115 ⏱️2025.08.31 [0a5e57f](https://gitee.com/dotnetchina/Furion/commit/0a5e57fae035c2c0a614a67451743f3444ff3178) [#ICVKHB](https://github.com/monksoul/HttpAgent/issues/ICVKHB) - Fixed Fixed the issue where the file name had leading and trailing double quotes when parsing the response header during file download in `HTTP` remote requests 4.9.7.113 ⏱️2025.08.29 [5e92eab](https://gitee.com/dotnetchina/Furion/commit/5e92eabe4a95cd1af8de90e01e1e9f3eeed5aa5e) - Fixed Fixed the issue where `Content-Type` was lost when forwarding `HttpContext` in `HTTP` remote requests 4.9.7.109 ⏱️2025.08.14 [9aaf17c](https://gitee.com/dotnetchina/Furion/commit/9aaf17c568f5b470858056849310d4808c103ab7) - Fixed Fixed the issue where disabling the cache was ineffective when forwarding `HttpContext` in `HTTP` remote requests 4.9.7.104 ⏱️2025.07.24 [3a386fa](https://gitee.com/dotnetchina/Furion/commit/3a386fa5345c56f6b29903e73a371a2bb3888fdf) - Fixed Fixed the issue where `MultipartFile` type properties could not be sent via forms in `HTTP` remote requests 4.9.7.93 ⏱️2025.07.05 [30c853d](https://gitee.com/dotnetchina/Furion/commit/30c853d2597feeb81cca6df288021b634d686631) - Fixed Fixed the issue where uploading files in `HTTP` remote requests without configuring the file name caused the server to fail to receive the file normally (if the file name is not specified, it defaults to `Unnamed_xxxxxxxxx`) 4.9.7.93 ⏱️2025.07.05 [30c853d](https://gitee.com/dotnetchina/Furion/commit/30c853d2597feeb81cca6df288021b634d686631) - Fixed Fixed the issue where the `HTTP` remote request analysis tool might produce incomplete output when printing binary content containing backspace characters 4.9.7.93 ⏱️2025.07.05 [30c853d](https://gitee.com/dotnetchina/Furion/commit/30c853d2597feeb81cca6df288021b634d686631) - Fixed Fixed the issue of configuring the timeout in `HTTP` remote requests, and clarified the exception type thrown after timeout 4.9.7.90 ⏱️2025.06.25 [679319d](https://gitee.com/dotnetchina/Furion/commit/679319ddbde84af6977114a5823f43fd3c006b96) - Fixed Fixed the issue where `HttpContent(Body)` could not be modified when converting `HttpContext` in `HTTP` remote requests 4.9.7.89 ⏱️2025.06.20 [ca7bfb5](https://gitee.com/dotnetchina/Furion/commit/ca7bfb5e1a3da863c84e03fbb8ce8e13aee7d101) - Fixed Fixed the issue where the `HTTP` remote request analysis tool did not support `Blazor WebAssembly` applications 4.9.7.69 ⏱️2025.05.22 [c257ed0](https://gitee.com/dotnetchina/Furion/commit/c257ed031236666d1e4bcb458c16f02c501d2175) - Fixed Fixed the format disorder issue when the `HTTP` remote request analysis tool was manually printed 4.9.7.52 ⏱️2025.04.27 [14261e4](https://gitee.com/dotnetchina/Furion/commit/14261e4fb2ff77c88b805ae8ac771bebfef51885) - Fixed **Fixed the memory overflow (`OOM`) issue in `HTTP` remote request deserialization caused by version `v4.9.7.49`** 4.9.7.50 ⏱️2025.04.25 [4cf7375](https://gitee.com/dotnetchina/Furion/commit/4cf7375508ac134f95e7e500c9aac7cffa19d115) [406ff44](https://gitee.com/dotnetchina/Furion/commit/406ff4495fb93a0a33827c63802c7d1f85bd4a1d) - Fixed Fixed the issue where a trailing `/` at the end of the request path was automatically removed in `HTTP` remote requests 4.9.7.45 ⏱️2025.04.17 [5b18955](https://gitee.com/dotnetchina/Furion/commit/5b18955367d208222a668a5ae1c76702e77e6e3e) - Fixed Fixed the issue where `User-Agent` could not be removed via `RemoveHeaders` in `HTTP` remote requests 4.9.7.44 ⏱️2025.04.17 [4d98d60](https://gitee.com/dotnetchina/Furion/commit/4d98d6053f7b05c73b6d60d5284c040538f18789) - Fixed Fixed the exception issue when forcing `IPv4` in `HTTP` remote requests while the request address was an `IP` address 4.9.7.28 ⏱️2025.03.23 [1d57a07](https://gitee.com/dotnetchina/Furion/commit/1d57a0732531a69ea1ed6774add60b24e86f8b7d) - Fixed Fixed the parsing failure issue when a parameter value contained multiple `=` while parsing `URL` parameters in `HTTP` remote requests 4.9.7.24 ⏱️2025.03.13 [5c9270f](https://gitee.com/dotnetchina/Furion/commit/5c9270f39721d5b25c08d081f27377f7bdb4bee5) - Fixed Fixed the issue where it was ineffective when no query parameters were set but a removal query parameter list was configured in `HTTP` remote requests 4.9.7.21 ⏱️2025.03.03 [7b3335e](https://gitee.com/dotnetchina/Furion/commit/7b3335e8af30509aa1f0465a881693bd3b6f114b) - Fixed Fixed the `CancellationToken` error handling issue in `HTTP` remote request file upload/download, long polling, and `Server-Sent Events` 4.9.7.16 ⏱️2025.02.28 [21c1f06](https://gitee.com/dotnetchina/Furion/commit/21c1f06cfed1f892eb5ee7bf91989103a3e922d5) - Fixed Fixed the null reference exception when the client configured the base address in `HTTP` remote requests 4.9.7.16 ⏱️2025.02.28 [21c1f06](https://gitee.com/dotnetchina/Furion/commit/21c1f06cfed1f892eb5ee7bf91989103a3e922d5) - Fixed Fixed the issue where the `HTTP` remote request analysis tool did not print requests that were not actually successful but were ensured to be successful 4.9.7.10 ⏱️2025.02.22 [82b4d81](https://gitee.com/dotnetchina/Furion/commit/82b4d81ae60f1918f06cc28b780902f7096c4fa4) - Fixed Fixed the issue of incorrectly handling the request method and request content in `HTTP` remote request redirect operations 4.9.7.2 ⏱️2025.01.26 [c326cf3](https://gitee.com/dotnetchina/Furion/commit/c326cf3c536f29dd29198477990708590b2aeeed) - Fixed Fixed the corrupted file issue when forwarding `HttpContext` files in `HTTP` remote requests 4.9.7.1 ⏱️2025.01.23 [e90a08c](https://gitee.com/dotnetchina/Furion/commit/e90a08cb76419d6a9130db89774130a8c13e27b4) - Fixed Fixed the issue where query parameters might be concatenated repeatedly when `HTTP` remote requests encountered redirects 4.9.7 ⏱️2025.01.23 [0e64da5](https://gitee.com/dotnetchina/Furion/commit/0e64da5fe468a0c925bc9bb21985dd119fe9834c) - Fixed Fixed the issue where `HTTP` remote requests could not automatically categorize request headers when setting them, e.g. being unable to identify which are request content headers 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Fixed Fixed the issue where `HTTP` remote request content type could not be automatically inferred from request headers 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Fixed Fixed the issue where `HTTP` remote requests could not automatically decompress when forwarding `HttpContext` to an `IActionResult` result 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Fixed Fixed the issue where the `HTTP` remote request file downloader failed due to network fluctuations (additionally added log trace output) 4.9.9.66 ⏱️2026.08.08 [152fa8e](https://gitee.com/dotnetchina/Furion/commit/152fa8e40207a53f84980830a7a55685c839201a) - Fixed Fixed the issue where `HTTP` remote requests occasionally hung after configuring the download/upload thread count 4.9.9.65 ⏱️2026.08.07 [632ad1e](https://gitee.com/dotnetchina/Furion/commit/632ad1e13a294d390136f7853a65ce8502900759) - Fixed Fixed the issue where a blank page (no output) appeared when `HTTP` remote requests forwarded a website via `HttpContext` 4.9.9.65 ⏱️2026.08.07 [632ad1e](https://gitee.com/dotnetchina/Furion/commit/632ad1e13a294d390136f7853a65ce8502900759) - Fixed Fixed the calculation deviation issue in `HTTP` remote request stress testing results 4.9.9.65 ⏱️2026.08.07 [632ad1e](https://gitee.com/dotnetchina/Furion/commit/632ad1e13a294d390136f7853a65ce8502900759) - Fixed Fixed the issue where the built-in `Furion` and `WeChat` `Access Token` manager automatic refresh configuration became ineffective in `HTTP` remote requests 4.9.9.65 ⏱️2026.08.07 [632ad1e](https://gitee.com/dotnetchina/Furion/commit/632ad1e13a294d390136f7853a65ce8502900759) - Fixed Fixed the issue where the `HTTP` remote request assertion context read the response content, making it impossible for external code to read it again 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Fixed Fixed the issue where `HTTP` remote request multicast delegates were handled incorrectly (sync and async) 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Fixed Fixed the issue where, when appending parameters in `HTTP` remote requests, configuring `replace: true` still could not replace the original `URL` address 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Fixed Fixed the issue where `HTTP` remote requests carried unnecessary original parameters on redirect, causing request failure 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - Fixed Fixed the issue where the console progress bar became disordered when downloading multiple files simultaneously in `HTTP` remote requests 4.9.9.59 ⏱️2026.08.03 [ecc25f9](https://gitee.com/dotnetchina/Furion/commit/ecc25f946d27c226234798277cca1cd56a5d8ca0) [029e51a](https://gitee.com/dotnetchina/Furion/commit/029e51aa27128bf6a22b66770a4fb1039cb1aa9c) - Fixed Fixed the `Boundary` parsing error of the content type when `HTTP` remote requests forwarded forms 4.9.9.50 ⏱️2026.07.31 [8a93868](https://gitee.com/dotnetchina/Furion/commit/8a93868afedaf56f879fcfdd256e90540ebb556d) - Fixed Fixed the `HTTP` remote request `Digest` digest authentication error handling format and algorithm issues 4.9.9.47 ⏱️2026.07.30 [312767f](https://gitee.com/dotnetchina/Furion/commit/312767f1a51f4c681fd6f8d6957d0ddaf9bd4885) - Fixed Fixed the stream-disposed exception when `HTTP` remote request Profiler logs printed response content of unknown size exceeding `5MB` 4.9.9.46 ⏱️2026.07.29 [22c8edb](https://gitee.com/dotnetchina/Furion/commit/22c8edbe5e249fe10ad011d0dc8982b61278422c) - Fixed Fixed the deadlock issue when `HTTP` remote request Profiler logs printed `SSE` or streaming response content 4.9.9.43 ⏱️2026.07.26 [5fd05a1](https://gitee.com/dotnetchina/Furion/commit/5fd05a1bacf8bf67883b3bb8000164e41cacee32) [09cb4f1](https://gitee.com/dotnetchina/Furion/commit/09cb4f130ccacd7c027cf06e752716013e19697c) - Fixed Fixed the `HTTP` remote request `SSE` and long polling error handling data issue 4.9.9.43 ⏱️2026.07.26 [5fd05a1](https://gitee.com/dotnetchina/Furion/commit/5fd05a1bacf8bf67883b3bb8000164e41cacee32) - Fixed Fixed the issue where console progress printing could throw exceptions during `HTTP` remote file upload/download 4.9.9.20 [acfd9dd](https://gitee.com/dotnetchina/Furion/commit/acfd9ddc1c3c97f0a25ac526e880e3242303537f) - Fixed Fixed the decompression exception when `HTTP` remote requests were used in `Blazor WebAssembly` applications 4.9.9.13 ⏱️2026.07.05 [0224fa0](https://gitee.com/dotnetchina/Furion/commit/0224fa0a5bdf21ee5f1488b0697b4f12daf95375) - Fixed Fixed the blank-line printing issue in `HTTP` remote request Profiler logs in some special scenarios 4.9.9.13 ⏱️2026.07.05 [0224fa0](https://gitee.com/dotnetchina/Furion/commit/0224fa0a5bdf21ee5f1488b0697b4f12daf95375) - Fixed Fixed the issue where `HTTP` remote requests had undisposed `HttpResponseMessage` instances 4.9.9.10 ⏱️2026.07.03 [9a37cfa](https://gitee.com/dotnetchina/Furion/commit/9a37cfa1f89f1decc0b745d3b73302c15e8c35ac) - Fixed Fixed the issue where `HTTP` remote request `SetJsonContent` could not apply global `HttpClient` configuration 4.9.8.90 ⏱️2026.06.05 [8c5df99](https://gitee.com/dotnetchina/Furion/commit/8c5df990b842b001ab111f27109d7d7608df0aa9) - Fixed Fixed the potential memory overflow issue when `HTTP` remote requests forwarded `HttpContext` 4.9.8.90 ⏱️2026.06.05 [8c5df99](https://gitee.com/dotnetchina/Furion/commit/8c5df990b842b001ab111f27109d7d7608df0aa9) - Fixed Fixed the issue where the `HTTP` remote request Profiler could not print compressed content (e.g. `gzip`) 4.9.8.64 ⏱️2026.05.13 [18d1922](https://gitee.com/dotnetchina/Furion/commit/18d1922b157d31dbec2a55302e8f173023559c70) - Fixed Fixed the memory issue in `HTTP` remote request stress testing and the excessive `QPS` calculation error 4.9.8.57 ⏱️2026.05.01 [ab3b093](https://gitee.com/dotnetchina/Furion/commit/ab3b093ae7569ae8a03853e989ed870e7c7d60cf) - Fixed Fixed the deadlock issue when enabling Profiler logs in synchronous requests in `Blazor` applications for `HTTP` remote requests 4.9.8.46 ⏱️2026.04.19 [bfa8579](https://gitee.com/dotnetchina/Furion/commit/bfa8579b6f90d158aca7ccaa68bd5d7a79a3c5b7) - Fixed Fixed the issue where `HTTP` remote requests did not recursively search sub-attributes when getting the proxy interface attribute list 4.9.8.44 ⏱️2026.04.18 [7b0098d](https://gitee.com/dotnetchina/Furion/commit/7b0098d7ef59100a617678966ce46c679eaa2a30) - Fixed Fixed the exception when `HTTP` remote requests added declarative interfaces of generic types 4.9.8.42 ⏱️2026.04.17 [b00f2b9](https://gitee.com/dotnetchina/Furion/commit/b00f2b9a1ebbe8e2005af6e2eb940a3eff48ebc4) - Fixed Fixed the issue where downloading failed if the server did not set `Content-Length` in `HTTP` remote requests 4.9.8.36 ⏱️2026.04.09 [d904e8d](https://gitee.com/dotnetchina/Furion/commit/d904e8de21210ec9b218594685b1da01be9be1f6) - Fixed Fixed the exception when the `HTTP` remote request Profiler printed files exceeding `2GB` 4.9.8.2 ⏱️2026.01.24 [600d02a](https://gitee.com/dotnetchina/Furion/commit/600d02a0b60ce2b0cb594aa3bf2ba1e81be49ed3) - Fixed Fixed the issue where `HTTP` remote requests did not remove path segments when handling redirects 4.9.8.1 ⏱️2026.01.22 [288facb](https://gitee.com/dotnetchina/Furion/commit/288facb79c71a71151ae9ec0bac650872fb1589e) - Fixed Fixed the issue where `HTTP` remote request base address settings did not support path parameters and configuration parameters 4.9.7.232 ⏱️2025.12.22 [5252bbd](https://gitee.com/dotnetchina/Furion/commit/5252bbd3f5932ccaf336cbcd936880252aef1b67) - Fixed Fixed the duplicate printing issue in the `HTTP` remote request Profiler 4.9.7.219 ⏱️2025.12.03 [82091b4](https://gitee.com/dotnetchina/Furion/commit/82091b43cd1f9b09eb7f0068630c34800124ccf4) - Fixed Fixed the issue where `HTTP` remote request Profiler logs printed incomplete form data 4.9.7.217 ⏱️2025.12.03 [5bce378](https://gitee.com/dotnetchina/Furion/commit/5bce37839a0eb933e41c3d1bc8679488599badfe) - Fixed Fixed the issue where `HTTP` remote request Profiler logs did not print `HttpClient` default-configuration request headers 4.9.7.217 ⏱️2025.12.03 [fd0eedc](https://gitee.com/dotnetchina/Furion/commit/fd0eedc372ba42f341c7d0d50a1d4f578ba5056d) - Fixed Fixed the null reference exception when the upstream server response did not carry a `Content-Type` header in `HTTP` remote requests 4.9.7.210 ⏱️2025.11.18 [48eae77](https://gitee.com/dotnetchina/Furion/commit/48eae773bbc01bb7da2e277b88c05007eb5e991a) - Fixed Fixed the Chinese mojibake issue when parsing the filename from the response `Content-Disposition` header in `HTTP` remote requests 4.9.7.124 ⏱️2025.09.16 [183cb5e](https://gitee.com/dotnetchina/Furion/commit/183cb5e84ac894f9cb5580498cc1709c26a910d4) - Fixed Fixed the issue where the console progress bar could not auto-adapt during `HTTP` remote request file upload/download 4.9.7.116 ⏱️2025.09.02 [47250ef](https://gitee.com/dotnetchina/Furion/commit/47250ef3103d72d861df1f073331ec7edf45d6cd) - Fixed Fixed the concurrent thread safety issue in `HTTP` remote declarative requests 4.9.7.115 ⏱️2025.08.31 [0a5e57f](https://gitee.com/dotnetchina/Furion/commit/0a5e57fae035c2c0a614a67451743f3444ff3178) [#ICVKHB](https://github.com/monksoul/HttpAgent/issues/ICVKHB) - Fixed Fixed the issue where the filename had surrounding double quotes when parsing response headers during `HTTP` remote request file downloads 4.9.7.113 ⏱️2025.08.29 [5e92eab](https://gitee.com/dotnetchina/Furion/commit/5e92eabe4a95cd1af8de90e01e1e9f3eeed5aa5e) - Fixed Fixed the issue where `MultipartFile`-typed properties could not be sent via forms in `HTTP` remote requests 4.9.7.93 ⏱️2025.07.05 [30c853d](https://gitee.com/dotnetchina/Furion/commit/30c853d2597feeb81cca6df288021b634d686631) - Fixed Fixed the issue where, when uploading files in `HTTP` remote requests, an unconfigured filename caused the server to fail to receive the file properly (if no filename is specified, the filename defaults to `Unnamed_xxxxxxxxx`) 4.9.7.93 ⏱️2025.07.05 [30c853d](https://gitee.com/dotnetchina/Furion/commit/30c853d2597feeb81cca6df288021b634d686631) - Fixed Fixed the issue where the `HTTP` remote request Profiler could produce incomplete output when printing binary content containing backspace characters 4.9.7.93 ⏱️2025.07.05 [30c853d](https://gitee.com/dotnetchina/Furion/commit/30c853d2597feeb81cca6df288021b634d686631) - Fixed Fixed the issue of configuring timeout in `HTTP` remote requests, and clarified the exception type thrown after a timeout 4.9.7.90 ⏱️2025.06.25 [679319d](https://gitee.com/dotnetchina/Furion/commit/679319ddbde84af6977114a5823f43fd3c006b96) - Fixed Fixed the issue where `HttpContent(Body)` could not be tampered with when converting `HttpContext` in `HTTP` remote requests 4.9.7.89 ⏱️2025.06.20 [ca7bfb5](https://gitee.com/dotnetchina/Furion/commit/ca7bfb5e1a3da863c84e03fbb8ce8e13aee7d101) - Fixed Fixed the issue where the `HTTP` remote request Profiler did not support `Blazor WebAssembly` applications 4.9.7.69 ⏱️2025.05.22 [c257ed0](https://gitee.com/dotnetchina/Furion/commit/c257ed031236666d1e4bcb458c16f02c501d2175) - Fixed Fixed the formatting disorder issue when manually printing with the `HTTP` remote request Profiler 4.9.7.52 ⏱️2025.04.27 [14261e4](https://gitee.com/dotnetchina/Furion/commit/14261e4fb2ff77c88b805ae8ac771bebfef51885) - Fixed Fixed the issue where `HTTP` remote requests could not remove `User-Agent` via `RemoveHeaders` 4.9.7.44 ⏱️2025.04.17 [4d98d60](https://gitee.com/dotnetchina/Furion/commit/4d98d6053f7b05c73b6d60d5284c040538f18789) - Fixed Fixed the exception when the request address was an `IP` address while `IPv4` was force-enabled in `HTTP` remote requests 4.9.7.28 ⏱️2025.03.23 [1d57a07](https://gitee.com/dotnetchina/Furion/commit/1d57a0732531a69ea1ed6774add60b24e86f8b7d) - Fixed Fixed the parsing failure in `HTTP` remote requests when a `URL` parameter value contained multiple `=` signs 4.9.7.24 ⏱️2025.03.13 [5c9270f](https://gitee.com/dotnetchina/Furion/commit/5c9270f39721d5b25c08d081f27377f7bdb4bee5) - Fixed Fixed the issue where `HTTP` remote requests had no effect when no query parameters were set but a list of query parameters to remove was set 4.9.7.21 ⏱️2025.03.03 [7b3335e](https://gitee.com/dotnetchina/Furion/commit/7b3335e8af30509aa1f0465a881693bd3b6f114b) - Fixed Fixed the `CancellationToken` handling issue in `HTTP` remote request file upload/download, long polling, and `Server-Sent Events` 4.9.7.16 ⏱️2025.02.28 [21c1f06](https://gitee.com/dotnetchina/Furion/commit/21c1f06cfed1f892eb5ee7bf91989103a3e922d5) - Fixed Fixed the null reference exception when the `HTTP` remote request client configured a base address 4.9.7.16 ⏱️2025.02.28 [21c1f06](https://gitee.com/dotnetchina/Furion/commit/21c1f06cfed1f892eb5ee7bf91989103a3e922d5) - Fixed Fixed the issue where the `HTTP` remote request Profiler did not print requests that did not actually succeed but were ensured to be successful 4.9.7.10 ⏱️2025.02.22 [82b4d81](https://gitee.com/dotnetchina/Furion/commit/82b4d81ae60f1918f06cc28b780902f7096c4fa4) - Fixed Fixed the issue where `HTTP` remote request redirect operations handled the request method and request content incorrectly 4.9.7.2 ⏱️2025.01.26 [c326cf3](https://gitee.com/dotnetchina/Furion/commit/c326cf3c536f29dd29198477990708590b2aeeed) - Fixed Fixed the issue where `HTTP` remote requests could duplicate query parameters when encountering redirects 4.9.7 ⏱️2025.01.23 [0e64da5](https://gitee.com/dotnetchina/Furion/commit/0e64da5fe468a0c925bc9bb21985dd119fe9834c) - **Other Changes** - Improved the `HTTP` remote request analysis tool to display `CURL` command strings 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Removed the log output of the `HTTP` remote request file upload and download managers 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Improved the `HTTP` remote request analysis tool, benchmarking against external packet capture tools such as `Fiddler/Wireshark` 4.9.9.63 ⏱️2026.08.05 [af1de91](https://gitee.com/dotnetchina/Furion/commit/af1de91d28b7a8a7aa2d38b4c4ecbcc26e464f94) - Improved the `HTTP` remote request analysis tool and the default `boundary` format for sending form data 4.9.9.61 ⏱️2026.08.04 [45d88e1](https://gitee.com/dotnetchina/Furion/commit/45d88e123a3a9cebcd97225324f169b49a3bfabd) - Improved the performance of `ETag` caching and redirection handling in `HTTP` remote requests 4.9.9.60 ⏱️2026.08.04 [1f99ba2](https://gitee.com/dotnetchina/Furion/commit/1f99ba28110e7c4d5575c34a2922fbe70b73fd2a) - Improved the issue where `QPS` calculation had large errors due to performance issues in `HTTP` remote request stress testing 4.9.9.56 ⏱️2026.08.02 [635af70](https://gitee.com/dotnetchina/Furion/commit/635af70bd71cf31bc4f6ba87923dfaf2ec2df50c) - Improved the `JSON` serialization feature of `HTTP` remote requests 4.9.9.51 ⏱️2026.08.01 [85f6dea](https://gitee.com/dotnetchina/Furion/commit/85f6dea02f34dac9f257ac831cbc05d09daca7b4) [#IK5MUW](https://gitee.com/dotnetchina/Furion/issues/IK5MUW) - Improved sending raw string content in `HTTP` remote requests 4.9.9.46 ⏱️2026.07.29 [22c8edb](https://gitee.com/dotnetchina/Furion/commit/22c8edbe5e249fe10ad011d0dc8982b61278422c) - Improved the path parameter template syntax of `HTTP` remote requests 4.9.9.41 ⏱️2026.07.24 [40b7d08](https://gitee.com/dotnetchina/Furion/commit/40b7d08bd5b97de971e85fdab31e221a4f38ccb0) - Improved the declarative data validation feature of `HTTP` remote requests 4.9.9.35 ⏱️2026.07.19 [3e91e0c](https://gitee.com/dotnetchina/Furion/commit/3e91e0c22ac9d5966fcea563f3fb4f4bb4c8ba3d) - Improved the low-level core logic for sending requests in `HTTP` remote requests 4.9.9.34 ⏱️2026.07.18 [3ae3f64](https://gitee.com/dotnetchina/Furion/commit/3ae3f64e8f42fd91e21707b6847550190a50f040) - Improved the default `Content-Type` inference logic of `HTTP` remote requests 4.9.9.15 ⏱️2026.07.06 [5495a09](https://gitee.com/dotnetchina/Furion/commit/5495a096f2f9dd98090ea0561a36afd8be8ebd38) - Improved `HTTP` remote request analysis logs to reduce memory usage 4.9.8.98 ⏱️2026.06.17 [dc8f573](https://gitee.com/dotnetchina/Furion/commit/dc8f5738d79be3ca69b3fd5fada3768655d99742) - Improved `HTTP` remote request analysis logs by merging log output 4.9.8.77 ⏱️2026.05.20 [fb8428c](https://gitee.com/dotnetchina/Furion/commit/fb8428cf8e03e4cd341bd69a4fae0767ea3c2cdb) - Improved the notification frequency of file download transfer progress in `HTTP` remote requests 4.9.8.37 ⏱️2026.04.11 [49223d6](https://gitee.com/dotnetchina/Furion/commit/49223d6d0569b7b5136df9db9f4e7b95c38d9b94) - Improved the timeout of `HTTP` remote requests, supporting setting it to `null` 4.9.8.22 ⏱️2026.03.09 [537400c](https://gitee.com/dotnetchina/Furion/commit/537400c15879548cad16ffda301fc1962a93e88b) - Improved the `.SetOnPreSendRequest` method of the `HTTP` remote request builder to support multiple calls 4.9.7.244 ⏱️2026.01.09 [e42e6b0](https://gitee.com/dotnetchina/Furion/commit/e42e6b0a58c72c526120c125189d3aab163f58e9) - Improved simplified custom configuration of the `HTTP` remote request static class HttpRemoteClient 4.9.7.221 ⏱️2025.12.06 [ca3d6f6](https://gitee.com/dotnetchina/Furion/commit/ca3d6f6a5546fc9d14a135a8fb0986ed8092c024) - Improved the issue where sending text content did not support setting `Content-Type` in `HTTP` remote requests 4.9.7.218 ⏱️2025.12.03 [9d6cdd1](https://gitee.com/dotnetchina/Furion/commit/9d6cdd124b44886f68b6cccab119176640c18492) - Improved the `HTTP` remote request logging system to facilitate accurate error location in production environments 4.9.7.212 ⏱️2025.11.26 [c40570b](https://gitee.com/dotnetchina/Furion/commit/c40570b72b604d947dca14f6c2787385466fcd39) - Improved the `WebSocket` client constructor option parameters of `HTTP` remote requests 4.9.7.130 ⏱️2025.10.15 [ca85e8e](https://gitee.com/dotnetchina/Furion/commit/ca85e8e846f6017eaac72b1b7c51dfab8181eefd) - Improved the file download feature of `HTTP` remote requests, adding the `FileTransferResult` return value 4.9.7.128 ⏱️2025.09.30 [9311ee3](https://gitee.com/dotnetchina/Furion/commit/9311ee357e134cc7ba89369b45cb3ef13929d823) [04010e2](https://gitee.com/dotnetchina/Furion/commit/04010e282cc8b58becd4e7c47ae6d516b304d455) - Improved the console progress bar time format for file upload and download in `HTTP` remote requests 4.9.7.117 ⏱️2025.09.02 [665a453](https://gitee.com/dotnetchina/Furion/commit/665a453227aecac21492f1a32435f8448550d2c2) - Improved the console progress bar effect printed during file upload and download in `HTTP` remote requests 4.9.7.114 ⏱️2025.08.29 [3204e72](https://gitee.com/dotnetchina/Furion/commit/3204e721c10ea60a523af39ca0578dd040d372ff) - Improved the multipart form setting method (overload) of `HTTP` remote requests 4.9.7.99 ⏱️2025.07.19 [60b9260](https://gitee.com/dotnetchina/Furion/commit/60b92609029f0afd6707582429d617e7caae9dc8) - Improved the `HTTP` remote request analysis tool to automatically handle `Unicode` escaping 4.9.7.48 ⏱️2025.04.23 [f0a01d6](https://gitee.com/dotnetchina/Furion/commit/f0a01d6b4524670bf3e38f93338ebee19a60ddd3) - Improved the `HTTP` remote request analysis tool to print the sizes of request and response content 4.9.7.47 ⏱️2025.04.20 [cf7956e](https://gitee.com/dotnetchina/Furion/commit/cf7956e227d978a05c6e0d294766aa45a681f1b9) - Changed the default `User-Agent` of `HTTP` remote requests to match the `Edge` browser (version `133`) `User-Agent` 4.9.7.18 ⏱️2025.03.01 [b6ba52b](https://gitee.com/dotnetchina/Furion/commit/b6ba52bea7f40098a101811c5eb403456139de3c) - Changed **the long polling property (event) type of `HTTP` remote requests from `Func?` to `Func`** 4.9.7.17 ⏱️2025.02.28 [050e64f](https://gitee.com/dotnetchina/Furion/commit/050e64f0a27c782f360ff3a78ef27f841c6260e6) - Changed **the `onMessage` property type of `ServerSentEvents` in `HTTP` remote requests from `Func?` to `Func`** 4.9.7.14 ⏱️2025.02.26 [5ef4b13](https://gitee.com/dotnetchina/Furion/commit/5ef4b13c522a824822266dbcf6ad91d8f65e701a) - Changed **the automatic `Host` request header of `HTTP` remote requests to `false`, i.e., disabled by default** 4.9.6.20 ⏱️2024.12.27 [4998e13](https://gitee.com/dotnetchina/Furion/commit/4998e139dec691a154bfbd52463c5fd5e33f6141) - Improved `HTTP` remote requests enable automatic `Host` request header setting by default 4.9.6.16 ⏱️2024.12.17 [61afe9a](https://gitee.com/dotnetchina/Furion/commit/61afe9a28cad036ac51b3a457f865cad36711837) - Improved `HTTP` remote requests set `Boundary` by default when submitting form data 4.9.6.16 ⏱️2024.12.17 [61afe9a](https://gitee.com/dotnetchina/Furion/commit/61afe9a28cad036ac51b3a457f865cad36711837) - Improved the `RateLimitedStream` with application rate limiting based on the token bucket algorithm in `HTTP` remote requests 4.9.6.10 ⏱️2024.12.03 [f0ee8af](https://gitee.com/dotnetchina/Furion/commit/f0ee8af32aed3e94d778b04be626bcdb069ec46f) - Improved the performance of the `HTTP` remote request analysis tool, outputting only `5KB` of content by default when printing 4.9.6.9 ⏱️2024.12.02 [88afe64](https://gitee.com/dotnetchina/Furion/commit/88afe64ec45921f1a53fe98edba9b12b7b5f2a9c) - Improved the `HTTP` remote request analysis tool, providing request content and response content printing 4.9.6.7 ⏱️2024.12.02 [250ea66](https://gitee.com/dotnetchina/Furion/commit/250ea66c6c9fff98480c79a26e4a5ef629b99153) - Improved the `HTTP` remote request analysis tool, providing more detailed printing 4.9.6.4 ⏱️2024.11.29 [6782110](https://gitee.com/dotnetchina/Furion/commit/6782110d073a6193c431023b8c40c7ad4fb1129e) - Improved Improved the `HTTP` remote request analysis tool to support displaying the `CURL` command string 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Removed Removed the log output of the `HTTP` remote request file upload and download managers 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Improved Improved the `HTTP` remote request analysis tool to benchmark against external packet capture tools such as `Fiddler/Wireshark` 4.9.9.63 ⏱️2026.08.05 [af1de91](https://gitee.com/dotnetchina/Furion/commit/af1de91d28b7a8a7aa2d38b4c4ecbcc26e464f94) - Improved Improved the default `boundary` format of the `HTTP` remote request analysis tool and form data sending 4.9.9.61 ⏱️2026.08.04 [45d88e1](https://gitee.com/dotnetchina/Furion/commit/45d88e123a3a9cebcd97225324f169b49a3bfabd) - Improved Improved the performance of `ETag` caching and redirect handling in `HTTP` remote requests 4.9.9.60 ⏱️2026.08.04 [1f99ba2](https://gitee.com/dotnetchina/Furion/commit/1f99ba28110e7c4d5575c34a2922fbe70b73fd2a) - Improved Improved the large `QPS` calculation error caused by performance issues in `HTTP` remote request stress testing 4.9.9.56 ⏱️2026.08.02 [635af70](https://gitee.com/dotnetchina/Furion/commit/635af70bd71cf31bc4f6ba87923dfaf2ec2df50c) - Improved Improved the `JSON` serialization feature of `HTTP` remote requests 4.9.9.51 ⏱️2026.08.01 [85f6dea](https://gitee.com/dotnetchina/Furion/commit/85f6dea02f34dac9f257ac831cbc05d09daca7b4) [#IK5MUW](https://gitee.com/dotnetchina/Furion/issues/IK5MUW) - Improved Improved sending raw string content in `HTTP` remote requests 4.9.9.46 ⏱️2026.07.29 [22c8edb](https://gitee.com/dotnetchina/Furion/commit/22c8edbe5e249fe10ad011d0dc8982b61278422c) - Improved Improved the path parameter template syntax of `HTTP` remote requests 4.9.9.41 ⏱️2026.07.24 [40b7d08](https://gitee.com/dotnetchina/Furion/commit/40b7d08bd5b97de971e85fdab31e221a4f38ccb0) - Improved Improved the declarative data validation feature of `HTTP` remote requests 4.9.9.35 ⏱️2026.07.19 [3e91e0c](https://gitee.com/dotnetchina/Furion/commit/3e91e0c22ac9d5966fcea563f3fb4f4bb4c8ba3d) - Improved Improved the underlying core request-sending logic of `HTTP` remote requests 4.9.9.34 ⏱️2026.07.18 [3ae3f64](https://gitee.com/dotnetchina/Furion/commit/3ae3f64e8f42fd91e21707b6847550190a50f040) - Improved Improved the default `Content-Type` inference logic of `HTTP` remote requests 4.9.9.15 ⏱️2026.07.06 [5495a09](https://gitee.com/dotnetchina/Furion/commit/5495a096f2f9dd98090ea0561a36afd8be8ebd38) - Improved Improved the `HTTP` remote request analysis log to reduce memory usage 4.9.8.98 ⏱️2026.06.17 [dc8f573](https://gitee.com/dotnetchina/Furion/commit/dc8f5738d79be3ca69b3fd5fada3768655d99742) - Improved Improved the `HTTP` remote request analysis log by merging log output 4.9.8.77 ⏱️2026.05.20 [fb8428c](https://gitee.com/dotnetchina/Furion/commit/fb8428cf8e03e4cd341bd69a4fae0767ea3c2cdb) - Improved Improved the notification frequency of the file download transfer progress in `HTTP` remote requests 4.9.8.37 ⏱️2026.04.11 [49223d6](https://gitee.com/dotnetchina/Furion/commit/49223d6d0569b7b5136df9db9f4e7b95c38d9b94) - Improved Improved the `HTTP` remote request timeout to support being set to `null` 4.9.8.22 ⏱️2026.03.09 [537400c](https://gitee.com/dotnetchina/Furion/commit/537400c15879548cad16ffda301fc1962a93e88b) - Improved Improved the `.SetOnPreSendRequest` method of the `HTTP` remote request builder to support multiple calls 4.9.7.244 ⏱️2026.01.09 [e42e6b0](https://gitee.com/dotnetchina/Furion/commit/e42e6b0a58c72c526120c125189d3aab163f58e9) - Improved Simplified custom configuration of the `HTTP` remote request static class HttpRemoteClient 4.9.7.221 ⏱️2025.12.06 [ca3d6f6](https://gitee.com/dotnetchina/Furion/commit/ca3d6f6a5546fc9d14a135a8fb0986ed8092c024) - Improved Improved the issue where sending text content in `HTTP` remote requests did not support setting `Content-Type` 4.9.7.218 ⏱️2025.12.03 [9d6cdd1](https://gitee.com/dotnetchina/Furion/commit/9d6cdd124b44886f68b6cccab119176640c18492) - Improved Improved the `HTTP` remote request logging system to facilitate accurate error location in production environments 4.9.7.212 ⏱️2025.11.26 [c40570b](https://gitee.com/dotnetchina/Furion/commit/c40570b72b604d947dca14f6c2787385466fcd39) - Improved Improved the constructor options parameters of the `HTTP` remote request `WebSocket` client 4.9.7.130 ⏱️2025.10.15 [ca85e8e](https://gitee.com/dotnetchina/Furion/commit/ca85e8e846f6017eaac72b1b7c51dfab8181eefd) - Improved Improved the `HTTP` remote request file download feature, adding the `FileTransferResult` return value 4.9.7.128 ⏱️2025.09.30 [9311ee3](https://gitee.com/dotnetchina/Furion/commit/9311ee357e134cc7ba89369b45cb3ef13929d823) [04010e2](https://gitee.com/dotnetchina/Furion/commit/04010e282cc8b58becd4e7c47ae6d516b304d455) - Improved Improved the time format of the console progress bar for file upload and download in `HTTP` remote requests 4.9.7.117 ⏱️2025.09.02 [665a453](https://gitee.com/dotnetchina/Furion/commit/665a453227aecac21492f1a32435f8448550d2c2) - Improved Improved the effect of printing the upload and download progress bar to the console in `HTTP` remote requests 4.9.7.114 ⏱️2025.08.29 [3204e72](https://gitee.com/dotnetchina/Furion/commit/3204e721c10ea60a523af39ca0578dd040d372ff) - Improved Improved the `HTTP` remote request multipart form setting method (overload) 4.9.7.99 ⏱️2025.07.19 [60b9260](https://gitee.com/dotnetchina/Furion/commit/60b92609029f0afd6707582429d617e7caae9dc8) - Improved Improved the `HTTP` remote request analysis tool to automatically handle `Unicode` escapes 4.9.7.48 ⏱️2025.04.23 [f0a01d6](https://gitee.com/dotnetchina/Furion/commit/f0a01d6b4524670bf3e38f93338ebee19a60ddd3) - Improved Improved the `HTTP` remote request analysis tool to support printing the sizes of request and response content 4.9.7.47 ⏱️2025.04.20 [cf7956e](https://gitee.com/dotnetchina/Furion/commit/cf7956e227d978a05c6e0d294766aa45a681f1b9) - Adjusted Adjusted the default `User-Agent` of `HTTP` remote requests to match the `User-Agent` of the `Edge` browser (version `133`) 4.9.7.18 ⏱️2025.03.01 [b6ba52b](https://gitee.com/dotnetchina/Furion/commit/b6ba52bea7f40098a101811c5eb403456139de3c) - Adjusted **`HTTP` remote request long polling property (event) type changed from `Func?` -> `Func`** 4.9.7.17 ⏱️2025.02.28 [050e64f](https://gitee.com/dotnetchina/Furion/commit/050e64f0a27c782f360ff3a78ef27f841c6260e6) - Adjusted **`HTTP` remote request `ServerSentEvents` `onMessage` property type changed from `Func?` -> `Func`** 4.9.7.14 ⏱️2025.02.26 [5ef4b13](https://gitee.com/dotnetchina/Furion/commit/5ef4b13c522a824822266dbcf6ad91d8f65e701a) - Adjusted **`HTTP` remote request automatic `Host` request header setting is `false`, i.e., disabled by default** 4.9.6.20 ⏱️2024.12.27 [4998e13](https://gitee.com/dotnetchina/Furion/commit/4998e139dec691a154bfbd52463c5fd5e33f6141) - Improved Improved the `HTTP` remote request to enable automatic request `Host` header setting by default 4.9.6.16 ⏱️2024.12.17 [61afe9a](https://gitee.com/dotnetchina/Furion/commit/61afe9a28cad036ac51b3a457f865cad36711837) - Improved Improved the `HTTP` remote request to set the `Boundary` by default when submitting form data 4.9.6.16 ⏱️2024.12.17 [61afe9a](https://gitee.com/dotnetchina/Furion/commit/61afe9a28cad036ac51b3a457f865cad36711837) - Improved Improved the `HTTP` remote request `RateLimitedStream`, a stream with application rate limiting, based on the token bucket algorithm 4.9.6.10 ⏱️2024.12.03 [f0ee8af](https://gitee.com/dotnetchina/Furion/commit/f0ee8af32aed3e94d778b04be626bcdb069ec46f) - Improved Improved the `HTTP` remote request analysis tool performance to output only `5KB` of content by default when printing content 4.9.6.9 ⏱️2024.12.02 [88afe64](https://gitee.com/dotnetchina/Furion/commit/88afe64ec45921f1a53fe98edba9b12b7b5f2a9c) - Improved Improved the `HTTP` remote request analysis tool to provide request content and response content printing 4.9.6.7 ⏱️2024.12.02 [250ea66](https://gitee.com/dotnetchina/Furion/commit/250ea66c6c9fff98480c79a26e4a5ef629b99153) - Improved Improved the `HTTP` remote request analysis tool to provide more detailed printing 4.9.6.4 ⏱️2024.11.29 [6782110](https://gitee.com/dotnetchina/Furion/commit/6782110d073a6193c431023b8c40c7ad4fb1129e) - Improved Improved the `HTTP` remote request Profiler, which now supports displaying the `CURL` command string 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - Improved Improved the `HTTP` remote request Profiler to benchmark against external packet capture tools such as `Fiddler/Wireshark` 4.9.9.63 ⏱️2026.08.05 [af1de91](https://gitee.com/dotnetchina/Furion/commit/af1de91d28b7a8a7aa2d38b4c4ecbcc26e464f94) - Improved Improved the `HTTP` remote request Profiler and the default `boundary` format for sending form data 4.9.9.61 ⏱️2026.08.04 [45d88e1](https://gitee.com/dotnetchina/Furion/commit/45d88e123a3a9cebcd97225324f169b49a3bfabd) - Improved Improved the performance of `HTTP` remote request `ETag` caching and redirect handling 4.9.9.60 ⏱️2026.08.04 [1f99ba2](https://gitee.com/dotnetchina/Furion/commit/1f99ba28110e7c4d5575c34a2922fbe70b73fd2a) - Improved Improved `HTTP` remote request stress testing, which had large `QPS` calculation errors due to performance issues 4.9.9.56 ⏱️2026.08.02 [635af70](https://gitee.com/dotnetchina/Furion/commit/635af70bd71cf31bc4f6ba87923dfaf2ec2df50c) - Improved Improved the `HTTP` remote request `JSON` serialization functionality 4.9.9.51 ⏱️2026.08.01 [85f6dea](https://gitee.com/dotnetchina/Furion/commit/85f6dea02f34dac9f257ac831cbc05d09daca7b4) [#IK5MUW](https://gitee.com/dotnetchina/Furion/issues/IK5MUW) - Improved Improved `HTTP` remote request sending of raw string content 4.9.9.46 ⏱️2026.07.29 [22c8edb](https://gitee.com/dotnetchina/Furion/commit/22c8edbe5e249fe10ad011d0dc8982b61278422c) - Improved Improved the `HTTP` remote request path parameter template syntax 4.9.9.41 ⏱️2026.07.24 [40b7d08](https://gitee.com/dotnetchina/Furion/commit/40b7d08bd5b97de971e85fdab31e221a4f38ccb0) - Improved Improved the `HTTP` remote request declarative data validation functionality 4.9.9.35 ⏱️2026.07.19 [3e91e0c](https://gitee.com/dotnetchina/Furion/commit/3e91e0c22ac9d5966fcea563f3fb4f4bb4c8ba3d) - Improved Improved the core logic of the underlying request sending in `HTTP` remote requests 4.9.9.34 ⏱️2026.07.18 [3ae3f64](https://gitee.com/dotnetchina/Furion/commit/3ae3f64e8f42fd91e21707b6847550190a50f040) - Improved Improved the `HTTP` remote request default `Content-Type` inference logic 4.9.9.15 ⏱️2026.07.06 [5495a09](https://gitee.com/dotnetchina/Furion/commit/5495a096f2f9dd98090ea0561a36afd8be8ebd38) - Improved Improved `HTTP` remote request Profiler logs to reduce memory usage 4.9.8.98 ⏱️2026.06.17 [dc8f573](https://gitee.com/dotnetchina/Furion/commit/dc8f5738d79be3ca69b3fd5fada3768655d99742) - Improved Improved `HTTP` remote request Profiler logs by merging log output 4.9.8.77 ⏱️2026.05.20 [fb8428c](https://gitee.com/dotnetchina/Furion/commit/fb8428cf8e03e4cd341bd69a4fae0767ea3c2cdb) - Improved Improved the notification frequency of `HTTP` remote request file download transfer progress 4.9.8.37 ⏱️2026.04.11 [49223d6](https://gitee.com/dotnetchina/Furion/commit/49223d6d0569b7b5136df9db9f4e7b95c38d9b94) - Improved Improved the `HTTP` remote request timeout, which now supports being set to `null` 4.9.8.22 ⏱️2026.03.09 [537400c](https://gitee.com/dotnetchina/Furion/commit/537400c15879548cad16ffda301fc1962a93e88b) - Improved Improved the `HTTP` remote request builder's `.SetOnPreSendRequest` method to support multiple invocations 4.9.7.244 ⏱️2026.01.09 [e42e6b0](https://gitee.com/dotnetchina/Furion/commit/e42e6b0a58c72c526120c125189d3aab163f58e9) - Improved Simplified the custom configuration of the `HTTP` remote request static class HttpRemoteClient 4.9.7.221 ⏱️2025.12.06 [ca3d6f6](https://gitee.com/dotnetchina/Furion/commit/ca3d6f6a5546fc9d14a135a8fb0986ed8092c024) - Improved Improved the issue where `HTTP` remote requests could not set `Content-Type` when sending text content 4.9.7.218 ⏱️2025.12.03 [9d6cdd1](https://gitee.com/dotnetchina/Furion/commit/9d6cdd124b44886f68b6cccab119176640c18492) - Improved Improved the `HTTP` remote request logging system to facilitate accurately locating errors in production environments 4.9.7.212 ⏱️2025.11.26 [c40570b](https://gitee.com/dotnetchina/Furion/commit/c40570b72b604d947dca14f6c2787385466fcd39) - Improved Improved the `HTTP` remote request `WebSocket` client constructor option parameters 4.9.7.130 ⏱️2025.10.15 [ca85e8e](https://gitee.com/dotnetchina/Furion/commit/ca85e8e846f6017eaac72b1b7c51dfab8181eefd) - Improved Improved the time format of the `HTTP` remote request file upload and download console progress bar 4.9.7.117 ⏱️2025.09.02 [665a453](https://gitee.com/dotnetchina/Furion/commit/665a453227aecac21492f1a32435f8448550d2c2) - Improved Improved the effect of printing the `HTTP` remote request file upload and download progress bar to the console 4.9.7.114 ⏱️2025.08.29 [3204e72](https://gitee.com/dotnetchina/Furion/commit/3204e721c10ea60a523af39ca0578dd040d372ff) - Improved Improved the `HTTP` remote request method for setting multipart forms (overloads) 4.9.7.99 ⏱️2025.07.19 [60b9260](https://gitee.com/dotnetchina/Furion/commit/60b92609029f0afd6707582429d617e7caae9dc8) - Improved Improved the `HTTP` remote request Profiler to automatically handle `Unicode` escapes 4.9.7.48 ⏱️2025.04.23 [f0a01d6](https://gitee.com/dotnetchina/Furion/commit/f0a01d6b4524670bf3e38f93338ebee19a60ddd3) - Improved Improved the `HTTP` remote request Profiler to support printing the sizes of request and response content 4.9.7.47 ⏱️2025.04.20 [cf7956e](https://gitee.com/dotnetchina/Furion/commit/cf7956e227d978a05c6e0d294766aa45a681f1b9) - Adjusted Adjusted the `HTTP` remote request default `User-Agent` to match the `User-Agent` of the `Edge` browser (version `133`) 4.9.7.18 ⏱️2025.03.01 [b6ba52b](https://gitee.com/dotnetchina/Furion/commit/b6ba52bea7f40098a101811c5eb403456139de3c) - Adjusted **`HTTP` remote request automatic `Host` request header setting changed to `false`, i.e. disabled by default** 4.9.6.20 ⏱️2024.12.27 [4998e13](https://gitee.com/dotnetchina/Furion/commit/4998e139dec691a154bfbd52463c5fd5e33f6141) - Improved Improved `HTTP` remote requests to enable automatic `Host` request header setting by default 4.9.6.16 ⏱️2024.12.17 [61afe9a](https://gitee.com/dotnetchina/Furion/commit/61afe9a28cad036ac51b3a457f865cad36711837) - Improved Improved `HTTP` remote requests to set `Boundary` by default when submitting form data 4.9.6.16 ⏱️2024.12.17 [61afe9a](https://gitee.com/dotnetchina/Furion/commit/61afe9a28cad036ac51b3a457f865cad36711837) - Improved Improved the `HTTP` remote request `RateLimitedStream`, an application-rate-limited stream based on the token bucket algorithm 4.9.6.10 ⏱️2024.12.03 [f0ee8af](https://gitee.com/dotnetchina/Furion/commit/f0ee8af32aed3e94d778b04be626bcdb069ec46f) - Improved Improved the `HTTP` remote request Profiler performance, which by default outputs only `5KB` of content when printing 4.9.6.9 ⏱️2024.12.02 [88afe64](https://gitee.com/dotnetchina/Furion/commit/88afe64ec45921f1a53fe98edba9b12b7b5f2a9c) - Improved Improved the `HTTP` remote request Profiler to provide request content and response content printing 4.9.6.7 ⏱️2024.12.02 [250ea66](https://gitee.com/dotnetchina/Furion/commit/250ea66c6c9fff98480c79a26e4a5ef629b99153) - Improved Improved the `HTTP` remote request Profiler to provide more detailed printing 4.9.6.4 ⏱️2024.11.29 [6782110](https://gitee.com/dotnetchina/Furion/commit/6782110d073a6193c431023b8c40c7ad4fb1129e) --- # HttpAgent Released: Built-in Industrial-Grade HTTP Traffic Inspection Engine > Source: https://http.furion.net/en/blog/hello-httpagent/ We are excited to announce the official release of **HttpAgent** — a high-performance, flexible, and easy-to-use `HTTP` open-source library for `.NET 8+`, providing comprehensive support for file transfer, polling, testing tools, real-time communication, request management, media type handling, `MessagePack`, Declarative Requests, and more. ## Why HttpAgent What is the most painful part of integrating third-party `API`s? **You cannot see exactly what your request sends or what the response returns.** The traditional approach is to spin up Fiddler or Wireshark, configure proxies, install certificates, and filter traffic — five minutes of debugging, thirty minutes of setup. HttpAgent ships with an industrial-grade `HTTP` traffic inspection engine (`Profiler`) that renders the complete request/response traffic directly in the console: ```cs showLineNumbers {1,3} await httpRemoteService.GetAsync("https://furion.net/", builder => builder.Profiler()); // Enable request inspection with one line ``` The console prints request headers, response headers, status codes, durations, and every other detail — no external tools required. > **Production Note** By default `Profiler` displays at most `5KB` of content, and it is recommended to disable it in production to avoid performance overhead. ## Core Capabilities at a Glance - **All request verbs**: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `TRACE`, `OPTIONS`, `QUERY`; - **File transfer**: upload/download, real-time progress, multi-threaded chunked downloads; - **Real-time communication**: `SSE`, `WebSocket`, standard/long polling; - **Resilience**: retries (exponential backoff), timeout control, exception suppression, extensible circuit breaker & fallback; - **Automatic Token management**: auto refresh, auto injection, automatic retry on `401`; - **Quotas & caching**: daily/weekly/monthly/lifetime quota windows, `ETag` response caching; - **Declarative Requests**: interfaces + attribute annotations, with inheritance and object-oriented design; - **cURL import**: fire requests directly from cURL command strings; - **Stress testing**: built-in stress-testing tools with automatically generated reports. ## Up and Running in One Minute ```bash dotnet add package HttpAgent ``` ```cs showLineNumbers {1,6} builder.Services.AddHttpRemote(); // Register the service public class YourService(IHttpRemoteService httpRemoteService) { public async Task GetContent() => await httpRemoteService.GetAsStringAsync("https://furion.net/"); } ``` For the full progressive learning path, start with [HTTP Remote Request Overview](/en/docs/getting-started/intro/). ## Acknowledgments The project is hosted publicly on [GitHub](https://github.com/monksoul/HttpAgent) — stars, issues, and PRs are all welcome. We will keep publishing tutorials and best practices, so stay tuned to the blog. --- # Stop Tormenting HttpClient! Meet HttpAgent — Redefining .NET HTTP Requests > Source: https://http.furion.net/en/blog/stop-suffering-with-httpclient/ > Make every HTTP request transparent. Every .NET developer has a love-hate memory of HttpClient: - For every request you rewrite the same boilerplate: `new HttpClient()`, URL concatenation, query strings, headers, content types; - One careless `using` around a `new HttpClient()` and production runs out of sockets — the moment load testing starts, it dies; - Timeouts, retries, backoff, circuit breaking? Sorry — either hand-roll them or pull in the whole Polly kit; - A colleague drops a curl one-liner on you "to reproduce" and you translate it line by line into C#, then debug your translation; - Want to see what was actually sent and received? Open Fiddler, configure proxies, install certificates, filter traffic — half an hour of setup for five minutes of debugging; - File uploads and downloads, progress bars, SSE, long polling, WebSocket, SOAP, OData, token auto-refresh… each one is another wheel to reinvent. In the end you realize: **the hard part was never "sending a request" — it's doing it right, doing it completely, and keeping it maintainable.** Today's protagonist is here to end that suffering — **HttpAgent**. ## Meet HttpAgent in one sentence HttpAgent is a high-performance, flexible and easy-to-use .NET HTTP open-source library built around one core idea: **make every HTTP request transparent.** A few numbers that are hard to say no to: - **Zero third-party dependencies**: one NuGet package, ready to go — no Polly, no Flurl, nothing; - **All platforms**: Console / Web / WASM / WinForms / WPF / MAUI, all on .NET 8+; - **Industrial-grade quality**: 98% test coverage, every release passes strict regression verification; - **Full feature toolkit**: retries, timeouts, circuit breaking, quotas, ETag caching, automatic Access Token management… all built in. ## Five styles, your choice HttpAgent's most addictive design decision: it splits "sending a request" into **five styles**. Pick whichever you like — mixing them in one project feels perfectly natural. ### Style one: verb methods, one-liners ```csharp // Fetch a website with one line var content = await httpRemoteService.GetAsStringAsync("https://api.example.com/"); // Generic overload returns strongly-typed results var user = await httpRemoteService.GetAsAsync("https://api.example.com/user/1"); ``` ### Style two: the builder, chain it up ```csharp var result = await httpRemoteService.SendAsync( HttpRequestBuilder.Post("https://api.example.com/login") .SetJsonContent(new { account = "admin", password = "123456" }) .SetTimeout(TimeSpan.FromSeconds(30)) .Profiler()); // Turn on the request profiler ``` ### Style three: declarative requests, interface = API ```csharp public interface IUserApi : IHttpDeclarative { [Get("https://api.example.com/user/{id}")] Task GetUserAsync(int id); [Post("https://api.example.com/user"), Profiler] Task CreateUserAsync([Body] User user); } ``` Register it, inject it, and **calling a remote API feels like calling a local method**. Path templates, query parameters, request bodies, forms, multipart uploads — all declared with attributes. The code is absurdly clean. ```csharp services.AddHttpRemote(builder => { builder.AddHttpDeclarative(); }); ``` ### Style four: send from cURL, paste and go You've been there: the third-party docs contain nothing but a curl command. Support sends it, QA sends it, even your boss sends it: "Just do it like this." You used to translate that curl into C# character by character. Now: ```csharp var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromCurl(""" curl -k -X POST 'https://api.example.com/order?query1=10&query2=hello' \ -H 'Content-Type: application/json' \ -d '{ "id": 1, "name": "sample" }' """)); ``` HttpAgent ships a built-in cURL parsing engine supporting `-X`, `-H`, `-d`, `-F`, `-u`, `--data-urlencode`, `--max-time`, `--http2` and more — file uploads, URL-encoded forms, Basic auth, plus custom flags you can extend yourself. **Paste the cURL in, let the engine take over.** Friendly to humans, and extremely friendly to AI — the curl commands an AI outputs no longer need manual translation into code. ### Style five: send from JSON, config is the request If chained calls still feel verbose, write the request as JSON: ```csharp var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://api.example.com/order", "method": "POST", "queries": { "page": 1, "size": 10 }, "headers": { "Content-Type": "application/json" }, "auth": { "type": "bearer", "token": "xxx" }, "timeout": 5000, "data": { "id": 1, "name": "sample" } } """)); ``` URL, base address, query parameters, headers, cookies, timeout, authentication (Bearer / Basic / Digest), body, multipart uploads, profiler switch — everything is JSON. **Request configs can live in databases, configuration files, or even be generated by AI on the fly.** ## One package, the entire HTTP engineering toolkit Many "lightweight" HTTP libraries are so light you end up rebuilding everything yourself. HttpAgent goes the opposite way: **one package with everything HTTP engineering needs built in — and zero third-party dependencies.** - **Resilience & fault tolerance**: retries, exponential backoff, timeouts, circuit breaking and fallback — one `SetRetry` call spins up a full resilience strategy; - **Automatic Access Token management**: fetch automatically, refresh on expiry, retry on `401`, inject into Header / Query / Cookie wherever you configure it — no more hand-written token timers when integrating with third-party platforms; - **Quotas & ETag caching**: daily / weekly / monthly / lifetime call-quota windows and `304` cache reuse — save bandwidth and prevent overruns; - **Real-time communication**: SSE, long polling, WebSocket and `IAsyncEnumerable` streaming responses, all included; - **File transfer**: upload/download, multi-threaded chunked downloads, progress callbacks, console progress bars, automatic RFC 2047 / RFC 5987 filename decoding; - **Gateway & proxying**: one-line `HttpContext` forwarding with built-in gateway capability; - **Testing & debugging**: request/response assertions, Mock testing, stress testing — built in; - **Every data format**: JSON, JSON Lines, MessagePack, XML, HTML, SOAP (WebService) and OData; - **Automatic decompression**: gzip / deflate / brotli / zstd responses decompressed for you. One example — a single line for "smart retry with exponential backoff": ```csharp HttpRequestBuilder.Get("https://api.example.com/order/1") .SetRetry(options => options .SetMaxRetries(3) .SetUseExponentialBackoff(true) .AddRetryStatusCodes(408, 429, 500, 502, 503, 504)); ``` ## Industrial-grade traffic inspection: black box to white box What hurts most when debugging third-party APIs? **You can't see.** Which headers were really sent? Is the body encoded correctly? Why is the response empty? HttpAgent has a built-in industrial-grade HTTP traffic inspection engine (Profiler). Append `.Profiler()` to any request and the console prints the complete, color-highlighted request and response: ```csharp await httpRemoteService.GetAsync("https://api.example.com/order/1", builder => builder.Profiler()); ``` Method, full URL, all request headers, body, status code, response headers, response body, elapsed time… everything at a glance. **Half an hour of packet capture becomes one line of code.** ## The most AI-friendly .NET HTTP library In 2026, AI-assisted coding is the daily routine. HttpAgent may well be the most AI-friendly .NET HTTP library: 1. **Native llms.txt support**: the official docs ship `llms.txt` and `llms-full.txt` (in both Chinese and English) so AI assistants like Claude Code and Codex can read the entire documentation in one pass — no page-by-page scraping. Drop `llms-full.txt` into your project's `CLAUDE.md` / `AGENTS.md` and AI-generated HttpAgent code just works. 2. **Declarative interfaces are an AI playground**: give the AI an interface definition and it will reliably generate a complete, attribute-annotated API client — highly readable, minimal review cost. 3. **cURL / JSON as code**: the curl commands and JSON configs an AI produces paste directly into runnable requests — zero translation cost. ## Up and running in three steps ```bash # 1. Install (any .NET 8+ app) dotnet add package HttpAgent # Web apps additionally get HttpContext forwarding dotnet add package HttpAgent.AspNetCore ``` ```csharp // 2. Register the service builder.Services.AddHttpRemote(); ``` ```csharp // 3. Go var user = await httpRemoteService.GetAsAsync("https://api.example.com/user/1"); ``` From install to first request in under three minutes. ## Finally If you've ever wrestled with a curl command late at night, chased HttpClient socket exhaustion in production, or stacked three thousand lines of wrapper code around "a simple request feature" — give HttpAgent a chance. And give yourself one too. - **Repository (GitHub)**: https://github.com/monksoul/HttpAgent - **NuGet**: https://www.nuget.org/packages/HttpAgent Make every HTTP request transparent. --- # Stop Digging Through Docs! HttpAgent Launches the Workshop + AI Assistant Duo — HTTP Code, Now WYSIWYG > Source: https://http.furion.net/en/blog/workshop-ai-assistant/ > Make every HTTP request transparent. Every .NET developer keeps a few browser tabs open permanently: - The HTTP library docs, endlessly searching for API names; - Fiddler, trying to see what the request actually sent; - A search engine: "how to upload a file with xxx", "how to set a timeout in xxx"… and there goes the afternoon. Don't blame yourself. **The problem was never you — it's that libraries give you the tool, but nobody ever helped with the two most time-consuming parts: writing the code and debugging it.** HttpClient didn't solve it. Refit didn't solve it. RestSharp didn't solve it. They all focused on "how to send a request" and left "how to write it correctly, completely and transparently" to you. Today, HttpAgent introduces two new species: the **Workshop** and the **HttpAgent Assistant**. ## Pain point 1: sample code is dead, your parameters never match The docs always show `GetAsStringAsync("https://example.com")`, but what you need is a real request with 8 query parameters, 3 custom headers, a 5-second timeout, 2 retries and the Profiler enabled. So you copy the template → change the parameters → miss a comma → fix the build → change them again. A request that should take five minutes eats half an hour. ## Solution 1: the Workshop — a visual code generator, WYSIWYG Open the [Workshop](/workshop/) and you're facing not a wall of text, but a live form: - Pick a method (GET / POST / PUT / DELETE… all 9 verbs); - Fill in the URL, query parameters, headers, body and content type; - Toggle the return type, timeout & retries, frozen parameters, the Profiler, digest authentication… The moment you finish configuring, **five code styles are generated simultaneously** — copy whichever you like: 1. **Builder** — the fluent style; 2. **Request predicate** — the one-liner; 3. **cURL command** — export it backwards and send it to a teammate to reproduce; 4. **Declarative interface** — interfaces + attributes, the team favorite; 5. **JSON config** — FromJson, done in one shot. Beginners never have to memorize an API name again; veterans skip mountains of boilerplate. **This is something Refit, RestSharp and even raw HttpClient have never offered.** ## Pain point 2: no matter how complete the docs are, you just need that one line Even the best documentation is a human searching for an answer: you must first know whether it lives in "2.13 Server-Sent Events" or "5.8 Timeout", then dig in, then locate three lines of code inside a thousand words of prose. At 2 a.m. during a bug hunt, this is torture. ## Solution 2: the HttpAgent Assistant — AI Q&A grounded in the official docs At the bottom-right corner of the HttpAgent site now lives an assistant whose **answers come 100% from the official documentation**: - Ask "how do I use SSE" → it returns the exact usage and code samples from the right section, with citation links at the end; - Ask "how do I contact the author / sponsor the project" → email, WeChat and the support page; - Ask "how does HttpAgent differ from Refit" → the full comparison, straight up; - Ask "who are you" → it knows better than anyone. It doesn't hallucinate from the open web — its retrieval source is the full site documentation (llms-full.txt, including every code sample), with an optional full-docs mode that feeds the entire manual to the model. **Bring your own DeepSeek or OpenAI API key; the key stays in your browser and calls the provider directly — no server in between.** ## Why can't the others do this? It's not that the technology is hard — the positioning is simply different: - Refit poured its energy into "interface as API"; its ecosystem has no official visual generator and no documentation AI; - RestSharp is a classic, battle-tested client — solid, but you still write and debug the code yourself; - Raw HttpClient isn't even a library — it's just the base. HttpAgent's answer: **the library makes requests right, the Workshop writes the code right, and the Assistant answers your questions right**. With the trio, the road from "can use" to "uses well" is paved for you. ## Try it now - 🛠️ [Workshop](/workshop/) — configure once, generate five ways, WYSIWYG; - 🤖 The HttpAgent Assistant at the bottom-right — a living dictionary of the official docs; - 📚 [Docs](/en/docs/) — 560+ bilingual pages; - ⭐ If it helps, give the project a [star on GitHub](https://github.com/monksoul/HttpAgent) so more people can stop digging through docs. > Make every HTTP request transparent — starting from writing the code. --- # Support & Sponsorship (contact the author — email & WeChat) > Source: https://http.furion.net/en/support/ To contact the author or sponsor the HttpAgent project: - Email: monksoul@outlook.com (mention your sponsorship intent — we reply promptly) - WeChat ID: ibaiqian (add a note "HttpAgent sponsor" when requesting) - Visit https://http.furion.net/en/support/ for sponsorship options and the thank-you list - Starring the project also helps: https://github.com/monksoul/HttpAgent