Overview
While HttpClient provides convenient shorthand methods like GetAsync and PostAsync, the SendAsync method is the preferred choice for granular control. By utilizing the HttpRequestMessage class, you can set individual headers per request, utilize non-standard methods (such as HEAD, OPTIONS, or PATCH), and dynamically switch HTTP methods.
Specifications (Input/Output)
- Input: Web API URL and a search keyword.
- Output: Extracts items matching the criteria from the retrieved JSON data and displays them in the console.
- Prerequisites: .NET 6.0 or higher. Uses
System.Net.Http.Json.
Basic Usage
- Create an instance of
HttpRequestMessage, specifying the HTTP method and the URL. - Configure request-specific headers or content as needed.
- Pass the message to
HttpClient.SendAsyncto transmit.
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com/data");
// Add a custom header specific to this request
request.Headers.Add("X-Custom-Header", "Value");
// Execute the request
using var response = await client.SendAsync(request);
Full Code Example
The following console application retrieves a list of posts from a test API (JSONPlaceholder) and displays only those whose titles contain a specific keyword. It demonstrates using SendAsync to attach a unique “Request ID” header to each call.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
class Program
{
// Reuse HttpClient as a singleton
private static readonly HttpClient _httpClient = new HttpClient();
static async Task Main()
{
// 1. Configure common headers for all requests
_httpClient.DefaultRequestHeaders.Accept.Clear();
_httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("MyTechBlogSearcher/1.0");
// API Endpoint
string url = "https://jsonplaceholder.typicode.com/posts";
string filterKeyword = "optio";
Console.WriteLine($"Building request for: {url}");
try
{
// 2. Construct the Request Message
using var request = new HttpRequestMessage(HttpMethod.Get, url);
// [Important] Add a unique header for this specific request
// This is added to request.Headers, not DefaultRequestHeaders
request.Headers.Add("X-Request-ID", Guid.NewGuid().ToString());
// 3. Send via SendAsync
using var response = await _httpClient.SendAsync(request);
if (response.StatusCode == HttpStatusCode.OK)
{
// Retrieve the response stream
using var stream = await response.Content.ReadAsStreamAsync();
// Configure JSON options for case-insensitive mapping
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
var articles = await JsonSerializer.DeserializeAsync<List<Article>>(stream, options);
Console.WriteLine($"--- Articles containing '{filterKeyword}' ---");
// Filter and display using LINQ
if (articles != null)
{
var filtered = articles.Where(a => a.Title.Contains(filterKeyword));
foreach (var item in filtered)
{
Console.WriteLine($"[ID:{item.Id}] {item.Title}");
Console.WriteLine($"URL: https://example.com/posts/{item.Id}");
Console.WriteLine("------------------------------------------");
}
}
}
else
{
Console.WriteLine($"Error: {response.StatusCode}");
}
}
catch (Exception ex)
{
Console.WriteLine($"Exception: {ex.Message}");
}
}
}
// Data model class
public class Article
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
[JsonPropertyName("body")]
public string Body { get; set; } = string.Empty;
[JsonPropertyName("userId")]
public int UserId { get; set; }
}
Customization Points
- Changing HTTP Methods: Replace
HttpMethod.GetwithPost,Put, orDelete. For non-standard methods like PATCH, usenew HttpMethod("PATCH"). - Setting Body Content: When performing POST or PUT, assign data to
request.Content.request.Content = new StringContent(jsonString, Encoding.UTF8, "application/json"); - Property Mapping: Use the
[JsonPropertyName("...")]attribute as shown in the code to map JSON names (e.g.,user_id) to C# properties.
Important Notes
- Disposable Nature: An
HttpRequestMessageinstance cannot be reused once it has been sent. To resend the same content, you must instantiate a new message. - Using Statement: Both
HttpRequestMessageandHttpResponseMessageimplementIDisposable. Always wrap them inusingstatements to prevent resource leaks. - Header Merging: Headers set in
HttpClient.DefaultRequestHeadersandHttpRequestMessage.Headersare merged. Be careful of duplicate values.
Advanced Application
Checking Existence with the HEAD Method
To check if a file exists or to retrieve metadata (like file size) without downloading the entire body, use the HEAD method.
var headRequest = new HttpRequestMessage(HttpMethod.Head, "https://example.com/large-file.zip");
using var response = await _httpClient.SendAsync(headRequest);
if (response.StatusCode == HttpStatusCode.OK)
{
Console.WriteLine($"File Size: {response.Content.Headers.ContentLength} bytes");
}
Conclusion
Combining HttpClient.SendAsync with HttpRequestMessage is the most flexible approach for interacting with REST APIs. It is essential for scenarios requiring per-request authentication tokens or specialized HTTP methods like HEAD and OPTIONS. Transitioning to this pattern provides complete control over the details of your HTTP communications.
