Pular para o conteúdo principal

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

SintomaCausa provável
HttpRequestException: 401 em loopCliente reentrando sem o refreshed guard. Use a versão acima ou Duende.
Propriedades chegando null no recordJSON em camelCase mas record em PascalCase sem opções configuradas. Configure JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase.
SocketException no cold startSem DNS resolvido. Aumente Timeout na primeira chamada ou pre-resolva o host.