C# (.NET)
Recomendação para .NET 8 ou 9: usar o pacote Duende.AccessTokenManagement (mantido pela Duende, ex-IdentityServer). Ele gerencia o cache de token, renovação automática e integração com IHttpClientFactory. Para projetos sem injeção de dependência, mostro também a versão manual.
Pré-requisitos
- .NET 8 ou superior.
- Credenciais em variáveis de ambiente ou em um secret store.
setx BANQER_TENANT "sua-empresa"
setx BANQER_CLIENT_ID "..."
setx BANQER_CLIENT_SECRET "..."
Versão recomendada: Duende.AccessTokenManagement
dotnet add package Duende.AccessTokenManagement
Setup (Program.cs)
using System.Net;
var tenant = Environment.GetEnvironmentVariable("BANQER_TENANT")
?? throw new InvalidOperationException("BANQER_TENANT nao definido");
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDistributedMemoryCache(); // cache de token em memoria
builder.Services.AddClientCredentialsTokenManagement()
.AddClient("banqer", c =>
{
c.TokenEndpoint = $"https://auth.banqer.com.br/realms/{tenant}/protocol/openid-connect/token";
c.ClientId = Environment.GetEnvironmentVariable("BANQER_CLIENT_ID");
c.ClientSecret = Environment.GetEnvironmentVariable("BANQER_CLIENT_SECRET");
});
builder.Services.AddClientCredentialsHttpClient("banqer-api", "banqer", client =>
{
client.BaseAddress = new Uri($"https://api-{tenant}.banqer.com.br");
client.Timeout = TimeSpan.FromSeconds(30);
})
// Retry transitorio com Polly (opcional mas recomendado).
.AddStandardResilienceHandler();
var app = builder.Build();
Uso
public class CredorService
{
private readonly IHttpClientFactory _factory;
public CredorService(IHttpClientFactory factory) => _factory = factory;
public async Task<CredoresResponse?> ListarCredoresAsync(CancellationToken ct)
{
var http = _factory.CreateClient("banqer-api");
// O token e injetado automaticamente pelo handler do Duende.
return await http.GetFromJsonAsync<CredoresResponse>("/v1/credores", ct);
}
}
public record Credor(int IdCredor, string Nome, string? NomeSms);
public record CredoresResponse(IReadOnlyList<Credor> Credores);
Use camelCase no JSON da API e PascalCase em C#: configure o JsonSerializerOptions global com PropertyNamingPolicy = JsonNamingPolicy.CamelCase, ou marque cada propriedade com [JsonPropertyName("idCredor")].
Versão manual (sem Duende, .NET stdlib)
Para consoles simples, scripts ou worker services sem DI:
using System.Net.Http.Headers;
using System.Net.Http.Json;
public sealed class BanqerClient : IDisposable
{
private readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(30) };
private readonly string _authUrl;
private readonly string _apiBase;
private readonly string _clientId;
private readonly string _clientSecret;
private string? _token;
private DateTimeOffset _tokenExp = DateTimeOffset.MinValue;
private readonly SemaphoreSlim _tokenLock = new(1, 1);
public BanqerClient(string tenant, string clientId, string clientSecret)
{
_apiBase = $"https://api-{tenant}.banqer.com.br";
_authUrl = $"https://auth.banqer.com.br/realms/{tenant}/protocol/openid-connect/token";
_clientId = clientId;
_clientSecret = clientSecret;
}
private async Task<string> GetTokenAsync(CancellationToken ct)
{
if (_token is not null && DateTimeOffset.UtcNow < _tokenExp) return _token;
await _tokenLock.WaitAsync(ct);
try
{
if (_token is not null && DateTimeOffset.UtcNow < _tokenExp) return _token;
using var form = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "client_credentials",
["client_id"] = _clientId,
["client_secret"] = _clientSecret,
});
using var res = await _http.PostAsync(_authUrl, form, ct);
res.EnsureSuccessStatusCode();
var body = await res.Content.ReadFromJsonAsync<TokenResponse>(ct);
_token = body!.access_token;
_tokenExp = DateTimeOffset.UtcNow.AddSeconds(body.expires_in * 0.8); // 80% do TTL
return _token;
}
finally
{
_tokenLock.Release();
}
}
public async Task<T?> GetAsync<T>(string path, CancellationToken ct = default)
{
var attempt = 0;
var refreshed = false;
while (true)
{
attempt++;
var token = await GetTokenAsync(ct);
using var req = new HttpRequestMessage(HttpMethod.Get, _apiBase + path);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
using var res = await _http.SendAsync(req, ct);
if (res.StatusCode == HttpStatusCode.Unauthorized && !refreshed)
{
_token = null;
refreshed = true;
continue;
}
if ((int)res.StatusCode == 429 && attempt < 3)
{
var wait = res.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(5);
await Task.Delay(wait, ct);
continue;
}
if ((int)res.StatusCode >= 500 && attempt < 3)
{
var wait = TimeSpan.FromMilliseconds(
Math.Min(Math.Pow(2, attempt) * 1000 + Random.Shared.NextDouble() * 1000, 30_000));
await Task.Delay(wait, ct);
continue;
}
res.EnsureSuccessStatusCode();
return await res.Content.ReadFromJsonAsync<T>(ct);
}
}
public void Dispose() => _http.Dispose();
private record TokenResponse(string access_token, int expires_in);
}
Uso da versão manual
var tenant = Environment.GetEnvironmentVariable("BANQER_TENANT")!;
var clientId = Environment.GetEnvironmentVariable("BANQER_CLIENT_ID")!;
var clientSecret = Environment.GetEnvironmentVariable("BANQER_CLIENT_SECRET")!;
using var banqer = new BanqerClient(tenant, clientId, clientSecret);
var credores = await banqer.GetAsync<CredoresResponse>("/v1/credores");
foreach (var c in credores!.Credores)
Console.WriteLine($"{c.IdCredor} - {c.Nome}");
Erros comuns
| Sintoma | Causa provável |
|---|---|
HttpRequestException: 401 em loop | Cliente reentrando sem o refreshed guard. Use a versão acima ou Duende. |
Propriedades chegando null no record | JSON em camelCase mas record em PascalCase sem opções configuradas. Configure JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase. |
SocketException no cold start | Sem DNS resolvido. Aumente Timeout na primeira chamada ou pre-resolva o host. |