Refit 使用紀錄
Refit, HttpClient 應用簡化方案之一。WASM, Blazor WebAssembly,
引言
因為 HttpClient 很難用,就出現了相應的方案來降低 HttpClient 叫用 REST API 的工作量。
開發環境
平台:.NET6
骨架:Blazor WASM App
IDE:Visual Studio 2022
套件:Refit.HttpClientFactory
關鍵原碼紀錄
前端範例 - 應用
@page "/todo"
@inject ITodoApi bizApi // 注入 Refit Client
<PageTitle>Counter</PageTitle>
...省略(render page)...
@code {
List<TodoDto> dataList = new();
string errMsg = string.Empty;
protected override async Task OnInitializedAsync()
{
await HandleQuery();
}
async Task HandleQuery()
{
try
{
var qryArgs = new TodoQryAgs{
Msg = f_testFail ? "測試邏輯失敗" : "今天天氣真好",
Amt = 999
};
dataList = await bizApi.QryDataListAsync(qryArgs);
/// 用 Refit Client 叫用後端 REST API。
}
catch (ApiException exx)
{
/// Refit 叫用失敗時會傳回 ApiException。
var err = await exx.GetContentAsAsync<ErrMsg>();
errMsg = $"Refit.ApiException: {err.Severity}-{err.Message}";
}
catch (Exception ex)
{
errMsg = "EXCEPTION: " + ex.Message;
}
}
}
當然要在前端先定義好 Refit Client 介面
using Refit;
using SmallEco.DTO;
namespace SmallEco.Client.RefitClient;
public interface ITodoApi
{
[Post("/api/Todo/QryDataList")]
Task<List<TodoDto>> QryDataListAsync(TodoQryAgs args);
}
後端 REST API 範例
using Microsoft.AspNetCore.Mvc;
using SmallEco.DTO;
using Swashbuckle.AspNetCore.Annotations;
namespace SmallEco.Server.Controllers;
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
List<TodoDto> _simsTodoRepo = new()
{
new() { Sn = 1, Description = "今天天氣真好", Done = false, CreateDtm = DateTime.Now.AddDays(-3) },
new() { Sn = 2, Description = "下午去看電影", Done = false, CreateDtm = DateTime.Now.AddDays(-2) },
new() { Sn = 3, Description = "晚上吃大餐", Done = false, CreateDtm = DateTime.Now.AddDays(-1) }
};
[HttpPost("[action]")]
[SwaggerResponse(200, type: typeof(List<TodoDto>))]
[SwaggerResponse(400, type: typeof(ErrMsg))]
public IActionResult QryDataList(TodoQryAgs args)
{
if(args.Msg == "測試邏輯失敗")
{
return BadRequest(new ErrMsg("這是邏輯失敗!"));
}
return Ok(_simsTodoRepo);
}
}
※ Refit API 之 NULL Response 問題處置 --- 補充 on 230919
ASP.NET Core Web API 的 HTTP Response 值為 null 時會自動轉換成 204 NoContent,導致 Refet API 解析 JSON 反序列化會失敗!下面指令將禁止自動把 null 轉換成 204 NoContent。
builder.Services.AddControllersWithViews(opt => // or AddControllers(), AddMvc()
{
//## 禁止自動把 HTTP Response 為 null 時轉換成 204 NoContent。因為 System.Text.Json 無法對 204 NoContent 解序列化!
// 此設定應該只適用於 Web API。
// remove formatter that turns nulls into 204 - No Content responses
opt.OutputFormatters.RemoveType<HttpNoContentOutputFormatter>();
});
[...略...]
在前端註冊成 DI Service
...省略...
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
/// 記得要預先註冊好 HttpClient
//## 註冊 RefitClient API。 --- 偵測 Refit Client 並自動註冊
var asm = Assembly.GetAssembly(typeof(App));
foreach (var refitClientType in asm.GetTypes().Where(
c => c.Namespace == "SmallEco.Client.RefitClient"
&& c.IsInterface
&& c.Name.EndsWith("Api")))
{
builder.Services.AddRefitClient(refitClientType)
.ConfigureHttpClient(http => http.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));
}
////## 註冊 RefitClient API。 --- 手動一個一個註冊
//builder.Services
// .AddRefitClient<ITodoApi>()
// .ConfigureHttpClient(http => http.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
// .AddHttpMessageHandler<AuthHeaderHandler>();
註冊 AuthHeaderHandler --- 補充 on 230919
使個 Refit API 自動加掛 Bearer Token。
using Blazored.SessionStorage;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http.Headers;
using System.Net.Http.Json;
namespace YourProject.Client.Authentication;
/// <summary>
/// Custom delegating handler for adding Auth headers to outbound requests
/// </summary>
class AuthHeaderHandler : DelegatingHandler
{
readonly CustomAuthenticationStateProvider authProvider;
readonly IWebAssemblyHostEnvironment env;
public AuthHeaderHandler(AuthenticationStateProvider authBase, IWebAssemblyHostEnvironment _env)
{
authProvider = (CustomAuthenticationStateProvider)authBase;
env = _env;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
//## 自 token 存放庫取得
string token = await authProvider.GetTokenAsync();
//※ potentially refresh token here if it has expired etc.
if(true /* token 快過期了 */)
{
AuthToken? newToken = await RefreshTokenAsync(token);
if (newToken != null)
{
//# 更新登入狀態
await authProvider.UpdateTokenAsync(newToken);
//# 更新 bearer token
token = newToken.Token;
}
}
//## GO
if (!String.IsNullOrWhiteSpace(token))
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var resp = await base.SendAsync(request, cancellationToken);
return resp;
}
protected async Task<AuthToken?> RefreshTokenAsync(string token)
{
try
{
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "/api/Account/RefreshToken");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
HttpClient http = new HttpClient { BaseAddress = new Uri(env.BaseAddress) };
var resp = await http.SendAsync(request);
if (resp.IsSuccessStatusCode)
{
var newToken = await resp.Content.ReadFromJsonAsync<AuthToken>();
return newToken;
}
//# for DEBUG
//string? reason = resp.ReasonPhrase;
//string? respContent = await resp.Content.ReadAsStringAsync();
return null;
}
catch(Exception ex)
{
//# for DEBUG
//string? reason = ex.Message;
return null;
}
}
}
(EOF)
Last updated