
Quando l’integrazione HTTP è "semplice" (CRUD, filtri via query string, body tipizzati), scrivere client a mano è uno spreco: boilerplate per serializzazione, gestione degli errori, mapping query/route, header ripetitivi. Refit risolve il problema definendo l’API come interfaccia; il client viene generato a compile-time (source generator), funziona con HttpClientFactory e non richiede reflection invasiva.
Refit è efficace quando:
Se l’API è iper-dinamica (query arbitraria, payload eterogenei), o se devi generare il client da uno schema OpenAPI complicato, conviene valutare un wrapper scritto a mano.
Partiamo dal contratto. Definisci con precisione route, query, body, status attesi. Evita overload ambigui e mantieni i DTO nullability aware (nullable reference types attivi).
1// contracts/IPaymentsApi.cs2using Refit;34public interface IPaymentsApi5{6 [Get("/v1/payments/{id}")]7 Task<ApiResponse<PaymentDto>> GetAsync(string id, CancellationToken ct = default);89 [Get("/v1/payments")]10 Task<ApiResponse<Page<PaymentDto>>> ListAsync([Query] int Page, [Query] int PageSize, [Query] PaymentStatus? Status, CancellationToken ct = default);1112 [Post("/v1/payments")]13 Task<ApiResponse<PaymentDto>> CreateAsync([Body] CreatePayment cmd, CancellationToken ct = default);1415 [Get("/v1/reports/{id}")]16 Task<Stream> DownloadReportAsync(string id, CancellationToken ct = default);17}1819public sealed record PaymentDto(string Id, string Status, decimal Amount, string Currency);20public sealed record Page<T>(IReadOnlyList<T> Items, int Page, int PageSize, int Total);21public sealed record CreatePayment(decimal Amount, string Currency, string CustomerId);
Note:
Una registrazione sensata evita problemi in produzione (timeout, retry aggressivi, circuit breaker rumorosi). Qui la versione “standard” che usiamo come baseline.
1// Program.cs2var jsonOptions = new JsonSerializerOptions3{4 PropertyNamingPolicy = JsonNamingPolicy.CamelCase,5 DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull6};7jsonOptions.Converters.Add(new JsonStringEnumConverter());89var refitSettings = new RefitSettings10{11 ContentSerializer = new SystemTextJsonContentSerializer(jsonOptions)12};1314builder.Services.AddTransient<AuthDelegatingHandler>(); // DelegatingHandler custom1516builder.Services17 .AddRefitClient<IPaymentsApi>(refitSettings)18 .ConfigureHttpClient(c =>19 {20 c.BaseAddress = new Uri(builder.Configuration["Payments:BaseUrl"]!);21 c.DefaultRequestHeaders.UserAgent.ParseAdd("webion-payments/1.0");22 })23 .AddHttpMessageHandler<AuthDelegatingHandler>()24 .AddStandardResilienceHandler(o =>25 {26 o.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(10);27 o.Retry.MaxRetryAttempts = 3;28 o.Retry.BackoffType = DelayBackoffType.Exponential;29 o.Retry.UseJitter = true;30 o.CircuitBreaker.MinimumThroughput = 10;31 o.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30);32 o.CircuitBreaker.FailureRatio = 0.5;33 o.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(15);34 });
Dettagli che contano:
Non mescolare auth nei metodi Refit: introduci un DelegatingHandler. Questo consente rotazione token, correlazione, metriche uniformi.
1public sealed class AuthDelegatingHandler : DelegatingHandler2{3 private readonly IExampleTokenGenerator _tokenGenerator;45 public AuthDelegatingHandler(IExampleTokenGenerator tokenGenerator)6 {7 _tokenGenerator = tokenGenerator;8 }910 protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)11 {12 // Your logic to retrieve auth token13 var token = _tokenGenerator.GenerateTokenAsync(cancellationToken);1415 request.Headers.Add("Authorization", $"Bearer {token}");16 return await base.SendAsync(request, cancellationToken);17 }18}
Con ritorno Task<T> Refit lancia ApiException su 4xx/5xx.
Con Task<ApiResponse<T>> non lancia e ti consegna status + body.
1// Con Task<ApiResponse<T>>2var res = await _api.GetAsync(id);3if (!res.IsSuccessStatusCode)4{5 // Gestione errore6}78return res.Content;910// Con Task<T>11try12{13 var res = await _api.GetAsync(id, ct);14 return res.Content!;15}16catch (ApiException ex)17{18 // Gestione errore19}
Puoi decidere di usare ciò che ti è più comodo in base alla situazione.
Se il provider utilizza header custom, puoi impostarli con un attributo.
1[Headers("Accept: application/json", "X-Api-Version: 1")]2public interface IPaymentsApi { /* ... */ }
Per esempio se la versione è via header puoi impostarlo così.
Se invece la versione è in path (/v2/...), modellala direttamente nelle route.
Si può decidere anche di non avere direttamente una sola interfaccia a rappresentare l'intera API, ma di creare un'interfaccia wrapper per il client che venga suddivisa nelle diverse componenti dell'API, usando RestServices.For<T>.
1// ApplicationManagerClient.cs2public sealed class ApplicationManagerClient : IApplicationManagerClient3{4 private readonly HttpClient _client;56 public ApplicationManagerClient(HttpClient client)7 {8 _client = client;9 }1011 public IApplicationsApi Applications => RestService.For<IApplicationsApi>(_client);12 public ISitesApi Sites => RestService.For<ISitesApi>(_client);13 public IAppPoolsApi AppPools => RestService.For<IAppPoolsApi>(_client);14}1516// IApplicationManagerClient.cs17public interface IApplicationManagerClient18{19 public IApplicationsApi Applications { get; }20 public ISitesApi Sites { get; }21 public IAppPoolsApi AppPools { get; }22}2324// program.cs25services.AddHttpClient<IApplicationManagerClient, ApplicationManagerClient>();
Refit consente di codificare l’integrazione HTTP come contratto C# e delegare al runtime i dettagli meccanici: instradamento, serializzazione, mapping query/route. L’integrazione con HttpClientFactory e i resilience handler di .NET 8 copre i requisiti reali (timeout, retry con jitter, circuit breaker). Mantieni l’interfaccia piccola, definisci DTO solidi e presidia errori/telemetria.