讓服務呼叫更簡單 - Caller.HttpClient

磊_磊發表於2022-05-09

前言

絕大多數專案都離不開服務呼叫,服務的呼叫方式通常是基於Http、RPC協議的呼叫,需要獲取到對應服務的域名或者ip地址以及詳細的控制器方法後才能進行呼叫,如果專案需要支援分散式部署,則需要藉助服務發現或者Nginx才能實現。

但隨著Dapr的崛起,服務的呼叫方式也發生了變化,它不僅僅提供了處理重試和瞬態錯誤等功能,還內建服務發現,啟用dapr的服務僅需知道任意一個啟用dapr服務的HttpPort埠、gRpc埠、以及對應服務的appid以及對應的方法名稱就可以完成呼叫,dapr的出現使得服務間呼叫變得更為的簡單、方便

目前我們有一個專案是Dapr的,但它所依賴的另外一個專案是基於Http協議的呼叫,目前只能使用HttpClientRestSharp實現服務間的呼叫,但未來有一天它會使用Dapr,因為我們計劃會把所有的專案都逐步升級到Dapr上

什麼是Masa.Utils.Caller?

Masa的Caller是一個用於服務呼叫的類庫,它提供了以下能力:

  • 基礎Http請求的能力,包括Get、Post、Put、Delete等
    • GetAsync
      • GetStringAsync: 得到響應資訊為字串型別的Get請求
      • GetByteArrayAsync: 得到響應資訊為位元組陣列的Get請求
      • GetStreamAsync: 得到響應資訊為流的Get請求
      • GetAsync: 得到響應資訊支援泛型型別的Get請求
    • PostAsync
    • PatchAsync
    • PutAsync
    • DeleteAsync
    • SendGrpcAsync (Caller.HttpClient暫不支援)
    • SendAsync
  • 降低了不同的部署方式對業務程式碼的影響,對於前期不使用Dapr,後期更改為通過Dapr呼叫,只需修改少量程式碼即可
  • 當請求方法發生異常後,會繼續丟擲異常,服務的呼叫變得像方法呼叫一樣簡單,對開發者友好,當然你也可以選擇返回HttpResponseMessage自行解析

檢查請求響應的StatusCode的值來判斷當前請求是否成功,具體程式碼可檢視

目前Caller支援了兩種實現方式:

下面就讓我們先看一下HttpClient版的Caller

Caller.HttpClient 入門

下面我們會寫一個簡單的Demo,作為入門教程,為大家講解一下Get請求與Post請求的使用辦法

在開始之前,我們先明確我們的目的以及打算如何做?

  • 目標:通過Caller.HttpClient讓大家理解Masa提供的Caller如何實現服務呼叫(請求)

  • 如何做:分別建立兩個Asp.Net Core空的Web服務,一個作為服務端(被呼叫方),一個作為客戶端(呼叫方),在服務端寫兩個方法,分別是Get請求(獲取使用者資訊)、Post請求(建立使用者)的方法,在客戶端同樣建立兩個對應的方法用來測試獲取使用者請求、建立使用者請求能否正常執行

準備工作

  1. 建立ASP.NET Core 空專案Assignment.Server作為服務端,並修改Program.cs

    using Microsoft.AspNetCore.Mvc;
    
    var builder = WebApplication.CreateBuilder(args);
    var app = builder.Build();
    
    app.MapGet("/", () => "Hello Assignment.Server!");
    
    app.MapGet("/User", ([FromQuery] int id) =>
    {
        //todo: 模擬根據id查詢使用者資訊
        return new
        {
            Id = id,
            Name = "John Doe"
        };
    });
    
    app.MapPost("/User", ([FromBody] AddUserRequest request) =>
    {
        //todo: 模擬新增使用者,並返回使用者名稱稱
        return request.Name;
    });
    
    app.Run();
    
    public class AddUserRequest
    {
        public string Name { get; set; }
    }
    
  2. 建立ASP.NET Core 空專案Assignment.Client.HttpClientWeb作為客戶端

  3. 選中Assignment.Client.HttpClientWeb並安裝Masa.Utils.Caller.HttpClient

    dotnet add package Masa.Utils.Caller.HttpClient --version 0.4.0-preview.4
    
  4. 修改Program.cs

    using System.Globalization;
    using Masa.Utils.Caller.Core;
    using Masa.Utils.Caller.HttpClient;
    using Microsoft.AspNetCore.Mvc;
    
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddCaller(option =>
    {
        option.UseHttpClient(opt =>
        {
            opt.BaseApi = "http://localhost:5000";
            opt.Name = "userCaller"; // 當前Caller的別名(僅有一個Caller時可以不填),Name不能重複
            opt.IsDefault = true; // 預設的Caller支援注入ICallerProvider獲取
        });
    });
    var app = builder.Build();
    
    app.MapGet("/", () => "Hello HttpClientWeb.V1!");
    
    app.MapGet("/Test/User/Get", async ([FromServices] ICallerProvider callerProvider) =>
    {
        var user = await callerProvider.GetAsync<object, UserDto>("User", new { id = new Random().Next(1, 10) });
        return $"獲取使用者資訊成功:使用者名稱稱為:{user!.Name}";
    });
    
    app.MapGet("/Test/User/Add", async ([FromServices] ICallerProvider callerProvider) =>
    {
        var dateTimeOffset = new DateTimeOffset(DateTime.UtcNow);
        string timeSpan = dateTimeOffset.ToUnixTimeSeconds().ToString();
        var userName = "ss_" + timeSpan; //模擬一個使用者名稱
        string? response = await callerProvider.PostAsync<object, string>("User", new { Name = userName });
        return $"建立使用者成功了,使用者名稱稱為:{response}";
    });
    
    app.Run();
    
    public class UserDto
    {
        public int Id { get; set; }
    
        public string Name { get; set; } = default!;
    }
    
    

-> > Q:為什麼BaseApi的地址是http://localhost:5000

A:因為服務端專案的地址是http://localhost:5000,根據實際情況替換成你自己的服務端專案地址

現在Caller的HttpClient版本就可以使用了,分別啟動Assignment.ServerAssignment.Client.HttpClientWeb服務,瀏覽器訪問http://localhost:5107/Test/User/Gethttp://localhost:5107/Test/User/Add,分別輸出對應的獲取使用者資訊成功以及建立使用者成功的提示,則證明呼叫成功了。

搞笑對話:

搞笑對話

HttpClient 進階版

隨著與工程師經理的一番切磋後發現了上述程式碼僅是基礎版的,更貼合傳統的HttpClient的寫法,與預設的HttpClient有異曲同工之妙,但並不是只能這樣寫,下面就來看看新的寫法:

  1. 建立ASP.NET Core 空專案Assignment.Client.HttpClientWeb.V2作為呼叫方V2版本

  2. 選中Assignment.Client.HttpClientWeb.V2並安裝Masa.Utils.Caller.HttpClient

    dotnet add package Masa.Utils.Caller.HttpClient --version 0.4.0-preview.4
    
  3. 新增類ServerCaller (對應服務端服務)

    using Masa.Utils.Caller.HttpClient;
    namespace Assignment.Client.HttpClientWeb.V2;
    
    public class ServerCaller : HttpClientCallerBase
    {
        protected override string BaseAddress { get; set; } = "http://localhost:5000";
    
        public ServerCaller(IServiceProvider serviceProvider) : base(serviceProvider)
        {
        }
    
        /// <summary>
        /// 呼叫服務獲取使用者資訊 (重點)
        /// </summary>
        /// <param name="id">使用者id</param>
        /// <returns></returns>
        public Task<UserDto?> GetUserAsync(int id)
            => CallerProvider.GetAsync<object, UserDto>("User", new { id = id });
    
        /// <summary>
        /// 呼叫服務新增使用者(重點)
        /// </summary>
        /// <param name="userName"></param>
        /// <returns></returns>
        public Task<string?> AddUserAsync(string userName)
            => CallerProvider.PostAsync<object, string>("User", new { Name = userName });
    }
    
    
    public class UserDto
    {
        public int Id { get; set; }
    
        public string Name { get; set; } = default!;
    }
    
  4. 修改Program.cs

    using System.Globalization;
    using Assignment.Client.HttpClientWeb.V2;
    using Masa.Utils.Caller.Core;
    using Microsoft.AspNetCore.Mvc;
    
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddCaller();
    
    var app = builder.Build();
    
    app.MapGet("/", () => "Hello HttpClientWeb.V2!");
    
    // 重點:直接注入對應的ServiceCaller,呼叫對應的方法即可
    app.MapGet("/Test/User/Get", async ([FromServices] ServerCaller serverCaller) =>
    {
        var id = new Random().Next(1, 10);//預設使用者id
        var user = await serverCaller.GetUserAsync(id);
        return $"獲取使用者資訊成功:使用者名稱稱為:{user!.Name}";
    });
    
    app.MapGet("/Test/User/Add", async ([FromServices] ServerCaller serverCaller) =>
    {
        var dateTimeOffset = new DateTimeOffset(DateTime.UtcNow);
        string timeSpan = dateTimeOffset.ToUnixTimeSeconds().ToString();
        var userName = "ss_" + timeSpan; //模擬一個使用者名稱
        string? response= await serverCaller.AddUserAsync(userName);
        return $"建立使用者成功了,使用者名稱稱為:{response}";
    });
    
    app.Run();
    

最後,分別啟動Assignment.ServerAssignment.Client.HttpClientWeb服務,瀏覽器訪問http://localhost:5188/Test/User/Gethttp://localhost:5188/Test/User/Add,分別輸出對應的獲取使用者資訊成功以及建立使用者成功的提示,則證明呼叫成功了。

這個版本的Caller很不錯,呼叫請求變成了跟呼叫方法一樣,簡單明瞭,很不錯

不過ServerCaller下面好像是同一個服務的方法,如果我這個服務方法特別多的話,那這個類豈不是特別龐大,但如果我要拆分成好幾個的話,那BaseAddress我豈不是需要複製很多份 ʅ( T﹏T )ʃ

HttpClient 推薦

讓請求呼叫更簡單,讓你的程式碼更簡潔

  1. 在V2版本的基礎上新增類ServerCallerBase

    using Masa.Utils.Caller.HttpClient;
    namespace Assignment.Client.HttpClientWeb.V3;
    
    /// <summary>
    /// 注意:ServerCallerBase是抽象類喲
    /// </summary>
    public abstract class ServerCallerBase: HttpClientCallerBase
    {
        protected override string BaseAddress { get; set; } = "http://localhost:5000";
    
        protected ServerCallerBase(IServiceProvider serviceProvider) : base(serviceProvider)
        {
        }
    }
    

ServerCallerBase可以以服務拆分,每個服務建一個'ServerCallerBase'

  1. 調整ServerCaller.cs重新命名為UserCaller.cs,然後刪除重寫BaseAddress屬性:

    namespace Assignment.Client.HttpClientWeb.V3;
    public class UserCaller : ServerCallerBase
    {
        // protected override string BaseAddress { get; set; } = "http://localhost:5000"; //注意:父類已經實現,無需重寫,所以被刪除了
        
        public UserCaller(IServiceProvider serviceProvider) : base(serviceProvider)
        {
        }
    
        /// <summary>
        /// 呼叫服務獲取使用者資訊
        /// </summary>
        /// <param name="id">使用者id</param>
        /// <returns></returns>
        public Task<UserDto?> GetUserAsync(int id)
            => CallerProvider.GetAsync<object, UserDto>("User", new { id = id });
    
        /// <summary>
        /// 呼叫服務新增使用者
        /// </summary>
        /// <param name="userName"></param>
        /// <returns></returns>
        public Task<string?> AddUserAsync(string userName)
            => CallerProvider.PostAsync<object, string>("User", new { Name = userName });
    }
    
    public class UserDto
    {
        public int Id { get; set; }
    
        public string Name { get; set; } = default!;
    }
    
  2. 修改Program.cs,將ServerCaller修改為UserCaller即可

    app.MapGet("/Test/User/Get", async ([FromServices] UserCaller caller) =>
    {
        var id = new Random().Next(1, 10);//預設使用者id
        var user = await caller.GetUserAsync(id);
        return $"獲取使用者資訊成功:使用者名稱稱為:{user!.Name}";
    });
    
    app.MapGet("/Test/User/Add", async ([FromServices] UserCaller caller) =>
    {
        var dateTimeOffset = new DateTimeOffset(DateTime.UtcNow);
        string timeSpan = dateTimeOffset.ToUnixTimeSeconds().ToString();
        var userName = "ss_" + timeSpan; //模擬一個使用者名稱
        string? response= await caller.AddUserAsync(userName);
        return $"建立使用者成功了,使用者名稱稱為:{response}";
    });
    

其餘程式碼不變,就形成了V3版本,V3版本與V2版本相比,減少了多次對BaseAddress的賦值、使得程式碼更加簡潔,按照控制器結構建立對應的Caller,讓服務呼叫像方法呼叫一樣簡單明瞭

最後,分別啟動Assignment.ServerAssignment.Client.HttpClientWeb服務,瀏覽器訪問http://localhost:5201/Test/User/Gethttp://localhost:5201/Test/User/Add,分別輸出對應的獲取使用者資訊成功以及建立使用者成功的提示,則證明呼叫成功了。

本章原始碼

Assignment02

https://github.com/zhenlei520/MasaFramework.Practice

開源地址

MASA.BuildingBlocks:https://github.com/masastack/MASA.BuildingBlocks

MASA.Contrib:https://github.com/masastack/MASA.Contrib

MASA.Utils:https://github.com/masastack/MASA.Utils

MASA.EShop:https://github.com/masalabs/MASA.EShop

MASA.Blazor:https://github.com/BlazorComponent/MASA.Blazor

如果你對我們的 MASA Framework 感興趣,無論是程式碼貢獻、使用、提 Issue,歡迎聯絡我們

16373211753064.png

相關文章