Browse Source

Add CSharpServiceProxyGenerator

pull/9905/head
liangshiwei 5 years ago
parent
commit
45e845a771
  1. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs
  2. 325
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxy/CSharp/CSharpServiceProxyGenerator.cs
  3. 37
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxy/JavaScript/JavaScriptServiceProxyGenerator.cs
  4. 42
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxy/ServiceProxyGeneratorBase.cs
  5. 40
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxyBase.cs
  6. 281
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs
  7. 269
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/HttpProxyExecuter.cs
  8. 30
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/HttpProxyExecuterContext.cs
  9. 12
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/IHttpProxyExecuter.cs

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs

@ -5,6 +5,7 @@ using Volo.Abp.Cli.Http;
using Volo.Abp.Cli.LIbs; using Volo.Abp.Cli.LIbs;
using Volo.Abp.Cli.ServiceProxy; using Volo.Abp.Cli.ServiceProxy;
using Volo.Abp.Cli.ServiceProxy.Angular; using Volo.Abp.Cli.ServiceProxy.Angular;
using Volo.Abp.Cli.ServiceProxy.CSharp;
using Volo.Abp.Cli.ServiceProxy.JavaScript; using Volo.Abp.Cli.ServiceProxy.JavaScript;
using Volo.Abp.Domain; using Volo.Abp.Domain;
using Volo.Abp.Http; using Volo.Abp.Http;
@ -68,6 +69,7 @@ namespace Volo.Abp.Cli
{ {
options.Generators[JavaScriptServiceProxyGenerator.Name] = typeof(JavaScriptServiceProxyGenerator); options.Generators[JavaScriptServiceProxyGenerator.Name] = typeof(JavaScriptServiceProxyGenerator);
options.Generators[AngularServiceProxyGenerator.Name] = typeof(AngularServiceProxyGenerator); options.Generators[AngularServiceProxyGenerator.Name] = typeof(AngularServiceProxyGenerator);
options.Generators[CSharpServiceProxyGenerator.Name] = typeof(CSharpServiceProxyGenerator);
}); });
} }
} }

325
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxy/CSharp/CSharpServiceProxyGenerator.cs

@ -0,0 +1,325 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using Volo.Abp.Cli.Http;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Modeling;
using Volo.Abp.Json;
using Volo.Abp.Modularity;
namespace Volo.Abp.Cli.ServiceProxy.CSharp
{
public class CSharpServiceProxyGenerator : ServiceProxyGeneratorBase, ITransientDependency
{
public const string Name = "CSHARP";
public const string UsingPlaceholder = "<using placeholder>";
public const string MethodPlaceholder = "<method placeholder>";
public const string ClassName = "<className>";
public const string ServiceInterface = "<serviceInterface>";
public const string ServicePostfix = "APPSERVICE";
public const string DefaultNamespace = "ClientProxies";
public const string Namespace = "<namespace>";
public readonly string ClientProxyTemplate = "<using placeholder>" +
$"{Environment.NewLine}" +
$"{Environment.NewLine}namespace <namespace>" +
$"{Environment.NewLine}{{" +
$"{Environment.NewLine} public class <className> : ClientProxyBase<<serviceInterface>>, <serviceInterface>" +
$"{Environment.NewLine} {{" +
$"{Environment.NewLine} <method placeholder>" +
$"{Environment.NewLine} }}" +
$"{Environment.NewLine}}}";
private readonly List<string> _usingNamespaceList = new()
{
"using System;",
"using Volo.Abp.Application.Dtos;",
"using Volo.Abp.Http.Client;",
"using Volo.Abp.Http.Modeling;"
};
public CSharpServiceProxyGenerator(
CliHttpClientFactory cliHttpClientFactory,
IJsonSerializer jsonSerializer) :
base(cliHttpClientFactory, jsonSerializer)
{
}
public override async Task GenerateProxyAsync(GenerateProxyArgs args)
{
var projectFilePath = CheckWorkDirectory(args.WorkDirectory);
var projectName = Path.GetFileNameWithoutExtension(projectFilePath);
var assemblyFilePath = Path.Combine(args.WorkDirectory, "bin", "Debug", GetTargetFrameworkVersion(projectFilePath), $"{projectName}.dll");
var startupModule = GetStartupModule(assemblyFilePath);
var appServiceTypes = new List<Type>();
FindAppServiceTypesRecursively(startupModule, appServiceTypes);
appServiceTypes = appServiceTypes.Distinct().ToList();
var applicationApiDescriptionModel = await GetApplicationApiDescriptionModelAsync(args);
foreach (var controller in applicationApiDescriptionModel.Modules[args.Module].Controllers)
{
if (ShouldGenerateProxy(controller.Value))
{
await GenerateClientProxyFile(args, controller.Value, appServiceTypes, startupModule.Namespace);
}
}
}
protected virtual async Task GenerateClientProxyFile(GenerateProxyArgs args, ControllerApiDescriptionModel controllerApiDescription, List<Type> appServiceTypes, string rootNamespace)
{
var appServiceType = appServiceTypes.FirstOrDefault(x => x.FullName == controllerApiDescription.Interfaces.Last().Type);
if (appServiceType == null)
{
return;
}
var folder = DefaultNamespace;
if (args.ExtraProperties.ContainsKey("--folder"))
{
folder = args.ExtraProperties["--folder"];
}
var usingNamespaceList = new List<string>(_usingNamespaceList);
var clientProxyName = $"{controllerApiDescription.ControllerName}ClientProxy";
var clientProxyBuilder = new StringBuilder(ClientProxyTemplate);
clientProxyBuilder.Replace(ClassName, clientProxyName);
clientProxyBuilder.Replace(Namespace, $"{rootNamespace}.{folder.Replace('/','.')}");
clientProxyBuilder.Replace(ServiceInterface, appServiceType.Name);
usingNamespaceList.Add($"using {appServiceType.Namespace};");
var methods = appServiceType.GetInterfaces().SelectMany(x => x.GetMethods()).ToList();
methods.AddRange(appServiceType.GetMethods());
foreach (var method in methods)
{
var actionApiDescription = controllerApiDescription.Actions.Values.FirstOrDefault(x => x.Name == method.Name);
if (actionApiDescription == null)
{
continue;
}
GenerateMethod(actionApiDescription, method, clientProxyBuilder, usingNamespaceList);
}
foreach (var usingNamespace in usingNamespaceList)
{
clientProxyBuilder.Replace($"{UsingPlaceholder}", $"{usingNamespace}{Environment.NewLine}{UsingPlaceholder}");
}
clientProxyBuilder.Replace($"{Environment.NewLine}{UsingPlaceholder}", string.Empty);
clientProxyBuilder.Replace($"{Environment.NewLine} {MethodPlaceholder}", string.Empty);
var filePath = Path.Combine(args.WorkDirectory, folder, clientProxyName + ".cs");
Directory.CreateDirectory(Path.GetDirectoryName(filePath));
using (var writer = new StreamWriter(filePath))
{
await writer.WriteAsync(clientProxyBuilder.ToString());
}
}
protected virtual void GenerateMethod(ActionApiDescriptionModel actionApiDescription, MethodInfo method, StringBuilder clientProxyBuilder, List<string> usingNamespaceList)
{
var methodBuilder = new StringBuilder();
var returnTypeName = GetRealTypeName(usingNamespaceList, method.ReturnType);
if(!typeof(Task).IsAssignableFrom(method.ReturnType))
{
GenerateSynchronizationMethod(method, returnTypeName, methodBuilder, usingNamespaceList);
clientProxyBuilder.Replace(MethodPlaceholder, $"{methodBuilder} {Environment.NewLine} {MethodPlaceholder}");
return;
}
GenerateAsynchronousMethod(actionApiDescription, method, returnTypeName, methodBuilder, usingNamespaceList);
clientProxyBuilder.Replace(MethodPlaceholder, $"{methodBuilder} {Environment.NewLine} {MethodPlaceholder}");
}
private void GenerateSynchronizationMethod(MethodInfo method, string returnTypeName, StringBuilder methodBuilder, List<string> usingNamespaceList)
{
methodBuilder.AppendLine($"public {returnTypeName} {method.Name}(<args>)");
foreach (var parameter in method.GetParameters())
{
methodBuilder.Replace("<args>", $"{GetRealTypeName(usingNamespaceList, parameter.ParameterType)} {parameter.Name}, <args>");
}
methodBuilder.Replace("<args>", string.Empty);
methodBuilder.Replace(", )", ")");
methodBuilder.AppendLine(" {");
methodBuilder.AppendLine(" //Client Proxy does not support the synchronization method, you should always use asynchronous methods as a best practice");
methodBuilder.AppendLine(" throw new System.NotImplementedException(); ");
methodBuilder.AppendLine(" }");
}
private void GenerateAsynchronousMethod(ActionApiDescriptionModel actionApiDescription, MethodInfo method, string returnTypeName, StringBuilder methodBuilder, List<string> usingNamespaceList)
{
methodBuilder.AppendLine($"public async {returnTypeName} {method.Name}(<args>)");
foreach (var parameter in method.GetParameters())
{
methodBuilder.Replace("<args>", $"{GetRealTypeName(usingNamespaceList, parameter.ParameterType)} {parameter.Name}, <args>");
}
methodBuilder.Replace("<args>", string.Empty);
methodBuilder.Replace(", )", ")");
methodBuilder.AppendLine(" {");
methodBuilder.AppendLine(" #region ActionApiDescriptionModel JSON");
methodBuilder.AppendLine($" var actionApiDescription = \"{JsonSerializer.Serialize(actionApiDescription).Replace("\"","\\\"")}\";");
methodBuilder.AppendLine(" #endregion");
methodBuilder.AppendLine("");
methodBuilder.AppendLine(" var action = JsonSerializer.Deserialize<ActionApiDescriptionModel>(actionApiDescription);");
methodBuilder.AppendLine("");
if (method.ReturnType.GenericTypeArguments.IsNullOrEmpty())
{
methodBuilder.AppendLine(" await MakeRequestAsync(action, <args>);");
}
else
{
methodBuilder.AppendLine($" return await MakeRequestAsync<{returnTypeName.Replace("Task<", string.Empty)}(action, <args>);");
}
foreach (var parameter in method.GetParameters())
{
methodBuilder.Replace("<args>", $"{parameter.Name}, <args>");
}
methodBuilder.Replace(", <args>", string.Empty);
methodBuilder.Replace(", )", ")");
methodBuilder.AppendLine("");
methodBuilder.AppendLine(" }");
}
protected virtual bool ShouldGenerateProxy(ControllerApiDescriptionModel controllerApiDescription)
{
if (!controllerApiDescription.Interfaces.Any())
{
return false;
}
var serviceInterface = controllerApiDescription.Interfaces.Last();
return serviceInterface.Type.ToUpper().EndsWith(ServicePostfix);
}
private string GetRealTypeName(List<string> usingNamespaceList, Type type)
{
AddUsingNamespace(usingNamespaceList, type);
if (!type.IsGenericType)
{
return NormalizeTypeName(type.Name);
}
var stringBuilder = new StringBuilder();
stringBuilder.Append(type.Name.Substring(0, type.Name.IndexOf('`')));
stringBuilder.Append('<');
var appendComma = false;
foreach (var arg in type.GetGenericArguments())
{
if (appendComma)
{
stringBuilder.Append(',');
}
stringBuilder.Append(GetRealTypeName(usingNamespaceList, arg));
appendComma = true;
}
stringBuilder.Append('>');
return stringBuilder.ToString();
}
private void AddUsingNamespace(List<string> usingNamespaceList, Type type)
{
var rootNamespace = $"using {type.Namespace};";
if (usingNamespaceList.Contains(type.Namespace) || usingNamespaceList.Any(x => rootNamespace.StartsWith(x)))
{
return;
}
usingNamespaceList.Add(rootNamespace);
}
private string NormalizeTypeName(string typeName)
{
typeName = typeName switch
{
"Void" => "void",
"Boolean" => "bool",
"String" => "string",
"Int32" => "int",
_ => typeName
};
return typeName;
}
private void FindAppServiceTypesRecursively(
Type module,
List<Type> appServiceTypes)
{
var types = module.Assembly
.GetTypes()
.Where(t => t.IsInterface)
.Where(t => typeof(IRemoteService).IsAssignableFrom(t))
.ToList();
appServiceTypes.AddRange(types);
var dependencyDescriptors = module
.GetCustomAttributes()
.OfType<IDependedTypesProvider>();
foreach (var descriptor in dependencyDescriptors)
{
foreach (var dependedModuleType in descriptor.GetDependedTypes().Where(x=>x.Name.EndsWith("HttpApiClientModule") || x.Name.EndsWith("ApplicationContractsModule")))
{
FindAppServiceTypesRecursively(dependedModuleType, appServiceTypes);
}
}
}
private static string CheckWorkDirectory(string directory)
{
if (!Directory.Exists(directory))
{
throw new CliUsageException("Specified directory does not exist.");
}
var projectFiles = Directory.GetFiles(directory, "*HttpApi.Client.csproj");
if (!projectFiles.Any())
{
throw new CliUsageException(
"No project file found in the directory. The working directory must have a HttpApi.Client project file.");
}
return projectFiles.First();
}
private Type GetStartupModule(string assemblyPath)
{
return Assembly
.LoadFrom(assemblyPath)
.GetTypes()
.SingleOrDefault(AbpModule.IsAbpModule);
}
private string GetTargetFrameworkVersion(string projectFilePath)
{
var document = new XmlDocument();
document.Load(projectFilePath);
return document.SelectSingleNode("//TargetFramework").InnerText;
}
}
}

37
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxy/JavaScript/JavaScriptProxyGenerator.cs → framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxy/JavaScript/JavaScriptServiceProxyGenerator.cs

@ -4,41 +4,34 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Volo.Abp.Cli.Http; using Volo.Abp.Cli.Http;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Modeling;
using Volo.Abp.Http.ProxyScripting.Generators.JQuery; using Volo.Abp.Http.ProxyScripting.Generators.JQuery;
using Volo.Abp.Json; using Volo.Abp.Json;
namespace Volo.Abp.Cli.ServiceProxy.JavaScript namespace Volo.Abp.Cli.ServiceProxy.JavaScript
{ {
public class JavaScriptServiceProxyGenerator : IServiceProxyGenerator, ITransientDependency public class JavaScriptServiceProxyGenerator : ServiceProxyGeneratorBase, ITransientDependency
{ {
public const string Name = "JS"; public const string Name = "JS";
public const string EventTriggerScript = "abp.event.trigger('abp.serviceProxyScriptInitialized');"; public const string EventTriggerScript = "abp.event.trigger('abp.serviceProxyScriptInitialized');";
public const string DefaultOutput = "wwwroot/client-proxies"; public const string DefaultOutput = "wwwroot/client-proxies";
public IJsonSerializer JsonSerializer { get; }
private readonly CliHttpClientFactory _cliHttpClientFactory;
private readonly JQueryProxyScriptGenerator _jQueryProxyScriptGenerator; private readonly JQueryProxyScriptGenerator _jQueryProxyScriptGenerator;
public JavaScriptServiceProxyGenerator( public JavaScriptServiceProxyGenerator(
CliHttpClientFactory cliHttpClientFactory, CliHttpClientFactory cliHttpClientFactory,
IJsonSerializer jsonSerializer, IJsonSerializer jsonSerializer,
JQueryProxyScriptGenerator jQueryProxyScriptGenerator) JQueryProxyScriptGenerator jQueryProxyScriptGenerator) :
base(cliHttpClientFactory, jsonSerializer)
{ {
JsonSerializer = jsonSerializer;
_cliHttpClientFactory = cliHttpClientFactory;
_jQueryProxyScriptGenerator = jQueryProxyScriptGenerator; _jQueryProxyScriptGenerator = jQueryProxyScriptGenerator;
} }
public async Task GenerateProxyAsync(GenerateProxyArgs args) public override async Task GenerateProxyAsync(GenerateProxyArgs args)
{ {
Check.NotNullOrWhiteSpace(args.Url, nameof(args.Url));
CheckWorkDirectory(args.WorkDirectory); CheckWorkDirectory(args.WorkDirectory);
var apiDescriptionModel = await GetApplicationApiDescriptionModelAsync(args); var applicationApiDescriptionModel = await GetApplicationApiDescriptionModelAsync(args);
var script = RemoveInitializedEventTrigger(_jQueryProxyScriptGenerator.CreateScript(apiDescriptionModel)); var script = RemoveInitializedEventTrigger(_jQueryProxyScriptGenerator.CreateScript(applicationApiDescriptionModel));
var output = $"{args.WorkDirectory}/{DefaultOutput}/{args.Module}-proxy.js"; var output = $"{args.WorkDirectory}/{DefaultOutput}/{args.Module}-proxy.js";
if (!args.Output.IsNullOrWhiteSpace()) if (!args.Output.IsNullOrWhiteSpace())
@ -54,24 +47,6 @@ namespace Volo.Abp.Cli.ServiceProxy.JavaScript
} }
} }
private async Task<ApplicationApiDescriptionModel> GetApplicationApiDescriptionModelAsync(GenerateProxyArgs args)
{
var client = _cliHttpClientFactory.CreateClient();
var apiDefinitionResult = await client.GetStringAsync(CliUrls.GetApiDefinitionUrl(args.Url));
var apiDefinition = JsonSerializer.Deserialize<ApplicationApiDescriptionModel>(apiDefinitionResult);
if (!apiDefinition.Modules.TryGetValue(args.Module, out var moduleDefinition))
{
throw new CliUsageException($"Module name: {args.Module} is invalid");
}
var apiDescriptionModel = ApplicationApiDescriptionModel.Create();
apiDescriptionModel.AddModule(moduleDefinition);
return apiDescriptionModel;
}
private static void CheckWorkDirectory(string directory) private static void CheckWorkDirectory(string directory)
{ {
if (!Directory.Exists(directory)) if (!Directory.Exists(directory))

42
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxy/ServiceProxyGeneratorBase.cs

@ -0,0 +1,42 @@
using System.Threading.Tasks;
using Volo.Abp.Cli.Http;
using Volo.Abp.Http.Modeling;
using Volo.Abp.Json;
namespace Volo.Abp.Cli.ServiceProxy
{
public abstract class ServiceProxyGeneratorBase : IServiceProxyGenerator
{
public IJsonSerializer JsonSerializer { get; }
public CliHttpClientFactory CliHttpClientFactory { get; }
protected ServiceProxyGeneratorBase(CliHttpClientFactory cliHttpClientFactory, IJsonSerializer jsonSerializer)
{
CliHttpClientFactory = cliHttpClientFactory;
JsonSerializer = jsonSerializer;
}
public abstract Task GenerateProxyAsync(GenerateProxyArgs args);
protected virtual async Task<ApplicationApiDescriptionModel> GetApplicationApiDescriptionModelAsync(GenerateProxyArgs args)
{
Check.NotNull(args.Url, nameof(args.Url));
var client = CliHttpClientFactory.CreateClient();
var apiDefinitionResult = await client.GetStringAsync(CliUrls.GetApiDefinitionUrl(args.Url));
var apiDefinition = JsonSerializer.Deserialize<ApplicationApiDescriptionModel>(apiDefinitionResult);
if (!apiDefinition.Modules.TryGetValue(args.Module, out var moduleDefinition))
{
throw new CliUsageException($"Module name: {args.Module} is invalid");
}
var apiDescriptionModel = ApplicationApiDescriptionModel.Create();
apiDescriptionModel.AddModule(moduleDefinition);
return apiDescriptionModel;
}
}
}

40
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxyBase.cs

@ -0,0 +1,40 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Modeling;
using Volo.Abp.Json;
namespace Volo.Abp.Http.Client
{
public class ClientProxyBase<TService>
{
public IAbpLazyServiceProvider LazyServiceProvider { get; set; }
protected IHttpProxyExecuter HttpProxyExecuter => LazyServiceProvider.LazyGetRequiredService<IHttpProxyExecuter>();
protected IJsonSerializer JsonSerializer => LazyServiceProvider.LazyGetRequiredService<IJsonSerializer>();
protected virtual async Task MakeRequestAsync(ActionApiDescriptionModel action, params object[] arguments)
{
await HttpProxyExecuter.MakeRequestAsync(new HttpProxyExecuterContext(action, BuildArguments(action.Name, arguments), typeof(TService)));
}
protected virtual async Task<T> MakeRequestAsync<T>(ActionApiDescriptionModel action, params object[] arguments)
{
return await HttpProxyExecuter.MakeRequestAndGetResultAsync<T>(new HttpProxyExecuterContext(action, BuildArguments(action.Name, arguments), typeof(TService)));
}
protected virtual Dictionary<string, object> BuildArguments(string methodName, object[] arguments)
{
var method = typeof(TService).GetMethod(methodName);
var dict = new Dictionary<string, object>();
var methodParameters = method.GetParameters();
for (var i = 0; i < methodParameters.Length; i++)
{
dict[methodParameters[i].Name] = arguments[i];
}
return dict;
}
}
}

281
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs

@ -1,26 +1,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection; using System.Reflection;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Volo.Abp.Content;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy; using Volo.Abp.DynamicProxy;
using Volo.Abp.Http.Client.Authentication;
using Volo.Abp.Http.Modeling; 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.DynamicProxying namespace Volo.Abp.Http.Client.DynamicProxying
{ {
@ -29,63 +17,54 @@ namespace Volo.Abp.Http.Client.DynamicProxying
// ReSharper disable once StaticMemberInGenericType // ReSharper disable once StaticMemberInGenericType
protected static MethodInfo MakeRequestAndGetResultAsyncMethod { get; } protected static MethodInfo MakeRequestAndGetResultAsyncMethod { get; }
protected ICancellationTokenProvider CancellationTokenProvider { get; } protected AbpHttpClientOptions ClientOptions { get; }
protected ICorrelationIdProvider CorrelationIdProvider { get; } protected IHttpProxyExecuter HttpProxyExecuter { get; }
protected ICurrentTenant CurrentTenant { get; }
protected AbpCorrelationIdOptions AbpCorrelationIdOptions { get; }
protected IDynamicProxyHttpClientFactory HttpClientFactory { get; } protected IDynamicProxyHttpClientFactory HttpClientFactory { get; }
protected IApiDescriptionFinder ApiDescriptionFinder { get; }
protected IRemoteServiceConfigurationProvider RemoteServiceConfigurationProvider { get; } protected IRemoteServiceConfigurationProvider RemoteServiceConfigurationProvider { get; }
protected AbpHttpClientOptions ClientOptions { get; } protected IApiDescriptionFinder ApiDescriptionFinder { get; }
protected IJsonSerializer JsonSerializer { get; }
protected IRemoteServiceHttpClientAuthenticator ClientAuthenticator { get; }
public ILogger<DynamicHttpProxyInterceptor<TService>> Logger { get; set; } public ILogger<DynamicHttpProxyInterceptor<TService>> Logger { get; set; }
static DynamicHttpProxyInterceptor() static DynamicHttpProxyInterceptor()
{ {
MakeRequestAndGetResultAsyncMethod = typeof(DynamicHttpProxyInterceptor<TService>) MakeRequestAndGetResultAsyncMethod = typeof(HttpProxyExecuter)
.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance) .GetMethods(BindingFlags.Public | BindingFlags.Instance)
.First(m => m.Name == nameof(MakeRequestAndGetResultAsync) && m.IsGenericMethodDefinition); .First(m => m.Name == nameof(IHttpProxyExecuter.MakeRequestAndGetResultAsync) && m.IsGenericMethodDefinition);
} }
public DynamicHttpProxyInterceptor( public DynamicHttpProxyInterceptor(
IDynamicProxyHttpClientFactory httpClientFactory, IHttpProxyExecuter httpProxyExecuter,
IOptions<AbpHttpClientOptions> clientOptions, IOptions<AbpHttpClientOptions> clientOptions,
IApiDescriptionFinder apiDescriptionFinder, IDynamicProxyHttpClientFactory httpClientFactory,
IJsonSerializer jsonSerializer, IRemoteServiceConfigurationProvider remoteServiceConfigurationProvider,
IRemoteServiceHttpClientAuthenticator clientAuthenticator, IApiDescriptionFinder apiDescriptionFinder)
ICancellationTokenProvider cancellationTokenProvider,
ICorrelationIdProvider correlationIdProvider,
IOptions<AbpCorrelationIdOptions> correlationIdOptions,
ICurrentTenant currentTenant,
IRemoteServiceConfigurationProvider remoteServiceConfigurationProvider)
{ {
CancellationTokenProvider = cancellationTokenProvider; HttpProxyExecuter = httpProxyExecuter;
CorrelationIdProvider = correlationIdProvider;
CurrentTenant = currentTenant;
RemoteServiceConfigurationProvider = remoteServiceConfigurationProvider;
AbpCorrelationIdOptions = correlationIdOptions.Value;
HttpClientFactory = httpClientFactory; HttpClientFactory = httpClientFactory;
RemoteServiceConfigurationProvider = remoteServiceConfigurationProvider;
ApiDescriptionFinder = apiDescriptionFinder; ApiDescriptionFinder = apiDescriptionFinder;
JsonSerializer = jsonSerializer;
ClientAuthenticator = clientAuthenticator;
ClientOptions = clientOptions.Value; ClientOptions = clientOptions.Value;
Logger = NullLogger<DynamicHttpProxyInterceptor<TService>>.Instance; Logger = NullLogger<DynamicHttpProxyInterceptor<TService>>.Instance;
} }
public override async Task InterceptAsync(IAbpMethodInvocation invocation) public override async Task InterceptAsync(IAbpMethodInvocation invocation)
{ {
var context = new HttpProxyExecuterContext(
await GetActionApiDescriptionModel(invocation),
invocation.ArgumentsDictionary,
typeof(TService));
if (invocation.Method.ReturnType.GenericTypeArguments.IsNullOrEmpty()) if (invocation.Method.ReturnType.GenericTypeArguments.IsNullOrEmpty())
{ {
await MakeRequestAsync(invocation); await HttpProxyExecuter.MakeRequestAsync(context);
} }
else else
{ {
var result = (Task)MakeRequestAndGetResultAsyncMethod var result = (Task)MakeRequestAndGetResultAsyncMethod
.MakeGenericMethod(invocation.Method.ReturnType.GenericTypeArguments[0]) .MakeGenericMethod(invocation.Method.ReturnType.GenericTypeArguments[0])
.Invoke(this, new object[] { invocation }); .Invoke(this, new object[] { context });
invocation.ReturnValue = await GetResultAsync( invocation.ReturnValue = await GetResultAsync(
result, result,
@ -94,231 +73,27 @@ namespace Volo.Abp.Http.Client.DynamicProxying
} }
} }
private async Task<object> GetResultAsync(Task task, Type resultType) private async Task<ActionApiDescriptionModel> GetActionApiDescriptionModel(IAbpMethodInvocation invocation)
{
await task;
return typeof(Task<>)
.MakeGenericType(resultType)
.GetProperty(nameof(Task<object>.Result), BindingFlags.Instance | BindingFlags.Public)
.GetValue(task);
}
private async Task<T> MakeRequestAndGetResultAsync<T>(IAbpMethodInvocation invocation)
{
var responseContent = await MakeRequestAsync(invocation);
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())
{
ContentType = responseContent.Headers.ContentType?.ToString(),
FileName = responseContent.Headers?.ContentDisposition?.FileNameStar ??
RemoveQuotes(responseContent.Headers?.ContentDisposition?.FileName).ToString()
};
}
var stringContent = await responseContent.ReadAsStringAsync();
if (typeof(T) == typeof(string))
{
return (T)(object)stringContent;
}
if (stringContent.IsNullOrWhiteSpace())
{
return default;
}
return JsonSerializer.Deserialize<T>(stringContent);
}
private async Task<HttpContent> MakeRequestAsync(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 remoteServiceConfig = await RemoteServiceConfigurationProvider.GetConfigurationOrDefaultAsync(clientConfig.RemoteServiceName);
var client = HttpClientFactory.Create(clientConfig.RemoteServiceName); var client = HttpClientFactory.Create(clientConfig.RemoteServiceName);
var action = await ApiDescriptionFinder.FindActionAsync( return await ApiDescriptionFinder.FindActionAsync(
client, client,
remoteServiceConfig.BaseUrl, remoteServiceConfig.BaseUrl,
typeof(TService), typeof(TService),
invocation.Method invocation.Method
); );
var apiVersion = await GetApiVersionInfoAsync(action);
var url = remoteServiceConfig.BaseUrl.EnsureEndsWith('/') + UrlBuilder.GenerateUrlWithParameters(action, invocation.ArgumentsDictionary, apiVersion);
var requestMessage = new HttpRequestMessage(action.GetHttpMethod(), url)
{
Content = RequestPayloadBuilder.BuildContent(action, invocation.ArgumentsDictionary, JsonSerializer, apiVersion)
};
AddHeaders(invocation, action, requestMessage, apiVersion);
if (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(invocation)
);
if (!response.IsSuccessStatusCode)
{
await ThrowExceptionForResponseAsync(response);
}
return response.Content;
}
private async Task<ApiVersionInfo> GetApiVersionInfoAsync(ActionApiDescriptionModel action)
{
var apiVersion = await FindBestApiVersionAsync(action);
//TODO: Make names configurable?
var versionParam = action.Parameters.FirstOrDefault(p => p.Name == "apiVersion" && p.BindingSourceId == ParameterBindingSources.Path) ??
action.Parameters.FirstOrDefault(p => p.Name == "api-version" && p.BindingSourceId == ParameterBindingSources.Query);
return new ApiVersionInfo(versionParam?.BindingSourceId, apiVersion);
}
private async Task<string> FindBestApiVersionAsync(ActionApiDescriptionModel action)
{
var configuredVersion = await GetConfiguredApiVersionAsync();
if (action.SupportedVersions.IsNullOrEmpty())
{
return configuredVersion ?? "1.0";
}
if (action.SupportedVersions.Contains(configuredVersion))
{
return configuredVersion;
}
return action.SupportedVersions.Last(); //TODO: Ensure to get the latest version!
} }
protected virtual void AddHeaders( private async Task<object> GetResultAsync(Task task, Type resultType)
IAbpMethodInvocation invocation,
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(invocation.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");
}
private async Task<string> GetConfiguredApiVersionAsync()
{
var clientConfig = ClientOptions.HttpClientProxies.GetOrDefault(typeof(TService))
?? throw new AbpException($"Could not get DynamicHttpClientProxyConfig for {typeof(TService).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 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(IAbpMethodInvocation invocation)
{ {
var cancellationTokenArg = invocation.Arguments.LastOrDefault(x => x is CancellationToken); await task;
if (cancellationTokenArg != null) return typeof(Task<>)
{ .MakeGenericType(resultType)
var cancellationToken = (CancellationToken) cancellationTokenArg; .GetProperty(nameof(Task<object>.Result), BindingFlags.Instance | BindingFlags.Public)
if (cancellationToken != default) .GetValue(task);
{
return cancellationToken;
}
}
return CancellationTokenProvider.Token;
} }
} }
} }

269
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/HttpProxyExecuter.cs

@ -0,0 +1,269 @@
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.DynamicProxying;
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
{
public class HttpProxyExecuter : IHttpProxyExecuter, ITransientDependency
{
protected ICancellationTokenProvider CancellationTokenProvider { get; }
protected ICorrelationIdProvider CorrelationIdProvider { get; }
protected ICurrentTenant CurrentTenant { get; }
protected AbpCorrelationIdOptions AbpCorrelationIdOptions { get; }
protected IDynamicProxyHttpClientFactory 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,
IDynamicProxyHttpClientFactory 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())
{
ContentType = responseContent.Headers.ContentType?.ToString(),
FileName = responseContent.Headers?.ContentDisposition?.FileNameStar ??
RemoveQuotes(responseContent.Headers?.ContentDisposition?.FileName).ToString()
};
}
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 DynamicHttpClientProxyConfig 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;
}
}
}

30
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/HttpProxyExecuterContext.cs

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Volo.Abp.Http.Modeling;
namespace Volo.Abp.Http.Client
{
public class HttpProxyExecuterContext
{
[NotNull]
public ActionApiDescriptionModel Action { get; }
[NotNull]
public IReadOnlyDictionary<string, object> Arguments { get; }
[NotNull]
public Type ServiceType { get; }
public HttpProxyExecuterContext(
[NotNull] ActionApiDescriptionModel action,
[NotNull] IReadOnlyDictionary<string, object> arguments,
[NotNull] Type serviceType)
{
ServiceType = serviceType;
Action = Check.NotNull(action, nameof(action));
Arguments = Check.NotNull(arguments, nameof(arguments));
ServiceType = Check.NotNull(serviceType, nameof(serviceType));
}
}
}

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

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