Utiliser Refit avec IHttpClientFactory pour créer des clients HTTP lisibles, résilients et correctement isolés du domaine dans une application .NET moderne.

Refit en .NET : concevoir des clients HTTP fiables
Refit transforme un contrat HTTP en interface .NET. Les routes, verbes, paramètres et corps de requête deviennent explicites, tandis que la bibliothèque génère l'implémentation du client.
Ce gain de lisibilité ne rend pas l'intégration fiable par lui-même. Une API distante reste un système partiellement disponible, versionné par une autre équipe et capable de retourner des données ou des erreurs inattendues. Refit décrit le protocole ; l'application doit encore gérer la durée de vie du client, l'authentification, les timeouts, les retries et la traduction vers son propre modèle.
Installer uniquement les packages nécessaires
Dans une application utilisant l'injection de dépendances et IHttpClientFactory, installe Refit et son intégration dédiée :
dotnet add package Refit
dotnet add package Refit.HttpClientFactory
dotnet add package Microsoft.Extensions.Http.Resilience
Le dernier package n'est pas obligatoire pour Refit. Il fournit les stratégies modernes de résilience HTTP de .NET lorsque l'intégration en a réellement besoin.
Décrire le contrat HTTP, pas le domaine
Supposons qu'une application doive interroger un fournisseur de paiement :
public sealed record ProviderPaymentRequest(
string OrderReference,
long AmountInMinorUnits,
string Currency);
public sealed record ProviderPaymentResponse(
string PaymentId,
string Status,
string? FailureCode);
public interface IPaymentProviderApi
{
[Post("/v1/payments")]
Task<ApiResponse<ProviderPaymentResponse>> CreatePaymentAsync(
[Body] ProviderPaymentRequest request,
[Header("Idempotency-Key")] string idempotencyKey,
CancellationToken cancellationToken);
[Get("/v1/payments/{paymentId}")]
Task<ApiResponse<ProviderPaymentResponse>> GetPaymentAsync(
string paymentId,
CancellationToken cancellationToken);
}
Ces DTO représentent le contrat du fournisseur. Ils ne doivent pas devenir les objets métier de l'application. Si le fournisseur renomme PaymentId ou ajoute un statut, le changement doit rester contenu dans la couche d'intégration.
Le CancellationToken placé en paramètre est transmis à l'appel HTTP. Cette propagation est indispensable pour arrêter le travail lorsque la requête entrante est annulée ou que le service s'arrête.
Choisir le type de retour selon la gestion d'erreur
Avec Task<T>, Refit désérialise une réponse réussie et lève une ApiException pour une réponse HTTP en erreur. Avec Task<ApiResponse<T>>, le code appelant reçoit le statut, les en-têtes, le contenu et l'erreur éventuelle dans un résultat structuré.
ApiResponse<T> est utile lorsqu'une couche d'intégration doit traduire précisément plusieurs statuts HTTP. Il ne faut toutefois pas propager ce type dans toute l'application : cela couplerait les cas d'usage à Refit.
Configurer le client avec IHttpClientFactory
La configuration centralise l'adresse, les en-têtes stables et la durée maximale d'un appel :
builder.Services
.AddRefitClient<IPaymentProviderApi>()
.ConfigureHttpClient((services, client) =>
{
var options = services
.GetRequiredService<IOptions<PaymentProviderOptions>>()
.Value;
client.BaseAddress = options.BaseUrl;
client.Timeout = TimeSpan.FromSeconds(10);
client.DefaultRequestHeaders.UserAgent.ParseAdd("YvaDev.Payments/1.0");
})
.RedactLoggedHeaders("Authorization", "X-Api-Key");
IHttpClientFactory gère la mise en commun et le renouvellement des handlers sous-jacents. Le client Refit typé est transient et doit rester court. Le capturer dans un singleton peut empêcher le renouvellement attendu du handler et la prise en compte de changements DNS.
L'adresse et les secrets ne doivent pas être écrits dans le code. Valide les options au démarrage :
public sealed class PaymentProviderOptions
{
public const string SectionName = "PaymentProvider";
public required Uri BaseUrl { get; init; }
public required string ApiKey { get; init; }
}
builder.Services
.AddOptions<PaymentProviderOptions>()
.BindConfiguration(PaymentProviderOptions.SectionName)
.Validate(options => options.BaseUrl.IsAbsoluteUri)
.Validate(options => !string.IsNullOrWhiteSpace(options.ApiKey))
.ValidateOnStart();
Ajouter l'authentification dans un handler
Un DelegatingHandler applique une préoccupation HTTP commune sans polluer chaque méthode Refit :
public sealed class PaymentAuthenticationHandler(
IOptions<PaymentProviderOptions> options)
: DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
request.Headers.TryAddWithoutValidation(
"X-Api-Key",
options.Value.ApiKey);
return base.SendAsync(request, cancellationToken);
}
}
Enregistre ensuite le handler dans la chaîne :
builder.Services.AddTransient<PaymentAuthenticationHandler>();
builder.Services
.AddRefitClient<IPaymentProviderApi>()
.ConfigureHttpClient(client =>
client.BaseAddress = new Uri("https://payments.example.com"))
.AddHttpMessageHandler<PaymentAuthenticationHandler>();
Dans le projet réel, conserve une seule inscription complète du client. Les deux extraits séparent volontairement les responsabilités pour la lecture.
Si le jeton expire et doit être renouvelé, le handler peut dépendre d'un fournisseur de jetons. Ce composant doit synchroniser les renouvellements concurrents et ne jamais journaliser la valeur du secret.
Encadrer les retries et les timeouts
Le handler standard de résilience de .NET combine limitation de concurrence, timeout total, retry, circuit breaker et timeout par tentative :
builder.Services
.AddRefitClient<IPaymentProviderApi>()
.ConfigureHttpClient(client =>
client.BaseAddress = new Uri("https://payments.example.com"))
.AddStandardResilienceHandler();
Les valeurs par défaut ne remplacent pas une décision d'architecture. Avant d'activer un retry, réponds à trois questions :
- l'erreur est-elle réellement transitoire ?
- l'opération peut-elle être rejouée sans créer un second effet ?
- le temps total reste-t-il compatible avec le délai du cas d'usage ?
Un GET est généralement rejouable. Un POST de paiement ne l'est que si le fournisseur garantit l'idempotence, par exemple avec une clé stable pour l'opération. Sans cette garantie, un timeout après l'envoi laisse une situation ambiguë : le paiement a peut-être été créé même si la réponse n'est jamais arrivée.
Évite aussi l'empilement de retries. Si Refit, un proxy, le service appelant et un orchestrateur réessaient chacun trois fois, une panne peut multiplier brutalement la charge sur le fournisseur.
Traduire l'intégration derrière une frontière applicative
Une gateway protège le domaine des DTO et des statuts du fournisseur :
public sealed class PaymentGateway(IPaymentProviderApi api)
: IPaymentGateway
{
public async Task<PaymentResult> AuthorizeAsync(
PaymentAttempt attempt,
CancellationToken cancellationToken)
{
var request = new ProviderPaymentRequest(
attempt.OrderNumber,
attempt.Amount.MinorUnits,
attempt.Amount.Currency);
var response = await api.CreatePaymentAsync(
request,
attempt.IdempotencyKey,
cancellationToken);
if (response.IsSuccessful && response.Content is not null)
{
return Map(response.Content);
}
return response.StatusCode switch
{
HttpStatusCode.BadRequest => PaymentResult.Rejected(
"provider_request_rejected"),
HttpStatusCode.TooManyRequests => PaymentResult.RetryLater(),
_ => PaymentResult.ProviderUnavailable()
};
}
}
La traduction doit préserver les informations utiles sans exposer les détails internes ou sensibles du fournisseur. Une réponse 400 peut représenter une erreur définitive, tandis qu'un 429 indique une limitation temporaire. Les traiter de la même manière rendrait les retries inefficaces ou dangereux.
Observer sans divulguer les données
Pour chaque dépendance HTTP, mesure :
- la durée et ses percentiles ;
- le statut HTTP ;
- le nombre de timeouts ;
- les ouvertures de circuit ;
- les tentatives supplémentaires ;
- le taux d'erreur par opération stable.
Évite d'utiliser un identifiant client ou une URL complète comme dimension de métrique. Redacte les en-têtes d'authentification et ne journalise pas les corps par défaut : ils peuvent contenir des données personnelles, des jetons ou des informations de paiement.
Tester le contrat et la traduction
Les tests unitaires de PaymentGateway peuvent utiliser une implémentation contrôlée de IPaymentProviderApi pour vérifier la traduction des résultats. Un test d'intégration doit ensuite exercer le vrai pipeline HTTP contre un serveur local simulé : sérialisation JSON, en-têtes, routes, statuts et annulation.
Ces tests détectent des erreurs que la compilation ne voit pas : nom JSON différent, format de date, paramètre oublié ou corps d'erreur incompatible. Pour une API importante, complète-les par un test contractuel ou une validation dans l'environnement sandbox du fournisseur.
Quand ne pas utiliser Refit
Refit apporte peu de valeur lorsque :
- le client ne contient qu'un appel trivial et stable ;
- le protocole n'est pas principalement REST/HTTP ;
- le streaming exige un contrôle très fin de la réponse ;
- la signature de requête dépend d'un traitement bas niveau complexe ;
- l'équipe doit maîtriser précisément chaque étape de sérialisation.
Un client écrit directement avec HttpClient reste parfois plus explicite. Le bon critère n'est pas le nombre de lignes économisées, mais la lisibilité du contrat et la capacité à diagnostiquer l'intégration.
Checklist avant la production
- Les DTO externes restent-ils dans la couche d'intégration ?
- L'adresse et les secrets sont-ils validés au démarrage ?
- Le client typé évite-t-il d'être capturé par un singleton ?
- L'annulation traverse-t-elle toutes les méthodes ?
- Les timeouts couvrent-ils le budget total du cas d'usage ?
- Chaque retry est-il limité à une opération rejouable ?
- Les erreurs HTTP sont-elles traduites en résultats applicatifs ?
- Les en-têtes sensibles sont-ils masqués dans les logs ?
- Le contrat JSON est-il couvert par un test d'intégration ?
Conclusion
Refit rend un contrat REST lisible, typé et facile à enregistrer avec IHttpClientFactory. Sa responsabilité s'arrête là.
Une intégration fiable exige encore une frontière applicative, des DTO externes isolés, une authentification maîtrisée, des délais bornés, des retries idempotents et une observabilité qui ne divulgue pas les données. Utilisé dans ce cadre, Refit réduit le bruit sans masquer les risques du réseau.
Pour approfondir, consulte le dépôt officiel Refit, la documentation d'IHttpClientFactory et les stratégies de résilience HTTP .NET.
