Advanced .NET Development: HTTP Interface Integration and Client Development Techniques
Introduction: The Critical Role of HTTP in Modern Architecture
With the widespread adoption of cloud computing and distributed systems, microservice architecture has become the preferred choice for many enterprise projects. In this paradigm, individual services must collaborate seamlessly to deliver complete functionality to end users, making inter-service communication a critical component of development efforts. As the most ubiquitous network protocol, HTTP serves as the foundation for communication in microservice architectures, enabling services to interact, access static resources, and invoke API endpoints across network boundaries.
However, crafting robust HTTP client code remains a time-consuming endeavor fraught with challenges. Network instability, timeout scenarios, authentication complexities, and error handling requirements all contribute to the difficulty of building reliable HTTP clients. This comprehensive guide explores .NET's HttpClient capabilities, demonstrates advanced configuration techniques, and introduces powerful tools that dramatically reduce repetitive work while enhancing development productivity.
HttpClient Fundamentals: Building Blocks of HTTP Communication
This section establishes essential knowledge for working with HttpClient and addresses common HTTP request operations. The concepts presented here form the foundation for all subsequent advanced topics.
Request Parameter Transmission Strategies
HTTP requests commonly transmit parameters through four primary mechanisms: query strings, headers, form data, and JSON payloads. Each approach serves distinct use cases and carries unique considerations.
Query String Parameters
Query parameters are appended directly to the URL using ampersand separators. For example, https://localhost:5001/test?a=1&b=2 contains two parameters: a and b. Since query parameters exist within the URL itself, they can be constructed through string interpolation or concatenation.
// URL Query Parameters
public static async Task Query(string a, string b)
{
using var httpClient = new HttpClient(httpclientHandler);
var response = await httpClient.GetAsync($"https://localhost:5001/query?a={a}&b={b}");
}In ASP.NET Core, the [FromQuery] attribute enables automatic parameter binding from URL query strings:
[HttpGet("/query")]
public string Query([FromQuery] string a, [FromQuery] string b)
{
return a + b;
}Parameters decorated with [FromQuery] are case-insensitive, providing flexibility in client implementations.
However, manually constructing query strings proves error-prone and tedious. The System.Web.HttpUtility class offers safer parameter handling:
var nv = System.Web.HttpUtility.ParseQueryString(string.Empty);
nv.Add("a", "1");
nv.Add("b", "2");
var query = nv.ToString();
var url = "https://localhost:5001/query?" + nv;
// Results in: https://localhost:5001/query?a=1&b=2ASP.NET Core provides a similar utility through QueryHelpers:
var dic = new Dictionary<string, string>()
{
{ "a", "1" },
{ "b", "2" }
};
var query = QueryHelpers.AddQueryString("https://localhost:5001/query", dic);Critical Consideration: Special Character Encoding
When query parameter values contain special characters, proper URL encoding becomes essential. Consider setting a=http://localhost:5001/query?a=1&b=2—direct concatenation produces:
http://localhost:5001/query?a=http://localhost:5001/query?a=1&b=2&b=2The server interprets this as two separate parameters:
a = http://localhost:5001/query?a=1b = 2
To prevent such parsing errors, special characters must be encoded before URL inclusion:
a = Uri.EscapeDataString("http://localhost:5001/query?a=1&b=2");
b = "2";
var response = await httpClient.GetAsync($"https://localhost:5001/query?a={a}&b={b}");The resulting URL becomes:
http://localhost:5001/query?a=http%3A%2F%2Flocalhost%3A5001%2Fquery%3Fa%3D1%26b%3D2&b=2This encoding ensures the entire URL string is treated as a single parameter value.
HTTP Headers
Headers constitute an integral portion of the HTTP protocol, carrying metadata about requests and responses. HttpClient stores headers as key-value pairs:
public static async Task Header()
{
using var httpClient = new HttpClient(httpclientHandler);
// Header attachment
httpClient.DefaultRequestHeaders.Add("MyEmail", "123@qq.com");
var response = await httpClient.GetAsync($"https://localhost:5001/header");
var result = await response.Content.ReadAsStringAsync();
}ASP.NET Core's [FromHeader] attribute simplifies header parameter extraction:
[HttpGet("/header")]
public string Header([FromHeader] string? myEmail)
{
return myEmail;
}Like query parameters, header parameters are case-insensitive when using [FromHeader].
HttpRequestHeaders defines several commonly used headers as strongly-typed properties:
AcceptAcceptCharsetAcceptEncodingAcceptLanguageAuthorization
These can be set directly without key-value pair manipulation:
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
httpClient.DefaultRequestHeaders.AcceptLanguage = ...;Form Data Submission
Forms support two primary content types: application/x-www-form-urlencoded and multipart/form-data. While form-data offers broader compatibility (including file uploads), x-www-form-urlencoded provides superior performance for simple key-value pairs.
X-WWW-Form-Urlencoded Example:
// Form Submission
// application/x-www-form-urlencoded
public static async Task From()
{
var formContent = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string,string>("Id", "1"),
new KeyValuePair<string,string>("Name", "痴者工良"),
new KeyValuePair<string, string>("Number", "666666")
});
using var httpClient = new HttpClient(httpclientHandler);
var response = await httpClient.PostAsync("http://localhost:5001/form1", formContent);
}ASP.NET Core can receive form parameters via Dictionary:
[HttpPost("/form1")]
public string Form1([FromForm] Dictionary<string, string> dic)
{
return "success";
}Alternatively, strongly-typed models provide better structure:
[HttpPost("/form2")]
public string Form2([FromForm] Form2Model model)
{
return "success";
}
public class Form2Model
{
public string Id { get; set; }
public string Name { get; set; }
public string Number { get; set; }
}File Upload with Multipart Form Data:
// File Upload
public static async Task SendFile(string filePath, string fromName, string url)
{
using var client = new HttpClient();
FileStream imagestream = System.IO.File.OpenRead(filePath);
var multipartFormDataContent = new MultipartFormDataContent()
{
{
new StreamContent(File.OpenRead(filePath)),
// Corresponds to server WebAPI parameter name
fromName,
// Uploaded file name
Path.GetFileName(filePath)
},
// Multiple files can be uploaded
};
HttpResponseMessage response = await client.PostAsync(url, multipartFormDataContent);
}HttpClient abstracts body parameter types through HttpContent, with numerous derived types including:
MultipartFormDataContentStreamContentStringContentFormUrlEncodedContent
Since MultipartFormDataContent implements IEnumerable<HttpContent>, it can nest other HttpContent types, enabling simultaneous file and form parameter transmission:
MultipartContent multipartContent = new MultipartContent();
multipartContent.Add(multipartFormDataContent);
multipartContent.Add(fromContent);
var multipartFormDataContent = new MultipartFormDataContent()
{
// File
{
new StreamContent(File.OpenRead(filePath)),
fromName,
Path.GetFileName(filePath)
},
// Form data
fromContent,
};ASP.NET Core handles file uploads through IFormFile:
[HttpPost("/form3")]
public string Form3([FromForm] IFormFile img)
{
return "success";
}
var multipartFormDataContent = new MultipartFormDataContent()
{
{
new StreamContent(File.OpenRead(filePath)),
// Must match API parameter name
"img",
Path.GetFileName(filePath)
},
};For batch file uploads, use IFormFileCollection:
[HttpPost("/form4")]
public string Form4([FromForm] IFormFileCollection imgs)
{
return "success";
}Combined file and form data reception uses model classes:
[HttpPost("/form5")]
public string Form5([FromForm] Form5Model model)
{
return "success";
}
public class Form5Model
{
public string Id { get; set; }
public string Name { get; set; }
public IFormFile Img { get; set; }
}JSON Payload Transmission
When transmitting JSON data in HTTP request bodies, the Content-Type header must indicate the body format.
Common Body Content Types:
| Type | Content-Type |
|---|---|
| Text | text/plain |
| JavaScript | application/javascript |
| HTML | text/html |
| JSON | application/json |
| XML | application/xml |
In HttpClient, request bodies use StringContent with explicit Content-Type specification:
// JSON transmission
public static async Task Json<T>(T obj) where T : class
{
var json = System.Text.Json.JsonSerializer.Serialize(obj);
var jsonContent = new StringContent(json);
// Specify Content-Type during request
jsonContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
using var httpClient = new HttpClient();
var response = httpClient.PostAsync("https://localhost:5001/json", jsonContent).Result;
var result = await response.Content.ReadAsStringAsync();
}
public class JsonModel
{
public string Id { get; set; }
public string Name { get; set; }
}
await HttpClientHelper.Json(new JsonModel
{
Id = "1",
Name = "工良"
});ASP.NET Core receives JSON via model binding:
[HttpPost("/json")]
public string Json([FromBody] JsonModel model)
{
return "success";
}
public class JsonModel
{
public string Id { get; set; }
public string Name { get; set; }
}For dynamic JSON structures with uncertain fields, object type provides flexibility:
[HttpPost("/json1")]
public string Json1([FromBody] object model)
{
return "success";
}However, the actual type depends on the configured serialization framework:
[HttpPost("/json2")]
public string Json2([FromBody] object model)
{
if (model is System.Text.Json.Nodes.JsonObject jsonObject)
{
// System.Text.Json handling
}
else if (model is Newtonsoft.Json.Linq.JObject jObject)
{
// Newtonsoft.Json handling
}
// ... additional type checks
return "success";
}Authentication and Credential Management
Client applications must authenticate with servers to access protected resources. Common authentication mechanisms include Basic authentication, JWT tokens, and cookie-based sessions.
Basic Authentication
Basic authentication offers simplicity suited for trusted networks, commonly found in routers and embedded devices. However, credentials are merely Base64-encoded (not encrypted), providing minimal security.
// Basic Authentication
public static async Task<string> Basic(string url, string user, string password)
{
using HttpClient client = new HttpClient(httpclientHandler);
AuthenticationHeaderValue authentication = new AuthenticationHeaderValue(
"Basic",
Convert.ToBase64String(Encoding.UTF8.GetBytes($"{user}:{password}"))
);
client.DefaultRequestHeaders.Authorization = authentication;
var response = await client.GetAsync(url);
return await response.Content.ReadAsStringAsync();
}Security Note: Basic authentication should only be used over HTTPS connections to prevent credential interception.
JWT Token Authentication
JWT (JSON Web Tokens) represents the most prevalent authentication method in modern microservice architectures. Tokens are transmitted via the Authorization header:
// JWT Authentication
public static async Task<string> Jwt(string token, string url)
{
using var client = new HttpClient(httpclientHandler);
// Create authentication header
// System.Net.Http.Headers.AuthenticationHeaderValue
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await client.GetAsync(url);
return await response.Content.ReadAsStringAsync();
}Cookie-Based Session Management
HttpClient supports two cookie handling approaches:
- Pre-existing cookies: Directly store known cookie values in HttpClient
- Login-acquired cookies: Authenticate to obtain cookies, which are automatically stored and reused
For HttpClient reuse with consistent cookie handling, configure the UseCookies property on HttpClientHandler:
var httpclientHandler = new HttpClientHandler()
{
UseCookies = true
};This setting instructs the handler to maintain a CookieContainer that stores server cookies and automatically includes them in subsequent requests.
Login and Cookie Acquisition:
// Obtain HttpClient after login
public static async Task<HttpClient> Cookie(string user, string password, string loginUrl)
{
var httpclientHandler = new HttpClientHandler()
{
ServerCertificateCustomValidationCallback = (message, cert, chain, error) => true,
UseCookies = true
};
var loginContent = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string,string>("user", user),
new KeyValuePair<string, string>("password", password)
});
var httpClient = new HttpClient(httpclientHandler);
var response = await httpClient.PostAsync(loginUrl, loginContent);
if (response.IsSuccessStatusCode) return httpClient;
throw new Exception($"Request failed, HTTP status code: {response.StatusCode}");
}After successful authentication, the server writes cookies to the HTTP client. Reusing the same HttpClient instance automatically includes these cookies in subsequent requests.
Manual Cookie Setting:
When cookies are already obtained, they can be set directly:
// Manually set cookie
public static async Task<string> Cookie(string cookie, string url)
{
var httpclientHandler = new HttpClientHandler()
{
ServerCertificateCustomValidationCallback = (message, cert, chain, error) => true,
};
using var client = new HttpClient(httpclientHandler);
client.DefaultRequestHeaders.Add("Cookie", cookie);
var response = await client.GetAsync(url);
return await response.Content.ReadAsStringAsync();
}Exception Handling Strategies
HttpClient operations can fail through several distinct exception types, each requiring appropriate handling strategies.
Primary Exception Categories:
- HttpRequestException: General request failures
- OperationCanceledException: Operation cancellation or timeout
- TimeoutException: Network timeout scenarios
Additionally, specific edge cases produce unique exceptions:
Invalid Status Code Handling:
When HttpClient receives response status codes outside the valid range (0-999), it throws ArgumentOutOfRangeException:
public HttpResponseMessage(HttpStatusCode statusCode)
{
if (((int)statusCode < 0) || ((int)statusCode > 999))
{
throw new ArgumentOutOfRangeException("statusCode");
}
}Non-Success Status Codes:
HttpClient considers status codes 200-299 as successful. Other status codes trigger HttpRequestException when EnsureSuccessStatusCode() is called:
public bool IsSuccessStatusCode
{
get { return ((int)statusCode >= 200) && ((int)statusCode <= 299); }
}
public HttpResponseMessage EnsureSuccessStatusCode()
{
if (!IsSuccessStatusCode)
{
// ... exception throwing logic
throw new HttpRequestException(...);
}
// ... success handling
}Timeout and Cancellation Distinction:
OperationCanceledException occurs when CancellationToken is triggered or times out. TimeoutException appears in two scenarios: network request timeouts and CancellationToken timeouts. Since both exceptions can result from CancellationToken timeout, careful distinction is necessary during exception handling:
catch (OperationCanceledException ex) when (ex.InnerException is TimeoutException tex)
{
Console.WriteLine($"Timed out: {ex.Message}, {tex.Message}");
}IHttpClientFactory: Managed HttpClient Lifecycle
Direct HttpClient usage requires manual lifecycle management and connection resource disposal. Moreover, rapid creation of numerous HttpClient instances can severely degrade system performance due to socket exhaustion. To address these challenges, .NET introduced IHttpClientFactory, which provides automatic HttpClient management, unified behavior control, and centralized configuration capabilities.
IHttpClientFactory Fundamentals
The following examples reference the Demo6.HttpFactory project. Simply add the Microsoft.Extensions.Http package to access IHttpClientFactory functionality.
Three Primary Injection Approaches:
static void Main()
{
var services = new ServiceCollection();
services.AddScoped<Test1>();
services.AddScoped<Test2>();
services.AddScoped<Test3>();
// Approach 1: Basic registration
services.AddHttpClient();
// Approach 2: Named client
services.AddHttpClient("Default");
// Approach 3: Typed client
services.AddHttpClient<Program>();
}
public class Test1
{
public Test1(IHttpClientFactory httpClientFactory)
{
var httpClient = httpClientFactory.CreateClient();
// Alternatively:
// httpClientFactory.CreateClient("Default");
}
}
public class Test2
{
public Test2(IHttpClientFactory httpClientFactory)
{
var httpClient = httpClientFactory.CreateClient("Default");
}
}
public class Test3
{
public Test3(HttpClient httpClient)
{
// HttpClient directly injected
}
}The second and third approaches enable HttpClient behavior configuration, including default parameter attachment and HttpMessageHandler binding.
Configured Named Client:
// Approach 2 with configuration
services.AddTransient<MyDelegatingHandler>();
services.AddHttpClient("Default")
.ConfigureHttpClient(x =>
{
x.MaxResponseContentBufferSize = 1024;
x.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "xxxxx");
})
.AddHttpMessageHandler<MyDelegatingHandler>();Logging and Interception via DelegatingHandler:
To maintain clear request logs for troubleshooting or intercept HTTP requests and responses, implement a DelegatingHandler type, register it as a Transient service, then inject via .AddHttpMessageHandler<MyDelegatingHandler>().
public class MyDelegatingHandler : DelegatingHandler
{
private readonly ILogger<MyDelegatingHandler> _logger;
public MyDelegatingHandler(ILogger<MyDelegatingHandler> logger)
{
_logger = logger;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
HttpResponseMessage httpResponseMessage = null;
try
{
httpResponseMessage = await base.SendAsync(request, cancellationToken);
#if DEBUG
_logger.LogDebug(MyException.CreateMessage(request, httpResponseMessage));
#endif
if (httpResponseMessage.IsSuccessStatusCode)
{
return httpResponseMessage;
}
throw new MyException(request, httpResponseMessage);
}
catch (Exception)
{
_logger.LogError(MyException.CreateMessage(request, httpResponseMessage));
throw;
}
}
}Request Resilience Policies
Microsoft.Extensions.Http.Polly extends HttpClient with fluent, thread-safe handling of retry logic, circuit breakers, timeouts, bulkhead isolation, and fallback strategies.
Reference the Demo6.Polly project for example implementations:
public static void Test()
{
var services = new ServiceCollection();
services.AddHttpClient("Default", client =>
{
client.BaseAddress = new Uri("http://localhost:5000");
})
.AddPolicyHandler(GetRetryPolicy())
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
}
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
.WaitAndRetryAsync(6, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}The Polly.Contrib.WaitAndRetry package offers additional extensions for fine-grained retry customization:
public async Task<string> GetAsync()
{
var delay = Backoff.DecorrelatedJitterBackoffV2(
medianFirstRetryDelay: TimeSpan.FromSeconds(1),
retryCount: 5);
var retryPolicy = Policy
.Handle<HttpRequestException>(ex =>
{
// Allow retry for these specific conditions
if (ex.StatusCode == HttpStatusCode.BadGateway ||
ex.StatusCode == HttpStatusCode.GatewayTimeout ||
ex.StatusCode == HttpStatusCode.ServiceUnavailable)
return true;
return false;
})
// Handle other exception types
.WaitAndRetryAsync(delay);
var result = await retryPolicy.ExecuteAsync<string>(async () =>
{
var responseMessage = await _httpClient.GetAsync("https://www.baidu.com");
return await responseMessage.Content.ReadAsStringAsync();
});
return result;
}Refit Framework: Declarative HTTP Client Generation
Refit represents a dynamic code generation library that automatically produces HttpClient implementation code from interface definitions. Rather than writing extensive invocation code, parameter marshaling, and exception handling, developers simply define interfaces, and Refit generates the underlying HTTP client code automatically.
For comprehensive Refit documentation, visit: https://reactiveui.github.io/refit/
Reference the Demo6.Refit project for examples. Install Refit.HttpClientFactory and Refit.Newtonsoft.Json via NuGet.
Sample API:
[ApiController]
[Route("[controller]")]
public class IndexController : ControllerBase
{
[HttpGet("name")]
public string GetName([FromQuery] string name)
{
return name;
}
}Refit Client Interface:
public interface IDemo6Client
{
[Get("/index/name")]
Task<string> GetAsync([Query] string name);
}Dependency Injection Registration:
services.AddRefitClient<IDemo6Client>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri(url))
.SetHandlerLifetime(TimeSpan.FromSeconds(3));Alternatively, use static method construction:
var client = RestService.For<IDemo6Client>(url, new RefitSettings());Custom Serialization Configuration:
JsonSerializerSettings j1 = new JsonSerializerSettings()
{
DateFormatString = "yyyy-MM-dd HH:mm:ss"
};
RefitSettings r1 = new RefitSettings(new NewtonsoftJsonContentSerializer(j1));
// JsonSerializerOptions j2 = new JsonSerializerOptions();
// RefitSettings r2 = new RefitSettings(new SystemTextJsonContentSerializer(j2));
services.AddRefitClient<IDemo6Client>(r1)
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://baidu.com"));Dynamic Address Configuration:
In scenarios like IoT where numerous devices each host web services, request addresses may only be determined at runtime:
public interface IDemo6ClientDynamic
{
HttpClient Client { get; }
[Get("/index/name")]
Task<string> GetAsync([Query] string name);
}After obtaining the IDemo6ClientDynamic service instance, configure the address dynamically:
services.AddRefitClient<IDemo6ClientDynamic>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://baidu.com"))
.SetHandlerLifetime(TimeSpan.FromSeconds(3));
ioc = services.BuildServiceProvider();
var clientDynamic = ioc.GetRequiredService<IDemo6ClientDynamic>();
clientDynamic.Client.BaseAddress = new Uri("https://baidu.com");
await clientDynamic.GetAsync("test");Refit with Policy Integration:
services.AddRefitClient<IDemo6Client>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://baidu.com"))
.SetHandlerLifetime(TimeSpan.FromSeconds(3))
.AddPolicyHandler(BuildRetryPolicy());
// Build retry policy
static IAsyncPolicy<HttpResponseMessage> BuildRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
.WaitAndRetryAsync(6, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}When developing HTTP clients, prefer IHttpClientFactory for lifecycle management. For rapid development, leverage frameworks like Refit to auto-generate code. Always implement proper error handling, configure appropriate timeouts, establish retry strategies, and handle different failure scenarios appropriately.
CLI Tool Development
Refit Code Generation Tool
Manually configuring each API endpoint proves repetitive and inefficient. A superior approach generates Refit code directly from Swagger documentation.
Install Refitter Tool:
dotnet tool install --global RefitterStart the Demo6.Api service, then execute:
refitter http://localhost:5001/swagger/v1/swagger.json --namespace "MyApi" --output ./IDemo6Api.csAfter execution, locate the generated IDemo6Api.cs file in the output directory.
Using Refit eliminates extensive HttpClient boilerplate code, simplifying configuration, request strategy control, and error handling. Refitter takes this further by automating interface definition entirely.
Building .NET Tool Packages
.NET tool packages represent a special NuGet package category. Tools like dotnet-dump and Refitter exemplify this pattern. For internal enterprise development, creating script-like tools enhances productivity, and .NET tool packages excel at this use case.
Reference the Maomi.Curl project for examples. Maomi.Curl functions as a curl-like tool for HTTP requests. Before development, install and explore its usage:
dotnet tool install --global Maomi.Curl --version 2.0.0Maomi.Curl aliases to mmurl. Enter mmurl in the terminal to view parameter lists and usage examples.
Testing with jsonplaceholder.typicode.com:
# GET request
mmurl https://jsonplaceholder.typicode.com/todos/1
request: https://jsonplaceholder.typicode.com/todos/1
{
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
}
# POST request
mmurl -X POST -d '{"userId":2}' https://jsonplaceholder.typicode.com/posts
request: https://jsonplaceholder.typicode.com/posts
{
"userId": 2,
"id": 101
}Tool Package Project Configuration:
Tool packages resemble console applications but require specific .csproj properties:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Maomi.Curl</RootNamespace>
</PropertyGroup>
<PropertyGroup>
<!-- Set as tool package project -->
<PackAsTool>true</PackAsTool>
<!-- Command-line tool name -->
<ToolCommandName>mmurl</ToolCommandName>
<Version>2.0.0</Version>
<Description>A curl-like tool</Description>
<PackageId>Maomi.Curl</PackageId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
</ItemGroup>
</Project>Implementing GET and POST Methods:
private static async Task GetAsync(
string url,
IReadOnlyDictionary<string, string> headers,
string? cookie = null)
{
var client = new HttpClient();
BuildHeader(headers, cookie, client);
var response = await client.GetAsync(new Uri(url));
Console.WriteLine(await response.Content.ReadAsStringAsync());
}
private static async Task PostAsync(
string url,
IReadOnlyDictionary<string, string> headers,
string body,
string? cookie = null)
{
var client = new HttpClient();
BuildHeader(headers, cookie, client);
var jsonContent = new StringContent(body);
jsonContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
var response = await client.PostAsync(new Uri(url), jsonContent);
Console.WriteLine(await response.Content.ReadAsStringAsync());
}
private static void BuildHeader(
IReadOnlyDictionary<string, string> headers,
string? cookie,
HttpClient client)
{
if (headers != null && headers.Count > 0)
{
foreach (var item in headers)
client.DefaultRequestHeaders.Add(item.Key, item.Value);
}
if (!string.IsNullOrEmpty(cookie))
{
client.DefaultRequestHeaders.Add("Cookie", cookie);
}
}Command-Line Parameter Parsing with System.CommandLine:
static async Task<int> Main(string[] args)
{
// Define command parameters
// HTTP header
var headers = new Option<Dictionary<string, string>?>(
name: "-H",
description: "header, ex: -H \"Accept-Language=zh-CN\".",
parseArgument: result =>
{
var dic = new Dictionary<string, string>();
if (result.Tokens.Count == 0) return dic;
foreach (var item in result.Tokens)
{
var header = item.Value.Split("=");
dic.Add(header[0], header[1]);
}
return dic;
})
{
// Can appear 0 or more times
Arity = ArgumentArity.ZeroOrMore,
};
var cookie = new Option<string?>(
name: "-b",
description: "cookie.")
{
Arity = ArgumentArity.ZeroOrOne
};
var body = new Option<string?>(
name: "-d",
description: "post body.")
{
Arity = ArgumentArity.ZeroOrOne
};
var httpMethod = new Option<string?>(
name: "-X",
description: "GET/POST ...",
getDefaultValue: () => "GET")
{
Arity = ArgumentArity.ZeroOrOne
};
// Other unnamed arguments
var otherArgument = new Argument<string>();
// Build command-line parameters
var rootCommand = new RootCommand("Input parameters to request URL address");
rootCommand.AddOption(headers);
rootCommand.AddOption(cookie);
rootCommand.AddOption(body);
rootCommand.AddOption(httpMethod);
rootCommand.Add(otherArgument);
// Parse parameters and invoke
rootCommand.SetHandler(async (headers, cookie, body, httpMethod, otherArgument) =>
{
Console.WriteLine($"request: {otherArgument}");
if (headers == null) headers = new Dictionary<string, string>();
try
{
if (!string.IsNullOrEmpty(body) ||
"POST".Equals(httpMethod, StringComparison.InvariantCultureIgnoreCase))
{
ArgumentNullException.ThrowIfNull(body);
await PostAsync(otherArgument, headers, body, cookie);
}
else
{
await GetAsync(otherArgument, headers, cookie);
}
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(ex.Message);
Console.ResetColor();
}
}, headers, cookie, body, httpMethod, otherArgument);
return await rootCommand.InvokeAsync(args);
}After completing the tool package project, create a NuGet package and upload it to nuget.org, enabling others to utilize your developed tools.
Conclusion: Best Practices Summary
When building HTTP clients in .NET, adhere to these guiding principles:
- Always use IHttpClientFactory for proper lifecycle management and resource efficiency
- Implement retry policies using Polly for transient failure resilience
- Configure appropriate timeouts to prevent hanging requests
- Handle exceptions comprehensively, distinguishing between different failure modes
- Use Refit for rapid API client development to reduce boilerplate code
- Consider CLI tool packages for internal productivity tools
- Secure authentication credentials and never transmit sensitive data over unencrypted connections
- Validate and encode all user inputs to prevent injection attacks
By following these practices and leveraging the powerful tools available in the .NET ecosystem, developers can build robust, maintainable, and efficient HTTP client applications that stand up to the demands of modern distributed systems.