mirror of https://github.com/abpframework/abp.git
9 changed files with 754 additions and 284 deletions
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
} |
|||
@ -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…
Reference in new issue