NetCore 之 DispatchProxy

如何使用Dispatchproxy封装REST API,让API调用更简单。

1、创建HttpClientDispathProxy类继承自DispatchProxy

    public class HttpClientDispathProxy<TInterface> : DispatchProxy
    {
        public Func<string> token;
        public ApiClient client;
        protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
        {
            if (targetMethod?.ReturnType == typeof(Task))
                return this.InvokeAsync(targetMethod, args);
            if (IsGenericTask(targetMethod?.ReturnType))
            {
                var method = this.GetType().GetMethod("InvokeAsyncT", BindingFlags.NonPublic | BindingFlags.Instance);
                var methodInfo = method!.MakeGenericMethod(targetMethod?.ReturnType?.GenericTypeArguments ?? new Type[] { });
                return methodInfo.Invoke(this, new object[] { targetMethod, args }); ;
            }
            if (targetMethod?.ReturnType != typeof(void))
            {
                var response = this.SendAsync(targetMethod, args);
            
                var result  = this.client.ReadResponse(targetMethod.ReturnType, response.Result);
                return result;
            }
            return default;
        }

        protected async Task InvokeAsync(MethodInfo? method, object?[]? args)
        {
            await this.SendAsync(method, args);
        }

        protected async Task<T> InvokeAsyncT<T>(MethodInfo? method, object?[]? args)
        {
            var response = await this.SendAsync(method, args);
            var result = this.client.ReadJson<T>(response);
            return result;
        }

        protected virtual async Task<string> SendAsync(MethodInfo methodInfo, Object[] args)
        {
            var attr = methodInfo.GetCustomAttribute<ApiAttribute>();
            if (attr == null)
                throw new ArgumentNullException(nameof(attr));
            using (HttpRequestMessage request = new())
            {
                request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token());
                BuildHttpRequestMessage(methodInfo, args, attr, request);
                return await client.SendAsync(request);
            }
        }

        protected virtual void BuildHttpRequestMessage(MethodInfo method, object[] args, ApiAttribute attr, HttpRequestMessage request)
        {
            request.Method = _ConvertHttpMethod(attr.HttpMethod);
            var parameters = this.GetParameters(method, args);

            if (attr.HttpMethod == "POST" || attr.HttpMethod == "PUT" || attr.HttpMethod == "PTCH")
            {
                request.RequestUri = GetRequestUrl(true, attr.Url, parameters.uri);
                if (parameters.forms != null)
                {
                    JsonSerializerOptions op = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull };
                    var contentString = "";
                    if (parameters.forms.Count() == 1)
                    {
                        contentString = JsonSerializer.Serialize(parameters.forms.First().Value, op);
                    }
                    else if (parameters.forms.Count() > 1)
                    {
                        contentString = JsonSerializer.Serialize(parameters.forms, op);
                    }

                    if (!String.IsNullOrEmpty(contentString))
                    {
                        request.Content = new StringContent(contentString, Encoding.UTF8, "application/json");
                    }
                }
            }
            else
            {
                request.RequestUri = GetRequestUrl(false, attr.Url, parameters.uri);
            }

        }

        protected virtual Uri GetRequestUrl(Boolean isPost, String path, Dictionary<String, Object> parameters)
        {
            var url = $"{path.TrimEnd('/')}".ToLower();
            var qMark = url.IndexOf("?") == -1 ? "?" : String.Empty;
            var and = String.IsNullOrEmpty(qMark) ? "&" : String.Empty;

            if (parameters != null)
            {
                List<String> parsedParam = new();

                foreach (var kv in parameters)
                {
                    var holder = "{" + kv.Key.ToLower() + "}";
                    if (url.Contains(holder))
                    {
                        if (kv.Value == null)
                        {
                            throw new ArgumentNullException(kv.Key, "Parameter in uri can not be null.");
                        }
                        String tempValue;
                        if (kv.Value is Enum enumValue)
                        {
                            tempValue = enumValue.ToString();//enumValue.GetEnumDescription();
                        }
                        else
                            tempValue = kv.Value.ToString();
                        url = url.Replace(holder, tempValue);

                        parsedParam.Add(kv.Key);
                    }
                }

                var leftParams = parameters.Where(i => !parsedParam.Contains(i.Key));
                if (leftParams.Any())
                {
                    List<String> tempParamsList = new();
                    foreach (var item in leftParams)
                    {
                        var value = item.Value?.ToString();
                        if (string.IsNullOrEmpty(value))
                        {
                            if (item.Value is GraphQuery query)
                            {
                                if (query.Top != 0)
                                {
                                    tempParamsList.Add("$top=" + query.Top);
                                }
                                if (string.IsNullOrEmpty(query.Expand))
                                {
                                    tempParamsList.Add("$expand=" + query.Expand);
                                }
                                if (string.IsNullOrEmpty(query.Select))
                                {
                                    tempParamsList.Add("$select=" + query.Select);
                                }
                                if (string.IsNullOrEmpty(query.Orderby))
                                {
                                    tempParamsList.Add("$orderby=" + query.Orderby);
                                }
                                if (string.IsNullOrEmpty(query.Filter))
                                {
                                    tempParamsList.Add("$filter=" + query.Filter);
                                }
                                if (query.Skip != 0)
                                {
                                    tempParamsList.Add("$skip=" + query.Skip);
                                }
                                if (query.Count)
                                {
                                    tempParamsList.Add("$count=true");
                                }
                            }
                            else
                            {
                                tempParamsList.Add($"{item.Key}={HttpUtility.UrlEncode(item.Value.ToString()).Replace("+", "%20")}");
                            }
                        }
                    }
                    url = $"{url}{qMark}{and}{String.Join("&", tempParamsList.ToArray())}";
                }
            }

            return new Uri(url);
        }

        protected virtual (Dictionary<string, object> uri, Dictionary<string, object> forms) GetParameters(MethodInfo method, object[] args)
        {
            var uri = new Dictionary<string, object>();
            var forms = new Dictionary<string, object>();
            if (args != null && args.Any())
            {
                var parameters = method.GetParameters();
                foreach (var param in parameters)
                {
                    var customAttributes = param.GetCustomAttributes();
                    foreach (var customAttribute in customAttributes)
                    {
                        if (customAttribute is ApiParameterAttribute attribute)
                        {
                            if (attribute.IsRequestUri)
                                if (!string.IsNullOrEmpty(attribute.Endpoint))
                                    uri.Add(attribute.Name ?? "endpoint", new Uri(attribute.Endpoint));
                                else
                                    uri.Add(attribute.Name, args[param.Position]);
                            else
                                forms.Add(attribute.Name, args[param.Position]);
                        }
                        else
                            forms.Add(param.Name, args[param.Position]);
                    }
                }
            }
            return (uri, forms);
        }

        private static HttpMethod _ConvertHttpMethod(string method)
            => method.ToLowerInvariant() switch
            {
                "get" => HttpMethod.Get,
                "post" => HttpMethod.Post,
                "put" => HttpMethod.Put,
                "delete" => HttpMethod.Delete,
                "patch" => HttpMethod.Patch,
                _ => throw new ArgumentOutOfRangeException($"The method type is not supported. method: {method}.")
            };

        private static Boolean IsGenericTask(Type? type)
        {
            if ((type?.IsGenericType ?? false) && type?.GetGenericTypeDefinition() == typeof(Task<>))
                return true;
            return false;
        }
    }
dispachproxy

2、创建HttpClientProxy类继承自HttpClientDispathProxy,用于创建泛型Interface实例

  public class HttpClientProxy<TInterface>: HttpClientDispathProxy<TInterface>
    {
        public static TInterface Create(ApiClient client, Func<string> func)
        {
            Object proxy = Create<TInterface, HttpClientDispathProxy<TInterface>>()!;
            ((HttpClientDispathProxy<TInterface>)proxy).token = func;
            ((HttpClientDispathProxy<TInterface>)proxy).client = client;
            return (TInterface)proxy;
        }
    }
proxy

3、创建ApiClient 类调用interface实例方法

 public partial class ApiClient
    {
        public T GetApiService<T>(Func<string> token) where T : IApiService
        {
            return HttpClientProxy<T>.Create(this, token);
        }
    }
APIClient

这里指定partial class,原因是方便扩展

    public partial class ApiClient
    {
        private readonly IHttpClientFactory _httpClient;
        public ApiClient(IHttpClientFactory httpClient)
        {
            this._httpClient = httpClient;
        }

        internal async Task<string> SendAsync(HttpRequestMessage request)
        {
            using var response = await this._httpClient.CreateClient().SendAsync(request);
            var content = await response.Content.ReadAsStringAsync();
            if (response.IsSuccessStatusCode)
                return content;
            throw new ApiClientException(response.StatusCode, content);
        }

        internal Object ReadResponse(Type type, String content)
        {
            if (!string.IsNullOrEmpty(content) && type != typeof(void))
            {
                try
                {
                    return System.Text.Json.JsonSerializer.Deserialize(content, type);
                }
                catch (Exception ex)
                {
                    throw new ApiClientException(ex.Message);
                }
            }
            return default;
        }

        internal T ReadJson<T>(String content)
        {
            if(!string.IsNullOrEmpty(content))
                try
                {
                    return (T)this.ReadResponse(typeof(T), content);
                }
                catch (Exception ex)
                {
                    throw new ApiClientException(ex.Message);
                }
            return default;
        }
    }
APIClient

4、创建接口,添加对应attribute

    public interface IApiService
    {
        [Api(HttpMethod = "GET", Url = "{endpoint}WeatherForecast")]
        Task<IEnumerable<WeatherForecast>> GetAsync(
            [ApiParameter(Endpoint = "https://localhost:7101", IsRequestUri = true)] string endpoint = default
            );
    }
interface

 ApiAttribute

   [AttributeUsage(AttributeTargets.Method)]
    public class ApiAttribute : Attribute
    {
        public string HttpMethod { get; set; }

        public string Url { get; set; }
    }
api attribute

ApiParameterAttribute

    public class ApiParameterAttribute: Attribute
    {
        public string Name { get; set; }
        public bool IsRequestUri { get; set; }
        public string Endpoint { get; set; }
        public ApiParameterAttribute()
        {

        }
        public ApiParameterAttribute(string name)
        {
            Name = name;
        }
    }
api parameter attribute

5、exception

    public class ApiClientException : Exception
    {
        public HttpStatusCode StatusCode { get; set; }
        public ApiClientException(HttpStatusCode httpStatusCode, string message)
            : base(message)
        {
            StatusCode = httpStatusCode;
        }

        public ApiClientException(string message) 
            : base(message)
        { 
        }
    }
exception

6、demo controller

    [ApiExplorerSettings(GroupName = "demo1")]
    [ApiController]
    [Route("[controller]")]
    public class SwagggerDemoController : ControllerBase
    {
        private readonly ApiClient _apiClient;
        public SwagggerDemoController(ApiClient apiClient)
        {
            this._apiClient = apiClient;
        }

        [HttpGet("getdispatchproxydemo"), Authorize]
        public IEnumerable<WeatherForecast> DispatchProxyDemo(string token)
        {
            var client = _apiClient.GetApiService<IApiService>(() => token);
            var response = client.GetAsync().GetAwaiter().GetResult();
            return response;
        }
    }
controller

 7、验证,正确取到数据

 

 

Notes:这里加了authentication验证,之前blog中有提过,可以参考。

OK 搞定!

posted @ 2023-03-04 09:35  云霄宇霁  阅读(54)  评论(0编辑  收藏  举报