走進WebApiClientCore的設計

jiulang發表於2020-05-20

WebApiClient

WebApiClientNCC開源社群的一個專案,是目前微服務裡http介面呼叫的一把鋒利尖刀,專案早期設計與開發的時候,是基於.netframework的,然後慢慢加入netstandard和netcoreapp多個框架的支援,設計能力出眾,AOP能力唾手可得易如反掌。

WebApiClientCore

WebApiClient很優秀,它將不同框架不同平臺都實現了統一的api;WebApiClient不夠優秀,它在.netcore下完全可以更好,但它不得不相容.net45開始所有框架而有所犧牲。所以WebApiClientCore橫空出世,它是WebApiClient.JIT的.netcore替代版本,目前尚屬於alpha階段,計劃只支援.netcore平臺,並緊密與.netcore新特性緊密結合。

WebApiClientCore的變化

  • 使用System.Text.Json替換Json.net,提升序列化效能
  • 移除HttpApiFactory和HttApiConfig功能,使用Microsoft.Extensions.Http的HttpClientFactory
  • 移除AOT功能,僅保留依賴於Emit的執行時代理
  • 高效的ActionInvoker,對返回Task<>和ITask<>作不同處理
  • 所有特性都都變成中介軟體,基於管道編排各個特性並生成Action執行委託
  • 良好設計的HttpContext、ApiRequestContext、ApiParameterContext和ApiResponseContext

WebApiClientCore執行流程設計

1 介面代理類生成設計

Cretate<THttpApi>() -> BuildProxyType() -> CreateProxyInstance(ActionInterceptor)

1.1 HttpApiProxyTypeBuilder

在HttpApi.Create()時,先呼叫HttpApiProxyTypeBuilder來生成THttpApi介面的代理類,HttpApiProxyTypeBuilder是基於Emit方案,Build出來的代理類在每個方法呼叫時觸發一次攔截器ActionInterceptor的Intercept()方法,將呼叫引數傳給攔截器。

1.2 HttpApiProxyBuilder

給定一個代理類的型別(Type),快速生成代理類的例項,這個Builder實際是生成並儲存了代理類構造器的高效呼叫委託,屬於反射優化。

2 ActionInterceptor的設計

ActionInterceptor.Intercept(MethodInfo) -> CreateActionInvoker() -> ActionInvoker.Invoke()

ActionInterceptor在攔截到方法呼叫時,根據方法的MethodInfo資訊,建立ActionInvoker,然後呼叫ActionInvoker.Invoke()執行。當然,ActionInvoker並不是總是建立的,因為它的建立是有成本的,ActionInterceptor使用了快取ActionInvoker的方案。

2.1 MultiplexedActionInvoker

WebApiClientCore支援加Task<>和ITask<>兩種非同步宣告,MultiplexedActionInvoker實際上包裝了ActionInvoker和ActionTask兩個欄位,當宣告為Task<>時,呼叫ActionInvoker執行,當宣告為ITask<>是,返回建立實現了ITask<>介面的ActionTask例項。

2.2 ActionInvoker

ActionInvoker是一個ApiActionDescriptor的執行器,其實現了IActionInvoker.Invoke(ServiceContext context, object[] arguments)介面。關於Descriptor的設計模式,我們在asp.netcore的各種AtionContext裡可以發現,有了ApiActionDescriptor,再給它各個引數值,Action就很容易執行起來了。

3 RequestDelegate生成設計

ActionInvoker在拿到各個引數值之後,並不是直接從ApiActionDescriptor查詢各個特性來執行,而是在執行前就把執行流程編譯好,得到一個執行委託,這個委託叫RequestDelegate,其原型為Func<ApiRequestContext, Task<ApiResponseContext>> Build(ApiActionDescriptor apiAction)。抽象成傳入請求上下檔案,返回響應上下文,當真正執行時,呼叫這個委託即可。如果你熟悉asp.netcore,那麼應該很容易理解下面程式碼的思路:

/// <summary>
/// 提供Action的呼叫鏈委託建立
/// </summary>
static class RequestDelegateBuilder
{
    /// <summary>
    /// 建立執行委託
    /// </summary>
    /// <returns></returns>
    public static Func<ApiRequestContext, Task<ApiResponseContext>> Build(ApiActionDescriptor apiAction)
    {
        var requestHandler = BuildRequestHandler(apiAction);
        var responseHandler = BuildResponseHandler(apiAction);

        return async request =>
        {
            await requestHandler(request).ConfigureAwait(false);
            var response = await SendRequestAsync(request).ConfigureAwait(false);
            await responseHandler(response).ConfigureAwait(false);
            return response;
        };
    }


    /// <summary>
    /// 建立請求委託
    /// </summary>
    /// <param name="apiAction"></param>
    /// <returns></returns>
    private static InvokeDelegate<ApiRequestContext> BuildRequestHandler(ApiActionDescriptor apiAction)
    {
        var builder = new PipelineBuilder<ApiRequestContext>();

        // 引數驗證特性驗證和引數模型屬性特性驗證
        builder.Use(next => context =>
        {
            var validateProperty = context.HttpContext.Options.UseParameterPropertyValidate;
            foreach (var parameter in context.ApiAction.Parameters)
            {
                var parameterValue = context.Arguments[parameter.Index];
                ApiValidator.ValidateParameter(parameter, parameterValue, validateProperty);
            }
            return next(context);
        });

        // action特性請求前執行
        foreach (var attr in apiAction.Attributes)
        {
            builder.Use(attr.OnRequestAsync);
        }

        // 引數特性請求前執行
        foreach (var parameter in apiAction.Parameters)
        {
            var index = parameter.Index;
            foreach (var attr in parameter.Attributes)
            {
                builder.Use(async (context, next) =>
                {
                    var ctx = new ApiParameterContext(context, index);
                    await attr.OnRequestAsync(ctx, next).ConfigureAwait(false);
                });
            }
        }

        // Return特性請求前執行
        foreach (var @return in apiAction.Return.Attributes)
        {
            if (@return.Enable == true)
            {
                builder.Use(@return.OnRequestAsync);
            }
        }

        // Filter請求前執行            
        foreach (var filter in apiAction.FilterAttributes)
        {
            if (filter.Enable == true)
            {
                builder.Use(filter.OnRequestAsync);
            }
        }

        return builder.Build();
    }

    /// <summary>
    /// 建立響應委託
    /// </summary>
    /// <param name="apiAction"></param>
    /// <returns></returns>
    private static InvokeDelegate<ApiResponseContext> BuildResponseHandler(ApiActionDescriptor apiAction)
    {
        var builder = new PipelineBuilder<ApiResponseContext>();

        // Return特性請求後執行
        foreach (var @return in apiAction.Return.Attributes)
        {
            if (@return.Enable == false)
            {
                continue;
            }

            builder.Use(async (context, next) =>
            {
                if (context.ResultStatus == ResultStatus.None)
                {
                    await @return.OnResponseAsync(context, next).ConfigureAwait(false);
                }
                else
                {
                    await next().ConfigureAwait(false);
                }
            });
        }

        // 驗證Result是否ok
        builder.Use(next => context =>
        {
            try
            {
                ApiValidator.ValidateReturnValue(context.Result, context.HttpContext.Options.UseReturnValuePropertyValidate);
            }
            catch (Exception ex)
            {
                context.Exception = ex;
            }
            return next(context);
        });

        // Filter請求後執行
        foreach (var filter in apiAction.FilterAttributes)
        {
            if (filter.Enable == true)
            {
                builder.Use(filter.OnResponseAsync);
            }
        }

        return builder.Build();
    }


    /// <summary>
    /// 執行http請求
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    private static async Task<ApiResponseContext> SendRequestAsync(ApiRequestContext context)
    {
        try
        {
            var apiCache = new ApiCache(context);
            var cacheValue = await apiCache.GetAsync().ConfigureAwait(false);

            if (cacheValue != null && cacheValue.Value != null)
            {
                context.HttpContext.ResponseMessage = cacheValue.Value;
            }
            else
            {
                using var cancellation = CreateLinkedTokenSource(context);
                var response = await context.HttpContext.Client.SendAsync(context.HttpContext.RequestMessage, cancellation.Token).ConfigureAwait(false);

                context.HttpContext.ResponseMessage = response;
                await apiCache.SetAsync(cacheValue?.Key, response).ConfigureAwait(false);
            }
            return new ApiResponseContext(context);
        }
        catch (Exception ex)
        {
            return new ApiResponseContext(context) { Exception = ex };
        }
    }

    /// <summary>
    /// 建立取消令牌源
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    private static CancellationTokenSource CreateLinkedTokenSource(ApiRequestContext context)
    {
        if (context.CancellationTokens.Count == 0)
        {
            return CancellationTokenSource.CreateLinkedTokenSource(CancellationToken.None);
        }
        else
        {
            var tokens = context.CancellationTokens.ToArray();
            return CancellationTokenSource.CreateLinkedTokenSource(tokens);
        }
    }
}

WebApiClientCore的特性設計

WebApiClientCore的核心特性為以下4種,每種功能各不一樣,在設計上使用了中介軟體的思想,每一步執行都可以獲取到context物件和下一個中介軟體next物件,開發者在實現自定義Attribute時,可以選擇性的進行短路設計。

1 IApiActionAttribute

表示Action執行前會呼叫,呼叫時接收到ApiRequestContext

/// <summary>
/// 定義ApiAction修飾特性的行為
/// </summary>
public interface IApiActionAttribute : IAttributeMultiplable
{
    /// <summary>
    /// 請求前
    /// </summary>
    /// <param name="context">上下文</param>
    /// <param name="next">下一個執行委託</param>
    /// <returns></returns>
    Task OnRequestAsync(ApiRequestContext context, Func<Task> next);
}

2 IApiParameterAttribute

表示引數執行前會呼叫,呼叫時接收到ApiParameterContext

/// <summary>
/// 定義Api引數修飾特性的行為
/// </summary>
public interface IApiParameterAttribute
{
    /// <summary>
    /// 請求前
    /// </summary>
    /// <param name="context">上下文</param>
    /// <param name="next">下一個執行委託</param>
    /// <returns></returns>
    Task OnRequestAsync(ApiParameterContext context, Func<Task> next); 
}

3 IApiReturnAttribute

執行前和執行後都會收到,設定為上下文的Result或Exception,會短路執行

/// <summary>
/// 定義回覆內容處理特性的行為
/// </summary>
public interface IApiReturnAttribute : IAttributeMultiplable, IAttributeEnable
{
    /// <summary>
    /// 請求前
    /// </summary>
    /// <param name="context">上下文</param>
    /// <param name="next">下一個執行委託</param>
    /// <returns></returns>
    Task OnRequestAsync(ApiRequestContext context, Func<Task> next);

    /// <summary>
    /// 響應後
    /// </summary>
    /// <param name="context">上下文</param>
    /// <param name="next">下一個執行委託</param>
    /// <returns></returns>
    Task OnResponseAsync(ApiResponseContext context, Func<Task> next);
}

5 IApiFilterAttribute

執行前和執行後都會收到,在IApiReturnAttribute之後執行

/// <summary>
/// 定義ApiAction過濾器修飾特性的行為
/// </summary>
public interface IApiFilterAttribute : IAttributeMultiplable, IAttributeEnable
{
    /// <summary>
    /// 請求前
    /// </summary>
    /// <param name="context">上下文</param>
    /// <param name="next">下一個執行委託</param>
    /// <returns></returns>
    Task OnRequestAsync(ApiRequestContext context, Func<Task> next);

    /// <summary>
    /// 響應後
    /// </summary>
    /// <param name="context">上下文</param>
    /// <param name="next">下一個執行委託</param>
    /// <returns></returns>
    Task OnResponseAsync(ApiResponseContext context, Func<Task> next);
}

結束語

程式碼可以寫得很爛,但設計必須高大上,希望WebApiClientCore可以在宣告式客戶端領域繼續引領其它開源庫,同時讓使用它的開發者為之讚歎。

如果你希望為望WebApiClientCore出力,可以Fork它然後pull request,和我一起完善單元測試,或編寫多語言資原始檔,或者加入一些更好的程式碼設計。

相關文章