How to Send an HTTP GET Request in C# Using HttpClient

Making HTTP requests is a fundamental task in modern application development. In C#, the HttpClient class provides a powerful and flexible way to send HTTP requests and receive responses.

This guide will show you how to make HTTP GET requests properly in C#.

Basic HTTP GET Request

Here's a simple example of how to make an HTTP GET request:

using System;
using System.Net.Http;
using System.Threading.Tasks;

public class Program
{
    static async Task Main()
    {
        // Create a single HttpClient instance to reuse throughout your application
        using HttpClient client = new HttpClient();
        
        try
        {
            // Send GET request
            HttpResponseMessage response = await client.GetAsync("https://api.example.com/data");
            
            // Check if the request was successful
            response.EnsureSuccessStatusCode();
            
            // Read response content
            string responseBody = await response.Content.ReadAsStringAsync();
            
            // Process the response
            Console.WriteLine(responseBody);
        }
        catch (HttpRequestException e)
        {
            Console.WriteLine($"Request error: {e.Message}");
        }
    }
}

Adding Request Headers

Often, you'll need to add headers to your request, such as authentication tokens:

// Add default headers to be used with all requests
client.DefaultRequestHeaders.Add("User-Agent", "My C# Application");
client.DefaultRequestHeaders.Add("API-Key", "your-api-key");

// For specific content type
client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));

// For Bearer authentication
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", "your-token-here");

Handling Query Parameters

If you need to include query parameters in your URL:

// Option 1: Build the URL with query parameters manually
string baseUrl = "https://api.example.com/search";
string query = "search_term";
int page = 1;
string requestUri = $"{baseUrl}?q={Uri.EscapeDataString(query)}&page={page}";

// Option 2: Use HttpRequestMessage with UriBuilder
var uriBuilder = new UriBuilder("https://api.example.com/search");
var query = System.Web.HttpUtility.ParseQueryString(string.Empty);
query["q"] = "search_term";
query["page"] = "1";
uriBuilder.Query = query.ToString();

var request = new HttpRequestMessage(HttpMethod.Get, uriBuilder.Uri);
var response = await client.SendAsync(request);

Best Practices

  1. Reuse HttpClient: Create a single HttpClient instance and reuse it throughout your application's lifecycle to avoid socket exhaustion.

  2. Use Cancellation Tokens: For operations that might take time, implement cancellation tokens:

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); // Timeout after 10 seconds
var response = await client.GetAsync("https://api.example.com/data", cts.Token);
  1. Configure Timeouts: Set appropriate timeouts for your requests:
client.Timeout = TimeSpan.FromSeconds(30);
  1. Dispose HttpClient Properly: Use using statements or implement IDisposable in containing classes.

  2. Use HttpClientFactory: In ASP.NET Core applications, use the built-in HttpClientFactory to manage HttpClient instances:

// In Startup.ConfigureServices
services.AddHttpClient("api", client =>
{
    client.BaseAddress = new Uri("https://api.example.com/");
    client.DefaultRequestHeaders.Add("User-Agent", "My C# Application");
});

// In your service/controller
public class MyService
{
    private readonly IHttpClientFactory _clientFactory;
    
    public MyService(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }
    
    public async Task GetDataAsync()
    {
        var client = _clientFactory.CreateClient("api");
        var response = await client.GetAsync("data");
        // Process response...
    }
}

Deserializing JSON Responses

Most modern APIs return data in JSON format. You can easily deserialize it using System.Text.Json:

using System.Text.Json;

// Send request
var response = await client.GetAsync("https://api.example.com/users/1");
response.EnsureSuccessStatusCode();

// Read and deserialize the response
var content = await response.Content.ReadAsStringAsync();
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var user = JsonSerializer.Deserialize<User>(content, options);

Console.WriteLine($"User name: {user.Name}");

// User class
public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

Conclusion

The HttpClient class provides a modern and efficient way to make HTTP requests in C#. By following the best practices outlined above, you can ensure your application handles network communication efficiently and robustly.

Remember that proper exception handling, timeouts, and resource management are crucial for building reliable networked applications. The HttpClient class makes these tasks straightforward, allowing you to focus on your application's core functionality.

0
1801

Related

Removing duplicates from a list in C# is a common task, especially when working with large datasets. C# provides multiple ways to achieve this efficiently, leveraging built-in collections and LINQ.

Using HashSet (Fastest for Unique Elements)

A HashSet<T> automatically removes duplicates since it only stores unique values. This is one of the fastest methods:

List<int> numbers = new List<int> { 1, 2, 2, 3, 4, 4, 5 };
numbers = new HashSet<int>(numbers).ToList();
Console.WriteLine(string.Join(", ", numbers)); // Output: 1, 2, 3, 4, 5

Using LINQ Distinct (Concise and Readable)

LINQ’s Distinct() method provides an elegant way to remove duplicates:

List<int> numbers = new List<int> { 1, 2, 2, 3, 4, 4, 5 };
numbers = numbers.Distinct().ToList();
Console.WriteLine(string.Join(", ", numbers)); // Output: 1, 2, 3, 4, 5

Removing Duplicates by Custom Property (For Complex Objects)

When working with objects, DistinctBy() from .NET 6+ simplifies duplicate removal based on a property:

using System.Linq;
using System.Collections.Generic;

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

List<Person> people = new List<Person>
{
    new Person { Name = "Alice", Age = 30 },
    new Person { Name = "Bob", Age = 25 },
    new Person { Name = "Alice", Age = 30 }
};

people = people.DistinctBy(p => p.Name).ToList();
Console.WriteLine(string.Join(", ", people.Select(p => p.Name))); // Output: Alice, Bob

For earlier .NET versions, use GroupBy():

people = people.GroupBy(p => p.Name).Select(g => g.First()).ToList();

Performance Considerations

  • HashSet<T> is the fastest but only works for simple types.
  • Distinct() is easy to use but slower than HashSet<T> for large lists.
  • DistinctBy() (or GroupBy()) is useful for complex objects but may have performance trade-offs.

Conclusion

Choosing the best approach depends on the data type and use case. HashSet<T> is ideal for primitive types, Distinct() is simple and readable, and DistinctBy() (or GroupBy()) is effective for objects.

0
260

Reading a file line by line is useful when handling large files without loading everything into memory at once.

✅ Best Practice: Use File.ReadLines() which is more memory efficient.

Example

foreach (string line in File.ReadLines("file.txt"))
{
    Console.WriteLine(line);
}

Why use ReadLines()?

Reads one line at a time, reducing overall memory usage. Ideal for large files (e.g., logs, CSVs).

Alternative: Use StreamReader (More Control)

For scenarios where you need custom processing while reading the contents of the file:

using (StreamReader reader = new StreamReader("file.txt"))
{
    string? line;
    while ((line = reader.ReadLine()) != null)
    {
        Console.WriteLine(line);
    }
}

Why use StreamReader?

Lets you handle exceptions, encoding, and buffering. Supports custom processing (e.g., search for a keyword while reading).

When to Use ReadAllLines()? If you need all lines at once, use:

string[] lines = File.ReadAllLines("file.txt");

Caution: Loads the entire file into memory—avoid for large files!

3
281

Primary constructors, introduced in C# 12, offer a more concise way to define class parameters and initialize fields.

This feature reduces boilerplate code and makes classes more readable.

Traditional Approach vs Primary Constructor

Before primary constructors, you would likely write something like the following:

public class UserService
{
    private readonly ILogger _logger;
    private readonly IUserRepository _repository;

    public UserService(ILogger logger, IUserRepository repository)
    {
        _logger = logger;
        _repository = repository;
    }

    public async Task<User> GetUserById(int id)
    {
        _logger.LogInformation("Fetching user {Id}", id);
        return await _repository.GetByIdAsync(id);
    }
}

With primary constructors, this becomes:

public class UserService(ILogger logger, IUserRepository repository)
{
    public async Task<User> GetUserById(int id)
    {
        logger.LogInformation("Fetching user {Id}", id);
        return await repository.GetByIdAsync(id);
    }
}

Key Benefits

  1. Reduced Boilerplate: No need to declare private fields and write constructor assignments
  2. Parameters Available Throughout: Constructor parameters are accessible in all instance methods
  3. Immutability by Default: Parameters are effectively readonly without explicit declaration

Real-World Example

Here's a practical example using primary constructors with dependency injection:

public class OrderProcessor(
    IOrderRepository orderRepo,
    IPaymentService paymentService,
    ILogger<OrderProcessor> logger)
{
    public async Task<OrderResult> ProcessOrder(Order order)
    {
        try
        {
            logger.LogInformation("Processing order {OrderId}", order.Id);
            
            var paymentResult = await paymentService.ProcessPayment(order.Payment);
            if (!paymentResult.Success)
            {
                return new OrderResult(false, "Payment failed");
            }

            await orderRepo.SaveOrder(order);
            return new OrderResult(true, "Order processed successfully");
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to process order {OrderId}", order.Id);
            throw;
        }
    }
}

Tips and Best Practices

  1. Use primary constructors when the class primarily needs dependencies for its methods
  2. Combine with records for immutable data types:
public record Customer(string Name, string Email)
{
    public string FormattedEmail => $"{Name} <{Email}>";
}
  1. Consider traditional constructors for complex initialization logic

Primary constructors provide a cleaner, more maintainable way to write C# classes, especially when working with dependency injection and simple data objects.

0
68