1. 程式人生 > >ABP開發框架前後端開發系列---(4)Web API呼叫類的封裝和使用

ABP開發框架前後端開發系列---(4)Web API呼叫類的封裝和使用

在前面隨筆介紹ABP應用框架的專案組織情況,以及專案中領域層各個類程式碼組織,以及簡化了ABP框架的各個層的內容,使得我們專案結構更加清晰。上篇隨筆已經介紹了字典模組中應用服務層介面的實現情況,並且通過執行Web API的宿主程式,可以在介面上進行介面測試了,本篇隨筆基於前面介紹的基礎上,介紹Web API呼叫類的封裝和使用,使用包括控制檯和Winform中對呼叫封裝類的使用。

在上篇隨筆《ABP開發框架前後端開發系列---(3)框架的分層和檔案組織》中我繪製了改進後的ABP框架的架構圖示,如下圖所示。

這個專案分層裡面的 03-Application.Common 應用服務通用層,我們主要放置在各個模組裡面公用的DTO和應用服務介面類。有了這些DTO檔案和介面類,我們就不用在客戶端(如Winform客戶、控制檯、WPF/UWP等)重複編寫這部分的內容,直接使用即可。

這些DTO檔案和介面類檔案,我們的主要用途是用來封裝客戶端呼叫Web API的呼叫類,使得我們在介面使用的時候,呼叫更加方便。

1)Web API呼叫類封裝

為了更方便在控制檯客戶端、Winform客戶端等場景下呼叫Web API的功能,我們需要對應用服務層丟擲的Web API介面進行封裝,然後結合DTO類實現一個標準的介面實現。

由於這些呼叫類可能在多個客戶端中進行共享,因此根據我們在混合框架中積累的經驗,我們把它們獨立為一個專案進行管理,如下專案檢視所示。

其中DictDataApiCaller 就是對應領域物件 <領域物件>ApiCaller的命名規則。

如對於字典模組的API封裝類,它們繼承一個相同的基類,然後實現特殊的自定義介面即可,這樣可以減少常規的Create、Get、GetAll、Update、Delete等操作的程式碼,這些全部由呼叫基類進行處理,而只需要實現自定義的介面呼叫即可。如下是字典模組DictType和DictData兩個業務物件的API封裝關係。

如對於字典型別的API封裝類定義程式碼如下所示。

    /// <summary>
    /// 字典型別物件的Web API呼叫處理
    /// </summary>
    public class DictTypeApiCaller : AsyncCrudApiCaller<DictTypeDto, string, DictTypePagedDto, CreateDictTypeDto, DictTypeDto>, IDictTypeAppService
    {
        /// <summary>
        /// 提供單件物件使用
        /// </summary>
        public static DictTypeApiCaller Instance
        {
            get
            {
                return Singleton<DictTypeApiCaller>.Instance;
            }
        }

......

這裡我們可以通過單件的方式來使用字典型別API的封裝類例項 DictTypeApiCaller.Instance

對於Web API的呼叫,我們知道,一般需要使用WebClient或者HttpRequest的底層類進行Url的訪問處理,通過提供相應的資料,獲取對應的返回結果。

而對於操作方法的型別,是使用POST、GET、INPUT、DELETE的不同,需要看具體的介面,我們可以通過Swagger UI 呈現出來的進行處理即可,如下所示的動作型別。

如果處理動作不匹配,如本來是Post的用Get方法,或者是Delete的用Post方法,都會出錯。

在Abp.Web.Api專案裡面有一個AbpWebApiClient的封裝方法,裡面實現了POST方法,可以參考來做對應的WebClient的封裝呼叫。

我在它的基礎上擴充套件了實現方法,包括了Get、Put、Delete方法的呼叫。

我們使用的時候,初始化它就可以了。

apiClient = new AbpWebApiClient();

例如,我們對於常規的使用者登入處理,它的API呼叫封裝的操作程式碼如下所示,這個是一個POST方法。

        /// <summary>
        /// 對使用者身份進行認證
        /// </summary>
        /// <param name="username">使用者名稱</param>
        /// <param name="password">使用者密碼</param>
        /// <returns></returns>
        public async virtual Task<AuthenticateResult> Authenticate(string username, string password)
        {
            var url = string.Format("{0}/api/TokenAuth/Authenticate", ServerRootAddress);
            var input = new
            {
                UsernameOrEmailAddress = username,
                Password = password
            };

            var result = await apiClient.PostAsync<AuthenticateResult>(url, input);
            return result;
        }

對於業務介面來說,我們都是基於約定的規則來命名介面名稱和地址的,如對於GetAll這個方法來說,字典型別的地址如下所示。

/api/services/app/DictData/GetAll

另外還包括伺服器的基礎地址,從而構建一個完整的呼叫地址如下所示。

http://localhost:21021/api/services/app/DictData/GetAll

由於這些規則確定,因此我們可以通過動態構建這個API地址即可。

            string url = GetActionUrl(MethodBase.GetCurrentMethod());//獲取訪問API的地址(未包含引數)
            url += string.Format("?SkipCount={0}&MaxResultCount={1}", dto.SkipCount, dto.MaxResultCount);

而對於GetAll函式來說,這個定義如下所示。

Task<PagedResultDto<TEntityDto>> GetAll(TGetAllInput input)

它是需要根據一定的條件進行查詢的,不僅僅是 SkipCount 和 MaxResultCount兩個屬性,因此我們需要動態組合它的url引數,因此建立一個輔助類來動態構建這些輸入引數地址。

        /// <summary>
        /// 獲取所有物件列表
        /// </summary>
        /// <param name="input">獲取所有條件</param>
        /// <returns></returns>
        public async virtual Task<PagedResultDto<TEntityDto>> GetAll(TGetAllInput input)
        {
            AddRequestHeaders();//加入認證的token頭資訊
            string url = GetActionUrl(MethodBase.GetCurrentMethod());//獲取訪問API的地址(未包含引數)
            url = GetUrlParam(input, url);

            var result = await apiClient.GetAsync<PagedResultDto<TEntityDto>>(url);
            return result;
        }

這樣我們這個API的呼叫封裝類的基類就實現了常規的功能了。效果如下所示。

而字典型別的API封裝類,我們只需要實現特定的自定義介面即可,省卻我們很多的工作量。

namespace MyProject.Caller
{
    /// <summary>
    /// 字典型別物件的Web API呼叫處理
    /// </summary>
    public class DictTypeApiCaller : AsyncCrudApiCaller<DictTypeDto, string, DictTypePagedDto, CreateDictTypeDto, DictTypeDto>, IDictTypeAppService
    {
        /// <summary>
        /// 提供單件物件使用
        /// </summary>
        public static DictTypeApiCaller Instance
        {
            get
            {
                return Singleton<DictTypeApiCaller>.Instance;
            }
        }

        /// <summary>
        /// 預設建構函式
        /// </summary>
        public DictTypeApiCaller()
        {
            this.DomainName = "DictType";//指定域物件名稱,用於組裝介面地址
        }

        public async Task<Dictionary<string, string>> GetAllType(string dictTypeId)
        {
            AddRequestHeaders();//加入認證的token頭資訊
            string url = GetActionUrl(MethodBase.GetCurrentMethod());//獲取訪問API的地址(未包含引數)
            url += string.Format("?dictTypeId={0}", dictTypeId);

            var result = await apiClient.GetAsync<Dictionary<string, string>>(url);
            return result; 
        }

        public async Task<IList<DictTypeNodeDto>> GetTree(string pid)
        {
            AddRequestHeaders();//加入認證的token頭資訊
            string url = GetActionUrl(MethodBase.GetCurrentMethod());//獲取訪問API的地址(未包含引數)
            url += string.Format("?pid={0}", pid);

            var result = await apiClient.GetAsync<IList<DictTypeNodeDto>>(url);
            return result;
        }
    }
}

 

2)API封裝類的呼叫

前面小節介紹了針對Web API介面的封裝,以適應客戶端快速呼叫的目的,這個封裝作為一個獨立的封裝層,以方便各個模組之間進行共同呼叫。

到這裡為止,我們還沒有測試過具體的呼叫,還沒有了解實際呼叫過程中是否有問題,當然我們在開發的時候,一般都是一步步來的,但也是確保整個路線沒有問題的。

實際情況如何,是騾是馬拉出來溜溜就知道了。

首先我們建立一個基於.net Core的控制檯程式,專案情況如下所示。

在其中我們定義這個專案的模組資訊,它是依賴於APICaller層的模組。

namespace RemoteApiConsoleApp
{
    [DependsOn(typeof(CallerModule))]
    public class MyModule : AbpModule
    {
        public override void Initialize()
        {
            IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());
        }
    }
}

在ABP裡面,模組是通過一定順序啟動的,如果我們通過AbpBootstrapper類來啟動相關的模組,啟動模組的程式碼如下所示。

//使用AbpBootstrapper建立類來處理
using (var bootstrapper = AbpBootstrapper.Create<MyModule>())
{
    bootstrapper.Initialize();

        ..........

模組啟動後,系統的IOC容器會為我們註冊好相關的介面物件,那麼呼叫API封裝類的程式碼如下所示。

                //使用AbpBootstrapper建立類來處理
                using (var bootstrapper = AbpBootstrapper.Create<MyModule>())
                {
                    bootstrapper.Initialize();

                    #region Role
                    using (var client = bootstrapper.IocManager.ResolveAsDisposable<RoleApiCaller>())
                    {
                        var caller = client.Object;

                        Console.WriteLine("Logging in with TOKEN based auth...");
                        var token = caller.Authenticate("admin", "123qwe").Result;
                        Console.WriteLine(token.ToJson());

                        caller.RequestHeaders.Add(new NameValue("Authorization", "Bearer " + token.AccessToken));

                        Console.WriteLine("Getting roles...");
                        var pagerDto = new PagedResultRequestDto() { SkipCount = 0, MaxResultCount = 10 };
                        var result = caller.GetAll(pagerDto);
                        Console.WriteLine(result.ToJson());

                        Console.WriteLine("Create role...");
                        List<string> permission = new List<string>() { "Pages.Roles" };
                        var createRoleDto = new CreateRoleDto { DisplayName = "test", Name = "Test", Description = "test", Permissions = permission };
                        var roleDto = caller.Create(createRoleDto).Result;
                        Console.WriteLine(roleDto.ToJson());

                        var singleDto = new EntityDto<int>() { Id = roleDto.Id };
                        Console.WriteLine("Getting role by id...");
                        roleDto = caller.Get(singleDto).Result;
                        Console.WriteLine(roleDto);

                        Console.WriteLine("Delete role...");
                        var delResult = caller.Delete(singleDto);
                        Console.WriteLine(delResult.ToJson());

                        Console.ReadLine();
                    }
                    #endregion

上面是對角色的相關介面操作,如果對於我們之前建立的字典模組,那麼它的操作程式碼類似,如下所示。

    #region DictType

    using (var client = bootstrapper.IocManager.ResolveAsDisposable<DictTypeApiCaller>())
    {
        var caller = client.Object;

        Console.WriteLine("Logging in with TOKEN based auth...");
        var token = caller.Authenticate("admin", "123qwe").Result;
        Console.WriteLine(token.ToJson());

        caller.RequestHeaders.Add(new NameValue("Authorization", "Bearer " + token.AccessToken));

        Console.WriteLine("Get All ...");
        var pagerDto = new DictTypePagedDto() { SkipCount = 0, MaxResultCount = 10 };
        var result = caller.GetAll(pagerDto).Result;
        Console.WriteLine(result.ToJson());

        Console.WriteLine("Get All by condition ...");
        var pagerdictDto = new DictTypePagedDto() { Name = "民族" };
        result = caller.GetAll(pagerdictDto).Result;
        Console.WriteLine(result.ToJson());
        
        Console.WriteLine("Get count by condition ...");
        pagerdictDto = new DictTypePagedDto() {};
        var count = caller.Count(pagerdictDto).Result;
        Console.WriteLine(count);
        Console.WriteLine();

        Console.WriteLine("Create DictType...");
        var createDto = new CreateDictTypeDto { Id = Guid.NewGuid().ToString(), Name = "Test", Code = "Test" };
        var dictDto = caller.Create(createDto).Result;
        Console.WriteLine(dictDto.ToJson());

        Console.WriteLine("Update DictType...");
        dictDto.Code = "testcode";
        var updateDto = caller.Update(dictDto).Result;
        Console.WriteLine(updateDto.ToJson());

        if (updateDto != null)
        {
            Console.WriteLine("Delete DictType...");
            caller.Delete(new EntityDto<string>() { Id = dictDto.Id });
        }

    }
    #endregion

測試字典模組的處理,執行效果如下所示。

刪除內容,我們是配置為軟刪除的,因此可以通過資料庫記錄檢視是否標記為刪除了。

同時,我們可以看到審計日誌裡面,有對相關應用層介面的呼叫記錄。

以上就是.net core控制檯程式中對於API封裝介面的呼叫,上面程式碼如果需要在.net framework裡面跑,也是一樣的,我同樣也做了一個基於.net framework控制檯程式,程式碼呼叫都差不多的,它的ApiCaller我們做成了 .net standard程式類庫的,因此都是通用的。

前面我們提到,我們的APICaller的類,設計了單件的例項呼叫,因此我們呼叫起來更加方便,除了上面使用ABP的啟動模組的方式呼叫外,我們可以用傳統的方式進行呼叫,也就是建立一個ApiCaller的例項物件的方式進行呼叫,如下程式碼所示。

    string loginName = this.txtUserName.Text.Trim();
    string password = this.txtPassword.Text;
    AuthenticateResult result = null;
    try
    {
        result = await DictTypeApiCaller.Instance.Authenticate(loginName, password);
    }
    catch(AbpException ex)
    {
        MessageDxUtil.ShowTips("使用者帳號密碼不正確。\r\n錯誤資訊:" + ex.Message);
        return;
    }

由於篇幅的原因,基於winform介面模組的呼叫,我在後面隨筆在另起一篇隨筆進行介紹吧,畢竟那是畢竟漂亮的字典模組呈現了。

&n