Caller 服務調用 - HttpClient

前言

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

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

目前我們有一個項目是 Dapr 的,但它所依賴的另外一個項目是基於 Http 協議的調用,目前只能使用 HttpClient 或 RestSharp 實現服務間的調用,但未來有一天它會使用 Dapr,因爲我們計劃會把所有的項目都逐步升級到 Dapr 上

什麼是 Masa.Utils.Caller?

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

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

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

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

Caller.HttpClient 入門

下面我們會寫一個簡單的 Demo,作爲入門教程,爲大家講解一下 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 >用戶id</param>
        /// <returns></returns>
        public Task<UserDto?> GetUserAsync(int id)
            => CallerProvider.GetAsync<object, UserDto>("User", new { id = id });
        
        /// <summary>
        /// 調用服務添加用戶(重點)
        /// </summary>
        /// <param ></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.V2服務,瀏覽器訪問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 >用戶id</param>
        /// <returns></returns>
        public Task<UserDto?> GetUserAsync(int id)
            => CallerProvider.GetAsync<object, UserDto>("User", new { id = id });
        
        /// <summary>
        /// 調用服務添加用戶
        /// </summary>
        /// <param ></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.V3服務,瀏覽器訪問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,歡迎聯繫我們

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/d7T4EqcV7G-6gZZIP5vW6Q