Browse Source

Merge pull request #10040 from abpframework/maliming/client-proxy

Enhance ClientProxyBase.
pull/10128/head
liangshiwei 5 years ago
committed by GitHub
parent
commit
7f05c4924d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpClientModule.cs
  2. 2
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ApiVersionInfo.cs
  3. 269
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs
  4. 6
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyRequestContext.cs
  5. 13
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyRequestPayloadBuilder.cs
  6. 17
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs
  7. 63
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs
  8. 19
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptorClientProxy.cs
  9. 267
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/HttpProxyExecuter.cs
  10. 12
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/IHttpProxyExecuter.cs

3
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<AbpRemoteServiceOptions>(configuration);
context.Services.AddTransient(typeof(DynamicHttpProxyInterceptorClientProxy<>));
}
}
}

2
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/ApiVersionInfo.cs → 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
{

269
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<IHttpProxyExecuter>();
protected IClientProxyApiDescriptionFinder ClientProxyApiDescriptionFinder => LazyServiceProvider.LazyGetRequiredService<IClientProxyApiDescriptionFinder>();
protected ICancellationTokenProvider CancellationTokenProvider => LazyServiceProvider.LazyGetRequiredService<ICancellationTokenProvider>();
protected ICorrelationIdProvider CorrelationIdProvider => LazyServiceProvider.LazyGetRequiredService<ICorrelationIdProvider>();
protected ICurrentTenant CurrentTenant => LazyServiceProvider.LazyGetRequiredService<ICurrentTenant>();
protected IOptions<AbpCorrelationIdOptions> AbpCorrelationIdOptions => LazyServiceProvider.LazyGetRequiredService<IOptions<AbpCorrelationIdOptions>>();
protected IProxyHttpClientFactory HttpClientFactory => LazyServiceProvider.LazyGetRequiredService<IProxyHttpClientFactory>();
protected IRemoteServiceConfigurationProvider RemoteServiceConfigurationProvider => LazyServiceProvider.LazyGetRequiredService<IRemoteServiceConfigurationProvider>();
protected IOptions<AbpHttpClientOptions> ClientOptions => LazyServiceProvider.LazyGetRequiredService<IOptions<AbpHttpClientOptions>>();
protected IJsonSerializer JsonSerializer => LazyServiceProvider.LazyGetRequiredService<IJsonSerializer>();
protected IRemoteServiceHttpClientAuthenticator ClientAuthenticator => LazyServiceProvider.LazyGetRequiredService<IRemoteServiceHttpClientAuthenticator>();
protected ClientProxyRequestPayloadBuilder ClientProxyRequestPayloadBuilder => LazyServiceProvider.LazyGetRequiredService<ClientProxyRequestPayloadBuilder>();
protected ClientProxyUrlBuilder ClientProxyUrlBuilder => LazyServiceProvider.LazyGetRequiredService<ClientProxyUrlBuilder>();
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<T> RequestAsync<T>(string methodName, params object[] arguments)
{
return await HttpProxyExecuter.MakeRequestAndGetResultAsync<T>(BuildHttpProxyExecuterContext(methodName, arguments));
return await RequestAsync<T>(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<string, object>(x.Key, arguments[i]))
.ToDictionary(x => x.Key, x => x.Value),
typeof(TService));
}
protected virtual Dictionary<string, object> BuildArguments(ActionApiDescriptionModel action, object[] arguments)
protected virtual async Task<T> RequestAsync<T>(ClientProxyRequestContext requestContext)
{
var parameters = action.Parameters.GroupBy(x => x.NameOnMethod).Select(x => x.Key).ToList();
var dict = new Dictionary<string, object>();
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<T>(stringContent);
}
protected virtual async Task<HttpContent> 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<ApiVersionInfo> 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<string> 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<HttpContent> 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<string> 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<string> 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<RemoteServiceErrorResponse>(
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<string, object> 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<string, object> arguments)
{
var cancellationTokenArg = arguments.LastOrDefault();
if (cancellationTokenArg.Value is CancellationToken cancellationToken)
{
if (cancellationToken != default)
{
return cancellationToken;
}
}
return CancellationTokenProvider.Token;
}
}
}

6
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/HttpProxyExecuterContext.cs → 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<string, object> arguments,
[NotNull] Type serviceType)

13
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/RequestPayloadBuilder.cs → 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<string, object> methodArguments, IJsonSerializer jsonSerializer, ApiVersionInfo apiVersion)
public virtual HttpContent BuildContent(ActionApiDescriptionModel action, IReadOnlyDictionary<string, object> 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<string, object> methodArguments, IJsonSerializer jsonSerializer)
protected virtual HttpContent GenerateBody(ActionApiDescriptionModel action, IReadOnlyDictionary<string, object> 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<string, object> methodArguments)
protected virtual HttpContent GenerateFormPostData(ActionApiDescriptionModel action, IReadOnlyDictionary<string, object> methodArguments)
{
var parameters = action
.Parameters

17
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/UrlBuilder.cs → 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<string, object> methodArguments, ApiVersionInfo apiVersion)
public string GenerateUrlWithParameters(ActionApiDescriptionModel action, IReadOnlyDictionary<string, object> 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<ParameterApiDescriptionModel> actionParameters, IReadOnlyDictionary<string, object> methodArguments, ApiVersionInfo apiVersion)
protected virtual void ReplacePathVariables(StringBuilder urlBuilder, IList<ParameterApiDescriptionModel> actionParameters, IReadOnlyDictionary<string, object> 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<ParameterApiDescriptionModel> actionParameters, IReadOnlyDictionary<string, object> methodArguments, ApiVersionInfo apiVersion)
protected virtual void AddQueryStringParameters(StringBuilder urlBuilder, IList<ParameterApiDescriptionModel> actionParameters, IReadOnlyDictionary<string, object> 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)
{

63
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<TService> : AbpInterceptor, ITransientDependency
{
// ReSharper disable once StaticMemberInGenericType
protected static MethodInfo MakeRequestAndGetResultAsyncMethod { get; }
protected static MethodInfo CallRequestAsyncMethod { get; }
static DynamicHttpProxyInterceptor()
{
CallRequestAsyncMethod = typeof(DynamicHttpProxyInterceptor<TService>)
.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
.First(m => m.Name == nameof(CallRequestAsync) && m.IsGenericMethodDefinition);
}
public ILogger<DynamicHttpProxyInterceptor<TService>> Logger { get; set; }
protected DynamicHttpProxyInterceptorClientProxy<TService> InterceptorClientProxy { get; }
protected AbpHttpClientOptions ClientOptions { get; }
protected IHttpProxyExecuter HttpProxyExecuter { get; }
protected IProxyHttpClientFactory HttpClientFactory { get; }
protected IRemoteServiceConfigurationProvider RemoteServiceConfigurationProvider { get; }
protected IApiDescriptionFinder ApiDescriptionFinder { get; }
public ILogger<DynamicHttpProxyInterceptor<TService>> 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<TService> interceptorClientProxy,
IOptions<AbpHttpClientOptions> 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<DynamicHttpProxyInterceptor<TService>>.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<ActionApiDescriptionModel> GetActionApiDescriptionModel(IAbpMethodInvocation invocation)
protected virtual async Task<ActionApiDescriptionModel> 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<object> GetResultAsync(Task task, Type resultType)
protected virtual async Task<T> CallRequestAsync<T>(ClientProxyRequestContext context)
{
return await InterceptorClientProxy.CallRequestAsync<T>(context);
}
protected virtual async Task<object> GetResultAsync(Task task, Type resultType)
{
await task;
return typeof(Task<>)
var resultProperty = typeof(Task<>)
.MakeGenericType(resultType)
.GetProperty(nameof(Task<object>.Result), BindingFlags.Instance | BindingFlags.Public)
.GetValue(task);
.GetProperty(nameof(Task<object>.Result), BindingFlags.Instance | BindingFlags.Public);
Check.NotNull(resultProperty, nameof(resultProperty));
return resultProperty.GetValue(task);
}
}
}

19
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<TService> : ClientProxyBase<TService>
{
public virtual async Task<T> CallRequestAsync<T>(ClientProxyRequestContext requestContext)
{
return await base.RequestAsync<T>(requestContext);
}
public virtual async Task<HttpContent> CallRequestAsync(ClientProxyRequestContext requestContext)
{
return await base.RequestAsync(requestContext);
}
}
}

267
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/HttpProxyExecuter.cs

@ -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> abpCorrelationIdOptions,
IProxyHttpClientFactory httpClientFactory,
IRemoteServiceConfigurationProvider remoteServiceConfigurationProvider,
IOptions<AbpHttpClientOptions> 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<T> MakeRequestAndGetResultAsync<T>(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<T>(stringContent);
}
public virtual async Task<HttpContent> 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<ApiVersionInfo> 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<string> 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<string> 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<RemoteServiceErrorResponse>(
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<string, object> 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<string, object> arguments)
{
var cancellationTokenArg = arguments.LastOrDefault();
if (cancellationTokenArg.Value is CancellationToken cancellationToken)
{
if (cancellationToken != default)
{
return cancellationToken;
}
}
return CancellationTokenProvider.Token;
}
}
}

12
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/IHttpProxyExecuter.cs

@ -1,12 +0,0 @@
using System.Net.Http;
using System.Threading.Tasks;
namespace Volo.Abp.Http.Client.Proxying
{
public interface IHttpProxyExecuter
{
Task<HttpContent> MakeRequestAsync(HttpProxyExecuterContext context);
Task<T> MakeRequestAndGetResultAsync<T>(HttpProxyExecuterContext context);
}
}
Loading…
Cancel
Save