diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpClientModule.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpClientModule.cs index c4d0d0d181..c72b99c819 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpClientModule.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpClientModule.cs @@ -5,6 +5,7 @@ using Volo.Abp.MultiTenancy; using Volo.Abp.Threading; using Volo.Abp.Validation; using Volo.Abp.ExceptionHandling; +using Volo.Abp.Http.Client.DynamicProxying; namespace Volo.Abp.Http.Client { @@ -22,6 +23,8 @@ namespace Volo.Abp.Http.Client { var configuration = context.Services.GetConfiguration(); Configure(configuration); + + context.Services.AddTransient(typeof(DynamicHttpProxyInterceptorClientProxy<>)); } } } diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/ApiVersionInfo.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ApiVersionInfo.cs similarity index 92% rename from framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/ApiVersionInfo.cs rename to framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ApiVersionInfo.cs index d37de5d452..ecf2db7b9f 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/ApiVersionInfo.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ApiVersionInfo.cs @@ -1,6 +1,6 @@ using System; -namespace Volo.Abp.Http.Client.Proxying +namespace Volo.Abp.Http.Client.ClientProxying { public class ApiVersionInfo //TODO: Rename to not conflict with api versioning apis { diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs index 33a1906114..e45176393b 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs @@ -1,9 +1,23 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Globalization; using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Volo.Abp.Content; using Volo.Abp.DependencyInjection; +using Volo.Abp.Http.Client.Authentication; using Volo.Abp.Http.Client.Proxying; using Volo.Abp.Http.Modeling; +using Volo.Abp.Http.ProxyScripting.Generators; +using Volo.Abp.Json; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Threading; +using Volo.Abp.Tracing; namespace Volo.Abp.Http.Client.ClientProxying { @@ -11,42 +25,265 @@ namespace Volo.Abp.Http.Client.ClientProxying { public IAbpLazyServiceProvider LazyServiceProvider { get; set; } - protected IHttpProxyExecuter HttpProxyExecuter => LazyServiceProvider.LazyGetRequiredService(); protected IClientProxyApiDescriptionFinder ClientProxyApiDescriptionFinder => LazyServiceProvider.LazyGetRequiredService(); + protected ICancellationTokenProvider CancellationTokenProvider => LazyServiceProvider.LazyGetRequiredService(); + protected ICorrelationIdProvider CorrelationIdProvider => LazyServiceProvider.LazyGetRequiredService(); + protected ICurrentTenant CurrentTenant => LazyServiceProvider.LazyGetRequiredService(); + protected IOptions AbpCorrelationIdOptions => LazyServiceProvider.LazyGetRequiredService>(); + protected IProxyHttpClientFactory HttpClientFactory => LazyServiceProvider.LazyGetRequiredService(); + protected IRemoteServiceConfigurationProvider RemoteServiceConfigurationProvider => LazyServiceProvider.LazyGetRequiredService(); + protected IOptions ClientOptions => LazyServiceProvider.LazyGetRequiredService>(); + protected IJsonSerializer JsonSerializer => LazyServiceProvider.LazyGetRequiredService(); + protected IRemoteServiceHttpClientAuthenticator ClientAuthenticator => LazyServiceProvider.LazyGetRequiredService(); + protected ClientProxyRequestPayloadBuilder ClientProxyRequestPayloadBuilder => LazyServiceProvider.LazyGetRequiredService(); + protected ClientProxyUrlBuilder ClientProxyUrlBuilder => LazyServiceProvider.LazyGetRequiredService(); protected virtual async Task RequestAsync(string methodName, params object[] arguments) { - await HttpProxyExecuter.MakeRequestAsync(BuildHttpProxyExecuterContext(methodName, arguments)); + await RequestAsync(BuildHttpProxyClientProxyContext(methodName, arguments)); } protected virtual async Task RequestAsync(string methodName, params object[] arguments) { - return await HttpProxyExecuter.MakeRequestAndGetResultAsync(BuildHttpProxyExecuterContext(methodName, arguments)); + return await RequestAsync(BuildHttpProxyClientProxyContext(methodName, arguments)); } - protected virtual HttpProxyExecuterContext BuildHttpProxyExecuterContext(string methodName, params object[] arguments) + protected virtual ClientProxyRequestContext BuildHttpProxyClientProxyContext(string methodName, params object[] arguments) { - var actionKey = GetActionKey(methodName, arguments); - var action = ClientProxyApiDescriptionFinder.FindAction(actionKey); - return new HttpProxyExecuterContext(action, BuildArguments(action, arguments), typeof(TService)); + var methodUniqueName = $"{typeof(TService).FullName}.{methodName}.{string.Join("-", arguments.Select(x => x.GetType().FullName))}"; + var action = ClientProxyApiDescriptionFinder.FindAction(methodUniqueName); + if (action == null) + { + throw new AbpException($"The API description of the {typeof(TService).FullName}.{methodName} method was not found!"); + } + return new ClientProxyRequestContext( + action, + action.Parameters + .GroupBy(x => x.NameOnMethod) + .Select((x, i) => new KeyValuePair(x.Key, arguments[i])) + .ToDictionary(x => x.Key, x => x.Value), + typeof(TService)); } - protected virtual Dictionary BuildArguments(ActionApiDescriptionModel action, object[] arguments) + protected virtual async Task RequestAsync(ClientProxyRequestContext requestContext) { - var parameters = action.Parameters.GroupBy(x => x.NameOnMethod).Select(x => x.Key).ToList(); - var dict = new Dictionary(); + var responseContent = await RequestAsync(requestContext); + + if (typeof(T) == typeof(IRemoteStreamContent) || + typeof(T) == typeof(RemoteStreamContent)) + { + /* returning a class that holds a reference to response + * content just to be sure that GC does not dispose of + * it before we finish doing our work with the stream */ + return (T)(object)new RemoteStreamContent( + await responseContent.ReadAsStreamAsync(), + responseContent.Headers?.ContentDisposition?.FileNameStar ?? + RemoveQuotes(responseContent.Headers?.ContentDisposition?.FileName).ToString(), + responseContent.Headers?.ContentType?.ToString(), + responseContent.Headers?.ContentLength); + } + + var stringContent = await responseContent.ReadAsStringAsync(); + if (typeof(T) == typeof(string)) + { + return (T)(object)stringContent; + } + + if (stringContent.IsNullOrWhiteSpace()) + { + return default; + } + + return JsonSerializer.Deserialize(stringContent); + } + + protected virtual async Task RequestAsync(ClientProxyRequestContext requestContext) + { + var clientConfig = ClientOptions.Value.HttpClientProxies.GetOrDefault(requestContext.ServiceType) ?? throw new AbpException($"Could not get HttpClientProxyConfig for {requestContext.ServiceType.FullName}."); + var remoteServiceConfig = await RemoteServiceConfigurationProvider.GetConfigurationOrDefaultAsync(clientConfig.RemoteServiceName); + + var client = HttpClientFactory.Create(clientConfig.RemoteServiceName); + + var apiVersion = await GetApiVersionInfoAsync(requestContext); + var url = remoteServiceConfig.BaseUrl.EnsureEndsWith('/') + await GetUrlWithParametersAsync(requestContext, apiVersion); + + var requestMessage = new HttpRequestMessage(requestContext.Action.GetHttpMethod(), url) + { + Content = ClientProxyRequestPayloadBuilder.BuildContent(requestContext.Action, requestContext.Arguments, JsonSerializer, apiVersion) + }; + + AddHeaders(requestContext.Arguments, requestContext.Action, requestMessage, apiVersion); + + if (requestContext.Action.AllowAnonymous != true) + { + await ClientAuthenticator.Authenticate( + new RemoteServiceHttpClientAuthenticateContext( + client, + requestMessage, + remoteServiceConfig, + clientConfig.RemoteServiceName + ) + ); + } + + var response = await client.SendAsync( + requestMessage, + HttpCompletionOption.ResponseHeadersRead /*this will buffer only the headers, the content will be used as a stream*/, + GetCancellationToken(requestContext.Arguments) + ); - for (var i = 0; i < parameters.Count; i++) + if (!response.IsSuccessStatusCode) { - dict[parameters[i]] = arguments[i]; + await ThrowExceptionForResponseAsync(response); } - return dict; + return response.Content; + } + + protected virtual async Task GetApiVersionInfoAsync(ClientProxyRequestContext requestContext) + { + var apiVersion = await FindBestApiVersionAsync(requestContext); + + //TODO: Make names configurable? + var versionParam = requestContext.Action.Parameters.FirstOrDefault(p => p.Name == "apiVersion" && p.BindingSourceId == ParameterBindingSources.Path) ?? + requestContext.Action.Parameters.FirstOrDefault(p => p.Name == "api-version" && p.BindingSourceId == ParameterBindingSources.Query); + + return new ApiVersionInfo(versionParam?.BindingSourceId, apiVersion); + } + + protected virtual Task GetUrlWithParametersAsync(ClientProxyRequestContext requestContext, ApiVersionInfo apiVersion) + { + return Task.FromResult(ClientProxyUrlBuilder.GenerateUrlWithParameters(requestContext.Action, requestContext.Arguments, apiVersion)); } - private static string GetActionKey(string methodName, params object[] arguments) + protected virtual Task GetHttpContentAsync(ClientProxyRequestContext requestContext, ApiVersionInfo apiVersion) { - return $"{typeof(TService).FullName}.{methodName}.{string.Join("-", arguments.Select(x => x.GetType().FullName))}"; + return Task.FromResult(ClientProxyRequestPayloadBuilder.BuildContent(requestContext.Action, requestContext.Arguments, JsonSerializer, apiVersion)); + } + + protected virtual async Task FindBestApiVersionAsync(ClientProxyRequestContext requestContext) + { + var configuredVersion = await GetConfiguredApiVersionAsync(requestContext); + + if (requestContext.Action.SupportedVersions.IsNullOrEmpty()) + { + return configuredVersion ?? "1.0"; + } + + if (requestContext.Action.SupportedVersions.Contains(configuredVersion)) + { + return configuredVersion; + } + + return requestContext.Action.SupportedVersions.Last(); //TODO: Ensure to get the latest version! + } + + protected virtual async Task GetConfiguredApiVersionAsync(ClientProxyRequestContext requestContext) + { + var clientConfig = ClientOptions.Value.HttpClientProxies.GetOrDefault(requestContext.ServiceType) + ?? throw new AbpException($"Could not get DynamicHttpClientProxyConfig for {requestContext.ServiceType.FullName}."); + + return (await RemoteServiceConfigurationProvider + .GetConfigurationOrDefaultOrNullAsync(clientConfig.RemoteServiceName))?.Version; + } + + protected virtual async Task ThrowExceptionForResponseAsync(HttpResponseMessage response) + { + if (response.Headers.Contains(AbpHttpConsts.AbpErrorFormat)) + { + var errorResponse = JsonSerializer.Deserialize( + await response.Content.ReadAsStringAsync() + ); + + throw new AbpRemoteCallException(errorResponse.Error) + { + HttpStatusCode = (int) response.StatusCode + }; + } + + throw new AbpRemoteCallException( + new RemoteServiceErrorInfo + { + Message = response.ReasonPhrase, + Code = response.StatusCode.ToString() + } + ) + { + HttpStatusCode = (int) response.StatusCode + }; + } + + protected virtual void AddHeaders( + IReadOnlyDictionary argumentsDictionary, + ActionApiDescriptionModel action, + HttpRequestMessage requestMessage, + ApiVersionInfo apiVersion) + { + //API Version + if (!apiVersion.Version.IsNullOrEmpty()) + { + //TODO: What about other media types? + requestMessage.Headers.Add("accept", $"{MimeTypes.Text.Plain}; v={apiVersion.Version}"); + requestMessage.Headers.Add("accept", $"{MimeTypes.Application.Json}; v={apiVersion.Version}"); + requestMessage.Headers.Add("api-version", apiVersion.Version); + } + + //Header parameters + var headers = action.Parameters.Where(p => p.BindingSourceId == ParameterBindingSources.Header).ToArray(); + foreach (var headerParameter in headers) + { + var value = HttpActionParameterHelper.FindParameterValue(argumentsDictionary, headerParameter); + if (value != null) + { + requestMessage.Headers.Add(headerParameter.Name, value.ToString()); + } + } + + //CorrelationId + requestMessage.Headers.Add(AbpCorrelationIdOptions.Value.HttpHeaderName, CorrelationIdProvider.Get()); + + //TenantId + if (CurrentTenant.Id.HasValue) + { + //TODO: Use AbpAspNetCoreMultiTenancyOptions to get the key + requestMessage.Headers.Add(TenantResolverConsts.DefaultTenantKey, CurrentTenant.Id.Value.ToString()); + } + + //Culture + //TODO: Is that the way we want? Couldn't send the culture (not ui culture) + var currentCulture = CultureInfo.CurrentUICulture.Name ?? CultureInfo.CurrentCulture.Name; + if (!currentCulture.IsNullOrEmpty()) + { + requestMessage.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue(currentCulture)); + } + + //X-Requested-With + requestMessage.Headers.Add("X-Requested-With", "XMLHttpRequest"); + } + + protected virtual StringSegment RemoveQuotes(StringSegment input) + { + if (!StringSegment.IsNullOrEmpty(input) && input.Length >= 2 && input[0] == '"' && input[input.Length - 1] == '"') + { + input = input.Subsegment(1, input.Length - 2); + } + + return input; + } + + protected virtual CancellationToken GetCancellationToken(IReadOnlyDictionary arguments) + { + var cancellationTokenArg = arguments.LastOrDefault(); + + if (cancellationTokenArg.Value is CancellationToken cancellationToken) + { + if (cancellationToken != default) + { + return cancellationToken; + } + } + + return CancellationTokenProvider.Token; } } } diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/HttpProxyExecuterContext.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyRequestContext.cs similarity index 85% rename from framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/HttpProxyExecuterContext.cs rename to framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyRequestContext.cs index e61b2af93a..089a61c0d8 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/HttpProxyExecuterContext.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyRequestContext.cs @@ -3,9 +3,9 @@ using System.Collections.Generic; using JetBrains.Annotations; using Volo.Abp.Http.Modeling; -namespace Volo.Abp.Http.Client.Proxying +namespace Volo.Abp.Http.Client.ClientProxying { - public class HttpProxyExecuterContext + public class ClientProxyRequestContext { [NotNull] public ActionApiDescriptionModel Action { get; } @@ -16,7 +16,7 @@ namespace Volo.Abp.Http.Client.Proxying [NotNull] public Type ServiceType { get; } - public HttpProxyExecuterContext( + public ClientProxyRequestContext( [NotNull] ActionApiDescriptionModel action, [NotNull] IReadOnlyDictionary arguments, [NotNull] Type serviceType) diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/RequestPayloadBuilder.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyRequestPayloadBuilder.cs similarity index 88% rename from framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/RequestPayloadBuilder.cs rename to framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyRequestPayloadBuilder.cs index 583798f43f..3ba7026c8e 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/RequestPayloadBuilder.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyRequestPayloadBuilder.cs @@ -6,17 +6,18 @@ using System.Net.Http.Headers; using System.Text; using JetBrains.Annotations; using Volo.Abp.Content; -using Volo.Abp.Http.Client.DynamicProxying; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Http.Client.Proxying; using Volo.Abp.Http.Modeling; using Volo.Abp.Http.ProxyScripting.Generators; using Volo.Abp.Json; -namespace Volo.Abp.Http.Client.Proxying +namespace Volo.Abp.Http.Client.ClientProxying { - public static class RequestPayloadBuilder + public class ClientProxyRequestPayloadBuilder : ITransientDependency { [CanBeNull] - public static HttpContent BuildContent(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments, IJsonSerializer jsonSerializer, ApiVersionInfo apiVersion) + public virtual HttpContent BuildContent(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments, IJsonSerializer jsonSerializer, ApiVersionInfo apiVersion) { var body = GenerateBody(action, methodArguments, jsonSerializer); if (body != null) @@ -29,7 +30,7 @@ namespace Volo.Abp.Http.Client.Proxying return body; } - private static HttpContent GenerateBody(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments, IJsonSerializer jsonSerializer) + protected virtual HttpContent GenerateBody(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments, IJsonSerializer jsonSerializer) { var parameters = action .Parameters @@ -57,7 +58,7 @@ namespace Volo.Abp.Http.Client.Proxying return new StringContent(jsonSerializer.Serialize(value), Encoding.UTF8, MimeTypes.Application.Json); } - private static HttpContent GenerateFormPostData(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments) + protected virtual HttpContent GenerateFormPostData(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments) { var parameters = action .Parameters diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/UrlBuilder.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs similarity index 84% rename from framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/UrlBuilder.cs rename to framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs index 5f916fdd88..e6a9b8ff71 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/UrlBuilder.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs @@ -5,16 +5,17 @@ using System.Globalization; using System.Linq; using System.Text; using JetBrains.Annotations; -using Volo.Abp.Http.Client.DynamicProxying; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Http.Client.Proxying; using Volo.Abp.Http.Modeling; using Volo.Abp.Http.ProxyScripting.Generators; using Volo.Abp.Localization; -namespace Volo.Abp.Http.Client.Proxying +namespace Volo.Abp.Http.Client.ClientProxying { - internal static class UrlBuilder + public class ClientProxyUrlBuilder : ITransientDependency { - public static string GenerateUrlWithParameters(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments, ApiVersionInfo apiVersion) + public string GenerateUrlWithParameters(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments, ApiVersionInfo apiVersion) { // The ASP.NET Core route value provider and query string value provider: // Treat values as invariant culture. @@ -30,7 +31,7 @@ namespace Volo.Abp.Http.Client.Proxying } } - private static void ReplacePathVariables(StringBuilder urlBuilder, IList actionParameters, IReadOnlyDictionary methodArguments, ApiVersionInfo apiVersion) + protected virtual void ReplacePathVariables(StringBuilder urlBuilder, IList actionParameters, IReadOnlyDictionary methodArguments, ApiVersionInfo apiVersion) { var pathParameters = actionParameters .Where(p => p.BindingSourceId == ParameterBindingSources.Path) @@ -72,7 +73,7 @@ namespace Volo.Abp.Http.Client.Proxying } } - private static void AddQueryStringParameters(StringBuilder urlBuilder, IList actionParameters, IReadOnlyDictionary methodArguments, ApiVersionInfo apiVersion) + protected virtual void AddQueryStringParameters(StringBuilder urlBuilder, IList actionParameters, IReadOnlyDictionary methodArguments, ApiVersionInfo apiVersion) { var queryStringParameters = actionParameters .Where(p => p.BindingSourceId.IsIn(ParameterBindingSources.ModelBinding, ParameterBindingSources.Query)) @@ -100,7 +101,7 @@ namespace Volo.Abp.Http.Client.Proxying } } - private static bool AddQueryStringParameter( + protected virtual bool AddQueryStringParameter( StringBuilder urlBuilder, bool isFirstParam, string name, @@ -133,7 +134,7 @@ namespace Volo.Abp.Http.Client.Proxying return true; } - private static string ConvertValueToString([CanBeNull] object value) + protected virtual string ConvertValueToString([CanBeNull] object value) { if (value is DateTime dateTimeValue) { diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs index 68eb146201..c35995447a 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; using Volo.Abp.DynamicProxy; +using Volo.Abp.Http.Client.ClientProxying; using Volo.Abp.Http.Client.Proxying; using Volo.Abp.Http.Modeling; @@ -15,32 +16,32 @@ namespace Volo.Abp.Http.Client.DynamicProxying { public class DynamicHttpProxyInterceptor : AbpInterceptor, ITransientDependency { + // ReSharper disable once StaticMemberInGenericType - protected static MethodInfo MakeRequestAndGetResultAsyncMethod { get; } + protected static MethodInfo CallRequestAsyncMethod { get; } + + static DynamicHttpProxyInterceptor() + { + CallRequestAsyncMethod = typeof(DynamicHttpProxyInterceptor) + .GetMethods(BindingFlags.NonPublic | BindingFlags.Instance) + .First(m => m.Name == nameof(CallRequestAsync) && m.IsGenericMethodDefinition); + } + public ILogger> Logger { get; set; } + protected DynamicHttpProxyInterceptorClientProxy InterceptorClientProxy { get; } protected AbpHttpClientOptions ClientOptions { get; } - protected IHttpProxyExecuter HttpProxyExecuter { get; } protected IProxyHttpClientFactory HttpClientFactory { get; } protected IRemoteServiceConfigurationProvider RemoteServiceConfigurationProvider { get; } protected IApiDescriptionFinder ApiDescriptionFinder { get; } - public ILogger> Logger { get; set; } - - static DynamicHttpProxyInterceptor() - { - MakeRequestAndGetResultAsyncMethod = typeof(HttpProxyExecuter) - .GetMethods(BindingFlags.Public | BindingFlags.Instance) - .First(m => m.Name == nameof(IHttpProxyExecuter.MakeRequestAndGetResultAsync) && m.IsGenericMethodDefinition); - } - public DynamicHttpProxyInterceptor( - IHttpProxyExecuter httpProxyExecuter, + DynamicHttpProxyInterceptorClientProxy interceptorClientProxy, IOptions clientOptions, IProxyHttpClientFactory httpClientFactory, IRemoteServiceConfigurationProvider remoteServiceConfigurationProvider, IApiDescriptionFinder apiDescriptionFinder) { - HttpProxyExecuter = httpProxyExecuter; + InterceptorClientProxy = interceptorClientProxy; HttpClientFactory = httpClientFactory; RemoteServiceConfigurationProvider = remoteServiceConfigurationProvider; ApiDescriptionFinder = apiDescriptionFinder; @@ -49,34 +50,32 @@ namespace Volo.Abp.Http.Client.DynamicProxying Logger = NullLogger>.Instance; } - public override async Task InterceptAsync(IAbpMethodInvocation invocation) { - var context = new HttpProxyExecuterContext( + var context = new ClientProxyRequestContext( await GetActionApiDescriptionModel(invocation), invocation.ArgumentsDictionary, typeof(TService)); if (invocation.Method.ReturnType.GenericTypeArguments.IsNullOrEmpty()) { - await HttpProxyExecuter.MakeRequestAsync(context); + await InterceptorClientProxy.CallRequestAsync(context); } else { - var result = (Task)MakeRequestAndGetResultAsyncMethod - .MakeGenericMethod(invocation.Method.ReturnType.GenericTypeArguments[0]) - .Invoke(HttpProxyExecuter, new object[] { context }); + var returnType = invocation.Method.ReturnType.GenericTypeArguments[0]; + var result = (Task)CallRequestAsyncMethod + .MakeGenericMethod(returnType) + .Invoke(this, new object[] { context }); - invocation.ReturnValue = await GetResultAsync( - result, - invocation.Method.ReturnType.GetGenericArguments()[0] - ); + invocation.ReturnValue = await GetResultAsync(result, returnType); } } - private async Task GetActionApiDescriptionModel(IAbpMethodInvocation invocation) + protected virtual async Task GetActionApiDescriptionModel(IAbpMethodInvocation invocation) { - var clientConfig = ClientOptions.HttpClientProxies.GetOrDefault(typeof(TService)) ?? throw new AbpException($"Could not get DynamicHttpClientProxyConfig for {typeof(TService).FullName}."); + var clientConfig = ClientOptions.HttpClientProxies.GetOrDefault(typeof(TService)) ?? + throw new AbpException($"Could not get DynamicHttpClientProxyConfig for {typeof(TService).FullName}."); var remoteServiceConfig = await RemoteServiceConfigurationProvider.GetConfigurationOrDefaultAsync(clientConfig.RemoteServiceName); var client = HttpClientFactory.Create(clientConfig.RemoteServiceName); @@ -88,13 +87,19 @@ namespace Volo.Abp.Http.Client.DynamicProxying ); } - private async Task GetResultAsync(Task task, Type resultType) + protected virtual async Task CallRequestAsync(ClientProxyRequestContext context) + { + return await InterceptorClientProxy.CallRequestAsync(context); + } + + protected virtual async Task GetResultAsync(Task task, Type resultType) { await task; - return typeof(Task<>) + var resultProperty = typeof(Task<>) .MakeGenericType(resultType) - .GetProperty(nameof(Task.Result), BindingFlags.Instance | BindingFlags.Public) - .GetValue(task); + .GetProperty(nameof(Task.Result), BindingFlags.Instance | BindingFlags.Public); + Check.NotNull(resultProperty, nameof(resultProperty)); + return resultProperty.GetValue(task); } } } diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptorClientProxy.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptorClientProxy.cs new file mode 100644 index 0000000000..0d8cd3f9df --- /dev/null +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptorClientProxy.cs @@ -0,0 +1,19 @@ +using System.Net.Http; +using System.Threading.Tasks; +using Volo.Abp.Http.Client.ClientProxying; + +namespace Volo.Abp.Http.Client.DynamicProxying +{ + public class DynamicHttpProxyInterceptorClientProxy : ClientProxyBase + { + public virtual async Task CallRequestAsync(ClientProxyRequestContext requestContext) + { + return await base.RequestAsync(requestContext); + } + + public virtual async Task CallRequestAsync(ClientProxyRequestContext requestContext) + { + return await base.RequestAsync(requestContext); + } + } +} diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/HttpProxyExecuter.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/HttpProxyExecuter.cs deleted file mode 100644 index 6fe5376f2d..0000000000 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/HttpProxyExecuter.cs +++ /dev/null @@ -1,267 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; -using Volo.Abp.Content; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Http.Client.Authentication; -using Volo.Abp.Http.Modeling; -using Volo.Abp.Http.ProxyScripting.Generators; -using Volo.Abp.Json; -using Volo.Abp.MultiTenancy; -using Volo.Abp.Threading; -using Volo.Abp.Tracing; - -namespace Volo.Abp.Http.Client.Proxying -{ - public class HttpProxyExecuter : IHttpProxyExecuter, ITransientDependency - { - protected ICancellationTokenProvider CancellationTokenProvider { get; } - protected ICorrelationIdProvider CorrelationIdProvider { get; } - protected ICurrentTenant CurrentTenant { get; } - protected AbpCorrelationIdOptions AbpCorrelationIdOptions { get; } - protected IProxyHttpClientFactory HttpClientFactory { get; } - protected IRemoteServiceConfigurationProvider RemoteServiceConfigurationProvider { get; } - protected AbpHttpClientOptions ClientOptions { get; } - protected IJsonSerializer JsonSerializer { get; } - protected IRemoteServiceHttpClientAuthenticator ClientAuthenticator { get; } - - public HttpProxyExecuter( - ICancellationTokenProvider cancellationTokenProvider, - ICorrelationIdProvider correlationIdProvider, - ICurrentTenant currentTenant, - IOptions abpCorrelationIdOptions, - IProxyHttpClientFactory httpClientFactory, - IRemoteServiceConfigurationProvider remoteServiceConfigurationProvider, - IOptions clientOptions, - IRemoteServiceHttpClientAuthenticator clientAuthenticator, - IJsonSerializer jsonSerializer) - { - CancellationTokenProvider = cancellationTokenProvider; - CorrelationIdProvider = correlationIdProvider; - CurrentTenant = currentTenant; - AbpCorrelationIdOptions = abpCorrelationIdOptions.Value; - HttpClientFactory = httpClientFactory; - RemoteServiceConfigurationProvider = remoteServiceConfigurationProvider; - ClientOptions = clientOptions.Value; - ClientAuthenticator = clientAuthenticator; - JsonSerializer = jsonSerializer; - } - - public virtual async Task MakeRequestAndGetResultAsync(HttpProxyExecuterContext context) - { - var responseContent = await MakeRequestAsync(context); - - if (typeof(T) == typeof(IRemoteStreamContent) || - typeof(T) == typeof(RemoteStreamContent)) - { - /* returning a class that holds a reference to response - * content just to be sure that GC does not dispose of - * it before we finish doing our work with the stream */ - return (T) (object) new RemoteStreamContent( - await responseContent.ReadAsStreamAsync(), - responseContent.Headers?.ContentDisposition?.FileNameStar ?? RemoveQuotes(responseContent.Headers?.ContentDisposition?.FileName).ToString(), - responseContent.Headers?.ContentType?.ToString(), - responseContent.Headers?.ContentLength); - } - - var stringContent = await responseContent.ReadAsStringAsync(); - if (typeof(T) == typeof(string)) - { - return (T)(object)stringContent; - } - - if (stringContent.IsNullOrWhiteSpace()) - { - return default; - } - - return JsonSerializer.Deserialize(stringContent); - } - - public virtual async Task MakeRequestAsync(HttpProxyExecuterContext context) - { - var clientConfig = ClientOptions.HttpClientProxies.GetOrDefault(context.ServiceType) ?? throw new AbpException($"Could not get HttpClientProxyConfig for {context.ServiceType.FullName}."); - var remoteServiceConfig = await RemoteServiceConfigurationProvider.GetConfigurationOrDefaultAsync(clientConfig.RemoteServiceName); - - var client = HttpClientFactory.Create(clientConfig.RemoteServiceName); - - var apiVersion = await GetApiVersionInfoAsync(context); - var url = remoteServiceConfig.BaseUrl.EnsureEndsWith('/') + UrlBuilder.GenerateUrlWithParameters(context.Action, context.Arguments, apiVersion); - - var requestMessage = new HttpRequestMessage(context.Action.GetHttpMethod(), url) - { - Content = RequestPayloadBuilder.BuildContent(context.Action, context.Arguments, JsonSerializer, apiVersion) - }; - - AddHeaders(context.Arguments, context.Action, requestMessage, apiVersion); - - if (context.Action.AllowAnonymous != true) - { - await ClientAuthenticator.Authenticate( - new RemoteServiceHttpClientAuthenticateContext( - client, - requestMessage, - remoteServiceConfig, - clientConfig.RemoteServiceName - ) - ); - } - - var response = await client.SendAsync( - requestMessage, - HttpCompletionOption.ResponseHeadersRead /*this will buffer only the headers, the content will be used as a stream*/, - GetCancellationToken(context.Arguments) - ); - - if (!response.IsSuccessStatusCode) - { - await ThrowExceptionForResponseAsync(response); - } - - return response.Content; - } - - private async Task GetApiVersionInfoAsync(HttpProxyExecuterContext context) - { - var apiVersion = await FindBestApiVersionAsync(context); - - //TODO: Make names configurable? - var versionParam = context.Action.Parameters.FirstOrDefault(p => p.Name == "apiVersion" && p.BindingSourceId == ParameterBindingSources.Path) ?? - context.Action.Parameters.FirstOrDefault(p => p.Name == "api-version" && p.BindingSourceId == ParameterBindingSources.Query); - - return new ApiVersionInfo(versionParam?.BindingSourceId, apiVersion); - } - - private async Task FindBestApiVersionAsync(HttpProxyExecuterContext context) - { - var configuredVersion = await GetConfiguredApiVersionAsync(context); - - if (context.Action.SupportedVersions.IsNullOrEmpty()) - { - return configuredVersion ?? "1.0"; - } - - if (context.Action.SupportedVersions.Contains(configuredVersion)) - { - return configuredVersion; - } - - return context.Action.SupportedVersions.Last(); //TODO: Ensure to get the latest version! - } - - private async Task GetConfiguredApiVersionAsync(HttpProxyExecuterContext context) - { - var clientConfig = ClientOptions.HttpClientProxies.GetOrDefault(context.ServiceType) - ?? throw new AbpException($"Could not get DynamicHttpClientProxyConfig for {context.ServiceType.FullName}."); - - return (await RemoteServiceConfigurationProvider - .GetConfigurationOrDefaultOrNullAsync(clientConfig.RemoteServiceName))?.Version; - } - - private async Task ThrowExceptionForResponseAsync(HttpResponseMessage response) - { - if (response.Headers.Contains(AbpHttpConsts.AbpErrorFormat)) - { - var errorResponse = JsonSerializer.Deserialize( - await response.Content.ReadAsStringAsync() - ); - - throw new AbpRemoteCallException(errorResponse.Error) - { - HttpStatusCode = (int) response.StatusCode - }; - } - - throw new AbpRemoteCallException( - new RemoteServiceErrorInfo - { - Message = response.ReasonPhrase, - Code = response.StatusCode.ToString() - } - ) - { - HttpStatusCode = (int) response.StatusCode - }; - } - - protected virtual void AddHeaders( - IReadOnlyDictionary argumentsDictionary, - ActionApiDescriptionModel action, - HttpRequestMessage requestMessage, - ApiVersionInfo apiVersion) - { - //API Version - if (!apiVersion.Version.IsNullOrEmpty()) - { - //TODO: What about other media types? - requestMessage.Headers.Add("accept", $"{MimeTypes.Text.Plain}; v={apiVersion.Version}"); - requestMessage.Headers.Add("accept", $"{MimeTypes.Application.Json}; v={apiVersion.Version}"); - requestMessage.Headers.Add("api-version", apiVersion.Version); - } - - //Header parameters - var headers = action.Parameters.Where(p => p.BindingSourceId == ParameterBindingSources.Header).ToArray(); - foreach (var headerParameter in headers) - { - var value = HttpActionParameterHelper.FindParameterValue(argumentsDictionary, headerParameter); - if (value != null) - { - requestMessage.Headers.Add(headerParameter.Name, value.ToString()); - } - } - - //CorrelationId - requestMessage.Headers.Add(AbpCorrelationIdOptions.HttpHeaderName, CorrelationIdProvider.Get()); - - //TenantId - if (CurrentTenant.Id.HasValue) - { - //TODO: Use AbpAspNetCoreMultiTenancyOptions to get the key - requestMessage.Headers.Add(TenantResolverConsts.DefaultTenantKey, CurrentTenant.Id.Value.ToString()); - } - - //Culture - //TODO: Is that the way we want? Couldn't send the culture (not ui culture) - var currentCulture = CultureInfo.CurrentUICulture.Name ?? CultureInfo.CurrentCulture.Name; - if (!currentCulture.IsNullOrEmpty()) - { - requestMessage.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue(currentCulture)); - } - - //X-Requested-With - requestMessage.Headers.Add("X-Requested-With", "XMLHttpRequest"); - } - - protected virtual StringSegment RemoveQuotes(StringSegment input) - { - if (!StringSegment.IsNullOrEmpty(input) && input.Length >= 2 && input[0] == '"' && input[input.Length - 1] == '"') - { - input = input.Subsegment(1, input.Length - 2); - } - - return input; - } - - protected virtual CancellationToken GetCancellationToken(IReadOnlyDictionary arguments) - { - var cancellationTokenArg = arguments.LastOrDefault(); - - if (cancellationTokenArg.Value is CancellationToken cancellationToken) - { - if (cancellationToken != default) - { - return cancellationToken; - } - } - - return CancellationTokenProvider.Token; - } - } -} diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/IHttpProxyExecuter.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/IHttpProxyExecuter.cs deleted file mode 100644 index a7f48f14fe..0000000000 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/IHttpProxyExecuter.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Net.Http; -using System.Threading.Tasks; - -namespace Volo.Abp.Http.Client.Proxying -{ - public interface IHttpProxyExecuter - { - Task MakeRequestAsync(HttpProxyExecuterContext context); - - Task MakeRequestAndGetResultAsync(HttpProxyExecuterContext context); - } -}