HTTP Client in ConnectSoft Microservice Template¶
Purpose & Overview¶
HTTP Client in the ConnectSoft Microservice Template provides a robust, production-ready foundation for making HTTP requests to external services and APIs. The template uses IHttpClientFactory to manage HttpClient instances, ensuring proper lifecycle management, connection pooling, and integration with service discovery, header propagation, and distributed tracing.
HTTP Client capabilities include:
- IHttpClientFactory Integration: Proper
HttpClientlifecycle management and connection pooling - Service Discovery: Automatic service endpoint resolution via configuration or DNS
- Header Propagation: Automatic propagation of correlation and tracing headers
- OpenTelemetry Instrumentation: Automatic distributed tracing for outgoing HTTP requests
- Resilience Patterns: Support for retry policies, circuit breakers, and timeout handling
- Type Safety: Typed client support for strongly-typed service clients
HTTP Client Philosophy
The ConnectSoft Microservice Template uses IHttpClientFactory exclusively for creating HttpClient instances. This ensures proper resource management, connection pooling, and integration with service discovery and observability features. Direct instantiation of HttpClient is avoided to prevent socket exhaustion and connection leaks.
Architecture Overview¶
HTTP Client Factory Pattern¶
Service Layer
├── Service Uses IHttpClientFactory
│ ├── CreateClient() → HttpClient Instance
│ ├── CreateClient("NamedClient") → Named HttpClient
│ └── Typed Client (Injected via DI)
└── HTTP Request
├── Header Propagation
├── Service Discovery Resolution
├── OpenTelemetry Instrumentation
└── Resilience Policies (if configured)
↓
External Service
HttpClient Lifecycle¶
Problem with Direct Instantiation:
// ❌ BAD - Socket exhaustion risk
public class BadService
{
public async Task CallApi()
{
using (var client = new HttpClient()) // Disposes socket but doesn't release immediately
{
// Socket remains in TIME_WAIT state
}
}
}
Solution with IHttpClientFactory:
// ✅ GOOD - Proper connection pooling
public class GoodService
{
private readonly IHttpClientFactory httpClientFactory;
public GoodService(IHttpClientFactory httpClientFactory)
{
this.httpClientFactory = httpClientFactory;
}
public async Task CallApi()
{
var client = this.httpClientFactory.CreateClient(); // Reuses pooled connections
// HttpClient instance is managed by factory
// No need to dispose - factory handles lifecycle
}
}
Configuration¶
Basic Setup¶
Service Registration:
// HttpClientExtensions.cs
internal static IServiceCollection AddMicroserviceHttpClient(
this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
// Configure HttpClient defaults
services.ConfigureHttpClientDefaults(http =>
{
if (OptionsExtensions.ServiceDiscoveryOptions.Enabled)
{
http.AddServiceDiscovery(); // Enable service discovery for all clients
}
});
// Add HttpClient factory with header propagation
services.AddHttpClient()
.AddHeaderPropagation(); // Automatically propagate headers
return services;
}
Integration in Application:
// MicroserviceRegistrationExtensions.cs
services.AddMicroserviceHeaderPropagation(); // Register header propagation services
services.AddMicroserviceHttpClient(); // Register HttpClient factory
Service Discovery Integration¶
Service Discovery Configuration:
{
"ServiceDiscovery": {
"Enabled": true,
"ServiceDiscoveryProvider": "Configuration",
"ConfigurationServiceEndpointSectionName": "Services"
},
"Services": {
"payment-service": {
"http": ["localhost:5001"],
"https": ["localhost:5002"]
},
"order-service": {
"http": ["localhost:5003"],
"https": ["localhost:5004"]
}
}
}
Usage with Service Discovery:
// Point to logical service name
services.AddHttpClient<PaymentServiceClient>(client =>
{
client.BaseAddress = new Uri("https://payment-service"); // Logical name
// Service discovery resolves to actual endpoint at runtime
});
Service Discovery Providers:
| Provider | Configuration | Use Case |
|---|---|---|
| Configuration | appsettings.json |
Development, testing, static endpoints |
| DNS | DNS A/AAAA records | Kubernetes, DNS-based service discovery |
| DNS SRV | DNS SRV records | Advanced DNS-based discovery |
Header Propagation¶
Automatic Header Propagation:
Headers are automatically propagated from incoming requests to outgoing HTTP requests:
X-TraceId: Custom trace identifierX-Correlation-Id: Business correlation identifiertraceparent: W3C Trace Context header (OpenTelemetry)
See Header Propagation for detailed documentation.
Configuration:
// HeaderPropagationExtensions.cs
services.AddHeaderPropagation(options =>
{
options.Headers.Add("X-TraceId");
options.Headers.Add("X-Correlation-Id");
options.Headers.Add("traceparent");
});
// Add to HttpClient factory
services.AddHttpClient()
.AddHeaderPropagation();
OpenTelemetry Instrumentation¶
Automatic Instrumentation:
// OpenTelemetryExtensions.cs
services.AddOpenTelemetry()
.WithTracing(builder => builder
.AddHttpClientInstrumentation(options =>
{
// Filter out telemetry endpoints
options.FilterHttpRequestMessage = message =>
message is not null &&
message.RequestUri is not null &&
!message.RequestUri.Host.Contains("visualstudio", StringComparison.Ordinal) &&
!message.RequestUri.Host.Contains("applicationinsights", StringComparison.Ordinal);
}));
Features:
- Automatic span creation for outgoing HTTP requests
- Trace context propagation via traceparent header
- Request/response metadata capture
- Error tracking and status code recording
See Distributed Tracing for detailed documentation.
Usage Patterns¶
1. Standard HttpClient¶
Create Client:
public class OrderService
{
private readonly IHttpClientFactory httpClientFactory;
private readonly ILogger<OrderService> logger;
public OrderService(
IHttpClientFactory httpClientFactory,
ILogger<OrderService> logger)
{
this.httpClientFactory = httpClientFactory;
this.logger = logger;
}
public async Task<PaymentResult> ProcessPaymentAsync(Order order)
{
// Create HttpClient instance (managed by factory)
var client = this.httpClientFactory.CreateClient();
try
{
// Headers are automatically propagated
var response = await client.PostAsJsonAsync(
"https://payment-service/api/payments",
new PaymentRequest { OrderId = order.Id });
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<PaymentResult>();
}
catch (HttpRequestException ex)
{
this.logger.LogError(
ex,
"Failed to process payment for order {OrderId}",
order.Id);
throw;
}
}
}
Characteristics:
- ✅ No need to dispose HttpClient (factory manages lifecycle)
- ✅ Connection pooling and reuse
- ✅ Header propagation enabled automatically
- ✅ OpenTelemetry instrumentation enabled
2. Named HttpClient¶
Register Named Client:
// In service registration
services.AddHttpClient("PaymentService", client =>
{
client.BaseAddress = new Uri("https://payment-service");
client.Timeout = TimeSpan.FromSeconds(30);
client.DefaultRequestHeaders.Add("X-Api-Version", "v1");
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddHeaderPropagation(); // Ensure header propagation
Use Named Client:
public class OrderService
{
private readonly IHttpClientFactory httpClientFactory;
public OrderService(IHttpClientFactory httpClientFactory)
{
this.httpClientFactory = httpClientFactory;
}
public async Task<PaymentResult> ProcessPaymentAsync(Order order)
{
// Get named client (pre-configured)
var client = this.httpClientFactory.CreateClient("PaymentService");
// Use relative URL (BaseAddress is already set)
var response = await client.PostAsJsonAsync(
"/api/payments",
new PaymentRequest { OrderId = order.Id });
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<PaymentResult>();
}
}
Advantages: - ✅ Pre-configured base address and headers - ✅ Consistent configuration across usage - ✅ Easy to update configuration in one place
3. Typed HttpClient (Recommended)¶
Define Typed Client:
public interface IPaymentServiceClient
{
Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request);
Task<PaymentStatus> GetPaymentStatusAsync(string paymentId);
}
public class PaymentServiceClient : IPaymentServiceClient
{
private readonly HttpClient httpClient;
private readonly ILogger<PaymentServiceClient> logger;
public PaymentServiceClient(
HttpClient httpClient,
ILogger<PaymentServiceClient> logger)
{
this.httpClient = httpClient;
this.logger = logger;
}
public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
{
try
{
var response = await this.httpClient.PostAsJsonAsync(
"/api/payments",
request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<PaymentResult>();
}
catch (HttpRequestException ex)
{
this.logger.LogError(
ex,
"Failed to process payment: {PaymentRequest}",
request);
throw;
}
}
public async Task<PaymentStatus> GetPaymentStatusAsync(string paymentId)
{
var response = await this.httpClient.GetAsync($"/api/payments/{paymentId}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<PaymentStatus>();
}
}
Register Typed Client:
// In service registration
services.AddHttpClient<IPaymentServiceClient, PaymentServiceClient>(client =>
{
client.BaseAddress = new Uri("https://payment-service");
client.Timeout = TimeSpan.FromSeconds(30);
client.DefaultRequestHeaders.Add("X-Api-Version", "v1");
})
.AddHeaderPropagation(); // Ensure header propagation
Use Typed Client:
public class OrderService
{
private readonly IPaymentServiceClient paymentServiceClient;
public OrderService(IPaymentServiceClient paymentServiceClient)
{
this.paymentServiceClient = paymentServiceClient;
}
public async Task<PaymentResult> ProcessPaymentAsync(Order order)
{
// Type-safe client usage
return await this.paymentServiceClient.ProcessPaymentAsync(
new PaymentRequest { OrderId = order.Id });
}
}
Advantages: - ✅ Type-safe service contracts - ✅ Encapsulated HTTP logic - ✅ Easy to test (mockable interface) - ✅ Single responsibility principle - ✅ Dependency injection ready
4. Configuration-Based Typed Clients¶
Configuration:
{
"HttpClients": {
"PaymentService": {
"BaseAddress": "https://payment-service",
"Timeout": "00:00:30",
"DefaultHeaders": {
"X-Api-Version": "v1"
}
}
}
}
Register from Configuration:
services.AddHttpClient<IPaymentServiceClient, PaymentServiceClient>((serviceProvider, client) =>
{
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
var section = configuration.GetSection("HttpClients:PaymentService");
client.BaseAddress = new Uri(section["BaseAddress"]);
client.Timeout = TimeSpan.Parse(section["Timeout"]);
var headers = section.GetSection("DefaultHeaders");
foreach (var header in headers.GetChildren())
{
client.DefaultRequestHeaders.Add(header.Key, header.Value);
}
})
.AddHeaderPropagation();
Advanced Configuration¶
Timeout Configuration¶
Client-Level Timeout:
services.AddHttpClient<PaymentServiceClient>(client =>
{
client.Timeout = TimeSpan.FromSeconds(30);
});
Request-Level Timeout:
public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var response = await this.httpClient.PostAsJsonAsync(
"/api/payments",
request,
cts.Token);
return await response.Content.ReadFromJsonAsync<PaymentResult>();
}
Custom Headers¶
Per-Request Headers:
public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
{
var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/payments")
{
Content = JsonContent.Create(request)
};
// Add custom headers
httpRequest.Headers.Add("X-Request-Id", Guid.NewGuid().ToString());
httpRequest.Headers.Add("X-User-Id", userId);
var response = await this.httpClient.SendAsync(httpRequest);
return await response.Content.ReadFromJsonAsync<PaymentResult>();
}
Default Headers (applied to all requests):
services.AddHttpClient<PaymentServiceClient>(client =>
{
client.DefaultRequestHeaders.Add("X-Api-Version", "v1");
client.DefaultRequestHeaders.Add("Accept", "application/json");
// Authorization header (if static)
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", "static-token");
});
Authentication¶
Bearer Token Authentication:
public class PaymentServiceClient
{
private readonly HttpClient httpClient;
private readonly ITokenProvider tokenProvider;
public PaymentServiceClient(
HttpClient httpClient,
ITokenProvider tokenProvider)
{
this.httpClient = httpClient;
this.tokenProvider = tokenProvider;
}
public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
{
// Get token dynamically
var token = await this.tokenProvider.GetTokenAsync();
var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/payments")
{
Content = JsonContent.Create(request)
};
httpRequest.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", token);
var response = await this.httpClient.SendAsync(httpRequest);
return await response.Content.ReadFromJsonAsync<PaymentResult>();
}
}
Using DelegatingHandler:
public class AuthenticationHandler : DelegatingHandler
{
private readonly ITokenProvider tokenProvider;
public AuthenticationHandler(ITokenProvider tokenProvider)
{
this.tokenProvider = tokenProvider;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var token = await this.tokenProvider.GetTokenAsync();
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", token);
return await base.SendAsync(request, cancellationToken);
}
}
// Register handler
services.AddHttpClient<PaymentServiceClient>()
.AddHttpMessageHandler<AuthenticationHandler>()
.AddHeaderPropagation();
Resilience Patterns¶
Retry Policy (using Polly):
services.AddHttpClient<PaymentServiceClient>()
.AddPolicyHandler(GetRetryPolicy())
.AddHeaderPropagation();
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.OrResult(r => (int)r.StatusCode >= 500)
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (outcome, timespan, retryCount, context) =>
{
var logger = context.GetLogger();
logger?.LogWarning(
"Retry {RetryCount} after {Delay}s",
retryCount,
timespan.TotalSeconds);
});
}
Circuit Breaker Policy:
services.AddHttpClient<PaymentServiceClient>()
.AddPolicyHandler(GetCircuitBreakerPolicy())
.AddHeaderPropagation();
private static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.OrResult(r => (int)r.StatusCode >= 500)
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromSeconds(30),
onBreak: (result, duration) =>
{
// Log circuit breaker opened
},
onReset: () =>
{
// Log circuit breaker reset
});
}
Combined Policies:
services.AddHttpClient<PaymentServiceClient>()
.AddPolicyHandler(GetRetryPolicy())
.AddPolicyHandler(GetCircuitBreakerPolicy())
.AddPolicyHandler(Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10)))
.AddHeaderPropagation();
See Resiliency for detailed resilience pattern documentation.
Error Handling¶
Standard Error Handling¶
public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
{
try
{
var response = await this.httpClient.PostAsJsonAsync(
"/api/payments",
request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<PaymentResult>();
}
catch (HttpRequestException ex)
{
this.logger.LogError(
ex,
"HTTP error calling payment service: {Request}",
request);
throw;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
this.logger.LogError(
ex,
"Timeout calling payment service: {Request}",
request);
throw new PaymentServiceTimeoutException("Payment service timeout", ex);
}
catch (JsonException ex)
{
this.logger.LogError(
ex,
"Failed to deserialize payment response");
throw;
}
}
Handling Specific Status Codes¶
public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
{
var response = await this.httpClient.PostAsJsonAsync(
"/api/payments",
request);
switch (response.StatusCode)
{
case HttpStatusCode.OK:
return await response.Content.ReadFromJsonAsync<PaymentResult>();
case HttpStatusCode.BadRequest:
var error = await response.Content.ReadFromJsonAsync<ErrorResponse>();
throw new ValidationException(error.Message);
case HttpStatusCode.NotFound:
throw new PaymentServiceNotFoundException("Payment service not found");
case HttpStatusCode.ServiceUnavailable:
throw new PaymentServiceUnavailableException("Payment service unavailable");
default:
response.EnsureSuccessStatusCode();
return null;
}
}
ProblemDetails Support¶
public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
{
var response = await this.httpClient.PostAsJsonAsync(
"/api/payments",
request);
if (!response.IsSuccessStatusCode)
{
// Try to read ProblemDetails
if (response.Content.Headers.ContentType?.MediaType == "application/problem+json")
{
var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
throw new PaymentServiceException(
problem.Title ?? "Payment service error",
problem);
}
response.EnsureSuccessStatusCode();
}
return await response.Content.ReadFromJsonAsync<PaymentResult>();
}
Testing¶
Unit Testing with Mock HttpClient¶
Using Moq:
[TestMethod]
public async Task ProcessPayment_ShouldReturnResult()
{
// Arrange
var mockHandler = new Mock<HttpMessageHandler>();
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = JsonContent.Create(new PaymentResult { Id = "123" })
};
mockHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(response);
var httpClient = new HttpClient(mockHandler.Object)
{
BaseAddress = new Uri("https://payment-service")
};
var client = new PaymentServiceClient(
httpClient,
Mock.Of<ILogger<PaymentServiceClient>>());
// Act
var result = await client.ProcessPaymentAsync(
new PaymentRequest { OrderId = "order-123" });
// Assert
Assert.IsNotNull(result);
Assert.AreEqual("123", result.Id);
}
Integration Testing with TestServer¶
public class PaymentServiceClientTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> factory;
private readonly HttpClient httpClient;
public PaymentServiceClientTests(WebApplicationFactory<Program> factory)
{
this.factory = factory;
this.httpClient = this.factory.CreateClient();
}
[TestMethod]
public async Task ProcessPayment_ShouldReturnResult()
{
// Arrange
var request = new PaymentRequest { OrderId = "order-123" };
// Act
var response = await this.httpClient.PostAsJsonAsync(
"/api/payments",
request);
// Assert
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<PaymentResult>();
Assert.IsNotNull(result);
}
}
Testing with HttpMock¶
Using RichardSzalay.MockHttp:
[TestMethod]
public async Task ProcessPayment_ShouldReturnResult()
{
// Arrange
var mockHttp = new MockHttpMessageHandler();
mockHttp
.When("https://payment-service/api/payments")
.Respond("application/json", JsonSerializer.Serialize(new PaymentResult { Id = "123" }));
var httpClient = mockHttp.ToHttpClient();
httpClient.BaseAddress = new Uri("https://payment-service");
var client = new PaymentServiceClient(
httpClient,
Mock.Of<ILogger<PaymentServiceClient>>());
// Act
var result = await client.ProcessPaymentAsync(
new PaymentRequest { OrderId = "order-123" });
// Assert
Assert.IsNotNull(result);
Assert.AreEqual("123", result.Id);
}
Best Practices¶
Do's¶
-
Always Use IHttpClientFactory
-
Use Typed Clients for Service Clients
-
Enable Header Propagation
-
Configure Timeouts
-
Handle Errors Gracefully
-
Use Service Discovery for Logical Names
-
Log HTTP Requests
Don'ts¶
-
Don't Dispose HttpClient from Factory
-
Don't Create HttpClient Directly
-
Don't Modify BaseAddress After Creation
// ❌ BAD - Thread safety issues var client = httpClientFactory.CreateClient("PaymentService"); client.BaseAddress = new Uri("https://other-service"); // ✅ GOOD - Configure during registration services.AddHttpClient("PaymentService", client => { client.BaseAddress = new Uri("https://payment-service"); }); -
Don't Store HttpClient in Fields
// ❌ BAD - HttpClient is not thread-safe for all operations public class BadService { private readonly HttpClient client; public BadService(IHttpClientFactory factory) { this.client = factory.CreateClient(); // Don't store } } // ✅ GOOD - Create per request or use typed client public class GoodService { private readonly IHttpClientFactory factory; public async Task CallApi() { var client = this.factory.CreateClient(); // Create per request } } -
Don't Ignore Cancellation Tokens
Troubleshooting¶
Issue: Socket Exhaustion¶
Symptoms: SocketException with "Too many open files" or connection timeout errors.
Solution:
- ✅ Use IHttpClientFactory instead of direct HttpClient instantiation
- ✅ Don't dispose HttpClient instances from factory
- ✅ Use typed clients for service clients
Issue: Headers Not Propagated¶
Symptoms: Missing correlation/trace headers in outgoing requests.
Solution:
- ✅ Verify AddHeaderPropagation() is called on HttpClient registration
- ✅ Ensure UseMicroserviceHeaderPropagation() middleware is registered
- ✅ Check middleware order (header propagation must be after routing)
Issue: Service Discovery Not Working¶
Symptoms: HttpRequestException with "No such host" or DNS resolution errors.
Solution:
- ✅ Verify service discovery is enabled in configuration
- ✅ Check service endpoint configuration in appsettings.json
- ✅ Ensure service name matches configuration key
- ✅ Verify DNS provider is configured correctly (if using DNS provider)
Issue: Timeout Errors¶
Symptoms: TaskCanceledException with timeout errors.
Solution: - ✅ Configure appropriate timeout on HttpClient - ✅ Use request-level timeout for long-running operations - ✅ Consider increasing timeout for specific operations - ✅ Check network connectivity and service availability
Issue: Connection Pool Exhaustion¶
Symptoms: Requests hanging or slow response times.
Solution:
- ✅ Verify IHttpClientFactory is used
- ✅ Check for connection leaks (disposing HttpClient incorrectly)
- ✅ Monitor connection pool metrics
- ✅ Consider increasing ServicePointManager.DefaultConnectionLimit
Summary¶
HTTP Client in the ConnectSoft Microservice Template provides:
- ✅ IHttpClientFactory Integration: Proper lifecycle management and connection pooling
- ✅ Service Discovery: Automatic endpoint resolution via configuration or DNS
- ✅ Header Propagation: Automatic correlation and tracing header propagation
- ✅ OpenTelemetry Instrumentation: Automatic distributed tracing
- ✅ Type Safety: Typed client support for strongly-typed service clients
- ✅ Resilience Patterns: Support for retry, circuit breaker, and timeout policies
- ✅ Best Practices: Socket exhaustion prevention and proper resource management
By following these patterns, teams can:
- Build Reliable Services: Proper HTTP client lifecycle management prevents socket exhaustion
- Enable Observability: Automatic tracing and correlation for distributed systems
- Simplify Configuration: Service discovery eliminates hardcoded endpoints
- Ensure Consistency: Typed clients provide type-safe, testable service clients
- Handle Failures Gracefully: Resilience patterns and error handling for production readiness
The HTTP client infrastructure ensures that ConnectSoft microservices can reliably communicate with external services while maintaining observability, consistency, and proper resource management.