Refit 使用紀錄

Refit, HttpClient 應用簡化方案之一。WASM, Blazor WebAssembly,

引言

因為 HttpClient 很難用,就出現了相應的方案來降低 HttpClient 叫用 REST API 的工作量。

Refit 官網

開發環境

平台:.NET6

骨架:Blazor WASM App

IDE:Visual Studio 2022

套件:Refit.HttpClientFactory

關鍵原碼紀錄

前端範例 - 應用

Client\Pages\_TodoLab.razor
@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 介面

Client\RefitClient\TodoApi.cs
using Refit;
using SmallEco.DTO;
namespace SmallEco.Client.RefitClient;

public interface ITodoApi
{
  [Post("/api/Todo/QryDataList")]
  Task<List<TodoDto>> QryDataListAsync(TodoQryAgs args);
}

後端 REST API 範例

Server\Controllers\TodoController.cs
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。

Server\Program.cs
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

Client\Program.cs
...省略...

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。

AuthHeaderHandler.cs
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