ASP.NET Core's `IHttpClientFactory` is the recommended way to make HTTP calls from .NET services — it manages connection pooling, avoids socket exhaustion, and makes the HttpClient easy to mock in tests. This guide builds a typed `VatApiClient` class that wraps the TaxID API, registers it in the DI container, and wires it into a minimal API endpoint. For the use-case guide, see the .NET VAT validation use case.
Response Model
namespace VatValidation.Models;
public record VatResponse(
bool Valid,
string Status,
string Vat,
string CountryCode,
string? CompanyName,
string? Address,
bool Cached,
string RequestId
);Typed HttpClient Service
using System.Net.Http.Json;
using VatValidation.Models;
namespace VatValidation.Services;
public class VatApiClient
{
private readonly HttpClient _http;
public VatApiClient(HttpClient http) => _http = http;
public async Task<VatResponse?> ValidateAsync(
string vatNumber,
CancellationToken ct = default)
{
var normalised = vatNumber.Replace(" ", "").ToUpperInvariant();
var country = normalised[..2];
try
{
return await _http.GetFromJsonAsync<VatResponse>(
$"/api/v1/validate/{country}/{normalised}", ct);
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
{
return new VatResponse(false, "service_unavailable",
normalised, country, null, null, false, string.Empty);
}
}
}DI Registration in Program.cs
using VatValidation.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient<VatApiClient>(client =>
{
client.BaseAddress = new Uri("https://taxid.dev");
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue(
"Bearer",
builder.Configuration["TaxId:ApiKey"]);
client.Timeout = TimeSpan.FromSeconds(6);
});
var app = builder.Build();
// Minimal API endpoint:
app.MapPost("/api/validate-vat", async (ValidateVatRequest req, VatApiClient vatClient) =>
{
if (string.IsNullOrWhiteSpace(req.VatNumber))
return Results.BadRequest(new { error = "vat_number_required" });
var result = await vatClient.ValidateAsync(req.VatNumber);
if (result is null) return Results.Problem("API error");
return result.Status switch
{
"active" => Results.Ok(new { result.Valid, result.CompanyName, result.Address }),
"inactive" => Results.UnprocessableEntity(new { error = "vat_inactive" }),
"format_invalid" => Results.UnprocessableEntity(new { error = "format_invalid" }),
_ => Results.Ok(new { result.Valid, Unavailable = true })
};
});
app.Run();
record ValidateVatRequest(string VatNumber);xUnit Tests with Mocked HttpMessageHandler
using System.Net;
using System.Net.Http.Json;
using Microsoft.Extensions.DependencyInjection;
using VatValidation.Services;
using Xunit;
public class VatApiClientTests
{
private VatApiClient CreateClient(HttpStatusCode status, object responseBody)
{
var handler = new MockHttpMessageHandler(status, responseBody);
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("https://taxid.dev") };
return new VatApiClient(httpClient);
}
[Fact]
public async Task ReturnsValidForActiveNumber()
{
var client = CreateClient(HttpStatusCode.OK, new
{
valid = true, status = "active", company_name = "Acme GmbH",
vat = "DE123456789", country_code = "DE",
address = "Berlin", cached = false, request_id = "r1"
});
var result = await client.ValidateAsync("DE123456789");
Assert.NotNull(result);
Assert.True(result!.Valid);
Assert.Equal("active", result.Status);
Assert.Equal("Acme GmbH", result.CompanyName);
}
[Fact]
public async Task ReturnsUnavailableOnNetworkError()
{
var handler = new ThrowingHttpMessageHandler(new HttpRequestException("timeout"));
var client = new VatApiClient(new HttpClient(handler) { BaseAddress = new Uri("https://taxid.dev") });
var result = await client.ValidateAsync("DE123456789");
Assert.NotNull(result);
Assert.Equal("service_unavailable", result!.Status);
Assert.False(result.Valid);
}
}Related guides
Start validating EU VAT numbers
Free plan — 100 validations/month. No credit card required.