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; } }
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; } }
3、创建ApiClient 类调用interface实例方法
public partial class ApiClient { public T GetApiService<T>(Func<string> token) where T : IApiService { return HttpClientProxy<T>.Create(this, token); } }
这里指定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; } }
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 ); }
ApiAttribute
[AttributeUsage(AttributeTargets.Method)] public class ApiAttribute : Attribute { public string HttpMethod { get; set; } public string Url { get; set; } }
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; } }
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) { } }
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; } }
7、验证,正确取到数据
Notes:这里加了authentication验证,之前blog中有提过,可以参考。
OK 搞定!