diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs index 2e7a270918..a6c4aedd85 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs +++ b/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.ServiceProxy; using Volo.Abp.Cli.ServiceProxy.Angular; +using Volo.Abp.Cli.ServiceProxy.CSharp; using Volo.Abp.Cli.ServiceProxy.JavaScript; using Volo.Abp.Domain; using Volo.Abp.Http; @@ -68,6 +69,7 @@ namespace Volo.Abp.Cli { options.Generators[JavaScriptServiceProxyGenerator.Name] = typeof(JavaScriptServiceProxyGenerator); options.Generators[AngularServiceProxyGenerator.Name] = typeof(AngularServiceProxyGenerator); + options.Generators[CSharpServiceProxyGenerator.Name] = typeof(CSharpServiceProxyGenerator); }); } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxy/CSharp/CSharpServiceProxyGenerator.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxy/CSharp/CSharpServiceProxyGenerator.cs new file mode 100644 index 0000000000..78eb25c511 --- /dev/null +++ b/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 = ""; + public const string MethodPlaceholder = ""; + public const string ClassName = ""; + public const string ServiceInterface = ""; + public const string ServicePostfix = "APPSERVICE"; + public const string DefaultNamespace = "ClientProxies"; + public const string Namespace = ""; + + public readonly string ClientProxyTemplate = "" + + $"{Environment.NewLine}" + + $"{Environment.NewLine}namespace " + + $"{Environment.NewLine}{{" + + $"{Environment.NewLine} public class : ClientProxyBase<>, " + + $"{Environment.NewLine} {{" + + $"{Environment.NewLine} " + + $"{Environment.NewLine} }}" + + $"{Environment.NewLine}}}"; + + private readonly List _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(); + 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 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(_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 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 usingNamespaceList) + { + methodBuilder.AppendLine($"public {returnTypeName} {method.Name}()"); + + foreach (var parameter in method.GetParameters()) + { + methodBuilder.Replace("", $"{GetRealTypeName(usingNamespaceList, parameter.ParameterType)} {parameter.Name}, "); + } + + methodBuilder.Replace("", 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 usingNamespaceList) + { + methodBuilder.AppendLine($"public async {returnTypeName} {method.Name}()"); + + foreach (var parameter in method.GetParameters()) + { + methodBuilder.Replace("", $"{GetRealTypeName(usingNamespaceList, parameter.ParameterType)} {parameter.Name}, "); + } + + methodBuilder.Replace("", 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(actionApiDescription);"); + methodBuilder.AppendLine(""); + + if (method.ReturnType.GenericTypeArguments.IsNullOrEmpty()) + { + methodBuilder.AppendLine(" await MakeRequestAsync(action, );"); + } + else + { + methodBuilder.AppendLine($" return await MakeRequestAsync<{returnTypeName.Replace("Task<", string.Empty)}(action, );"); + } + + foreach (var parameter in method.GetParameters()) + { + methodBuilder.Replace("", $"{parameter.Name}, "); + } + + methodBuilder.Replace(", ", 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 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 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 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(); + + 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; + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxy/JavaScript/JavaScriptProxyGenerator.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxy/JavaScript/JavaScriptServiceProxyGenerator.cs similarity index 60% rename from framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxy/JavaScript/JavaScriptProxyGenerator.cs rename to framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxy/JavaScript/JavaScriptServiceProxyGenerator.cs index f0f80d694e..e97ba8540b 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxy/JavaScript/JavaScriptProxyGenerator.cs +++ b/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 Volo.Abp.Cli.Http; using Volo.Abp.DependencyInjection; -using Volo.Abp.Http.Modeling; using Volo.Abp.Http.ProxyScripting.Generators.JQuery; using Volo.Abp.Json; namespace Volo.Abp.Cli.ServiceProxy.JavaScript { - public class JavaScriptServiceProxyGenerator : IServiceProxyGenerator, ITransientDependency + public class JavaScriptServiceProxyGenerator : ServiceProxyGeneratorBase, ITransientDependency { public const string Name = "JS"; public const string EventTriggerScript = "abp.event.trigger('abp.serviceProxyScriptInitialized');"; public const string DefaultOutput = "wwwroot/client-proxies"; - public IJsonSerializer JsonSerializer { get; } - - private readonly CliHttpClientFactory _cliHttpClientFactory; - private readonly JQueryProxyScriptGenerator _jQueryProxyScriptGenerator; public JavaScriptServiceProxyGenerator( CliHttpClientFactory cliHttpClientFactory, IJsonSerializer jsonSerializer, - JQueryProxyScriptGenerator jQueryProxyScriptGenerator) + JQueryProxyScriptGenerator jQueryProxyScriptGenerator) : + base(cliHttpClientFactory, jsonSerializer) { - JsonSerializer = jsonSerializer; - _cliHttpClientFactory = cliHttpClientFactory; _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); - var apiDescriptionModel = await GetApplicationApiDescriptionModelAsync(args); - var script = RemoveInitializedEventTrigger(_jQueryProxyScriptGenerator.CreateScript(apiDescriptionModel)); + var applicationApiDescriptionModel = await GetApplicationApiDescriptionModelAsync(args); + var script = RemoveInitializedEventTrigger(_jQueryProxyScriptGenerator.CreateScript(applicationApiDescriptionModel)); var output = $"{args.WorkDirectory}/{DefaultOutput}/{args.Module}-proxy.js"; if (!args.Output.IsNullOrWhiteSpace()) @@ -54,24 +47,6 @@ namespace Volo.Abp.Cli.ServiceProxy.JavaScript } } - private async Task GetApplicationApiDescriptionModelAsync(GenerateProxyArgs args) - { - var client = _cliHttpClientFactory.CreateClient(); - - var apiDefinitionResult = await client.GetStringAsync(CliUrls.GetApiDefinitionUrl(args.Url)); - var apiDefinition = JsonSerializer.Deserialize(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) { if (!Directory.Exists(directory)) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxy/ServiceProxyGeneratorBase.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxy/ServiceProxyGeneratorBase.cs new file mode 100644 index 0000000000..b90fd88f1e --- /dev/null +++ b/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 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(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; + } + } +} diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxyBase.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxyBase.cs new file mode 100644 index 0000000000..bed5bcb268 --- /dev/null +++ b/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 + { + public IAbpLazyServiceProvider LazyServiceProvider { get; set; } + + protected IHttpProxyExecuter HttpProxyExecuter => LazyServiceProvider.LazyGetRequiredService(); + protected IJsonSerializer JsonSerializer => LazyServiceProvider.LazyGetRequiredService(); + + 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 MakeRequestAsync(ActionApiDescriptionModel action, params object[] arguments) + { + return await HttpProxyExecuter.MakeRequestAndGetResultAsync(new HttpProxyExecuterContext(action, BuildArguments(action.Name, arguments), typeof(TService))); + } + + protected virtual Dictionary BuildArguments(string methodName, object[] arguments) + { + var method = typeof(TService).GetMethod(methodName); + var dict = new Dictionary(); + + var methodParameters = method.GetParameters(); + for (var i = 0; i < methodParameters.Length; i++) + { + dict[methodParameters[i].Name] = arguments[i]; + } + + return dict; + } + } +} diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs index df890812bb..cf138a7b3c 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs @@ -1,26 +1,14 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; using System.Reflection; -using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; -using Volo.Abp.Content; using Volo.Abp.DependencyInjection; using Volo.Abp.DynamicProxy; -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.DynamicProxying { @@ -29,63 +17,54 @@ namespace Volo.Abp.Http.Client.DynamicProxying // ReSharper disable once StaticMemberInGenericType protected static MethodInfo MakeRequestAndGetResultAsyncMethod { get; } - protected ICancellationTokenProvider CancellationTokenProvider { get; } - protected ICorrelationIdProvider CorrelationIdProvider { get; } - protected ICurrentTenant CurrentTenant { get; } - protected AbpCorrelationIdOptions AbpCorrelationIdOptions { get; } + protected AbpHttpClientOptions ClientOptions { get; } + protected IHttpProxyExecuter HttpProxyExecuter { get; } protected IDynamicProxyHttpClientFactory HttpClientFactory { get; } - protected IApiDescriptionFinder ApiDescriptionFinder { get; } protected IRemoteServiceConfigurationProvider RemoteServiceConfigurationProvider { get; } - protected AbpHttpClientOptions ClientOptions { get; } - protected IJsonSerializer JsonSerializer { get; } - protected IRemoteServiceHttpClientAuthenticator ClientAuthenticator { get; } + protected IApiDescriptionFinder ApiDescriptionFinder { get; } public ILogger> Logger { get; set; } static DynamicHttpProxyInterceptor() { - MakeRequestAndGetResultAsyncMethod = typeof(DynamicHttpProxyInterceptor) - .GetMethods(BindingFlags.NonPublic | BindingFlags.Instance) - .First(m => m.Name == nameof(MakeRequestAndGetResultAsync) && m.IsGenericMethodDefinition); + MakeRequestAndGetResultAsyncMethod = typeof(HttpProxyExecuter) + .GetMethods(BindingFlags.Public | BindingFlags.Instance) + .First(m => m.Name == nameof(IHttpProxyExecuter.MakeRequestAndGetResultAsync) && m.IsGenericMethodDefinition); } public DynamicHttpProxyInterceptor( - IDynamicProxyHttpClientFactory httpClientFactory, + IHttpProxyExecuter httpProxyExecuter, IOptions clientOptions, - IApiDescriptionFinder apiDescriptionFinder, - IJsonSerializer jsonSerializer, - IRemoteServiceHttpClientAuthenticator clientAuthenticator, - ICancellationTokenProvider cancellationTokenProvider, - ICorrelationIdProvider correlationIdProvider, - IOptions correlationIdOptions, - ICurrentTenant currentTenant, - IRemoteServiceConfigurationProvider remoteServiceConfigurationProvider) + IDynamicProxyHttpClientFactory httpClientFactory, + IRemoteServiceConfigurationProvider remoteServiceConfigurationProvider, + IApiDescriptionFinder apiDescriptionFinder) { - CancellationTokenProvider = cancellationTokenProvider; - CorrelationIdProvider = correlationIdProvider; - CurrentTenant = currentTenant; - RemoteServiceConfigurationProvider = remoteServiceConfigurationProvider; - AbpCorrelationIdOptions = correlationIdOptions.Value; + HttpProxyExecuter = httpProxyExecuter; HttpClientFactory = httpClientFactory; + RemoteServiceConfigurationProvider = remoteServiceConfigurationProvider; ApiDescriptionFinder = apiDescriptionFinder; - JsonSerializer = jsonSerializer; - ClientAuthenticator = clientAuthenticator; ClientOptions = clientOptions.Value; Logger = NullLogger>.Instance; } + public override async Task InterceptAsync(IAbpMethodInvocation invocation) { + var context = new HttpProxyExecuterContext( + await GetActionApiDescriptionModel(invocation), + invocation.ArgumentsDictionary, + typeof(TService)); + if (invocation.Method.ReturnType.GenericTypeArguments.IsNullOrEmpty()) { - await MakeRequestAsync(invocation); + await HttpProxyExecuter.MakeRequestAsync(context); } else { var result = (Task)MakeRequestAndGetResultAsyncMethod .MakeGenericMethod(invocation.Method.ReturnType.GenericTypeArguments[0]) - .Invoke(this, new object[] { invocation }); + .Invoke(this, new object[] { context }); invocation.ReturnValue = await GetResultAsync( result, @@ -94,231 +73,27 @@ namespace Volo.Abp.Http.Client.DynamicProxying } } - private async Task GetResultAsync(Task task, Type resultType) - { - await task; - return typeof(Task<>) - .MakeGenericType(resultType) - .GetProperty(nameof(Task.Result), BindingFlags.Instance | BindingFlags.Public) - .GetValue(task); - } - - private async Task MakeRequestAndGetResultAsync(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(stringContent); - } - - private async Task MakeRequestAsync(IAbpMethodInvocation invocation) + private async Task GetActionApiDescriptionModel(IAbpMethodInvocation invocation) { 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); - var action = await ApiDescriptionFinder.FindActionAsync( + return await ApiDescriptionFinder.FindActionAsync( client, remoteServiceConfig.BaseUrl, typeof(TService), 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 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 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( - 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 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( - 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) + private async Task GetResultAsync(Task task, Type resultType) { - var cancellationTokenArg = invocation.Arguments.LastOrDefault(x => x is CancellationToken); - if (cancellationTokenArg != null) - { - var cancellationToken = (CancellationToken) cancellationTokenArg; - if (cancellationToken != default) - { - return cancellationToken; - } - } - - return CancellationTokenProvider.Token; + await task; + return typeof(Task<>) + .MakeGenericType(resultType) + .GetProperty(nameof(Task.Result), BindingFlags.Instance | BindingFlags.Public) + .GetValue(task); } } } diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/HttpProxyExecuter.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/HttpProxyExecuter.cs new file mode 100644 index 0000000000..cc0fd1c381 --- /dev/null +++ b/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, + IDynamicProxyHttpClientFactory httpClientFactory, + IRemoteServiceConfigurationProvider remoteServiceConfigurationProvider, + IOptions clientOptions, + IRemoteServiceHttpClientAuthenticator clientAuthenticator, + IJsonSerializer jsonSerializer) + { + CancellationTokenProvider = cancellationTokenProvider; + CorrelationIdProvider = correlationIdProvider; + CurrentTenant = currentTenant; + AbpCorrelationIdOptions = abpCorrelationIdOptions.Value; + HttpClientFactory = httpClientFactory; + RemoteServiceConfigurationProvider = remoteServiceConfigurationProvider; + ClientOptions = clientOptions.Value; + ClientAuthenticator = clientAuthenticator; + JsonSerializer = jsonSerializer; + } + + public virtual async Task MakeRequestAndGetResultAsync(HttpProxyExecuterContext context) + { + var responseContent = await MakeRequestAsync(context); + + if (typeof(T) == typeof(IRemoteStreamContent) || + typeof(T) == typeof(RemoteStreamContent)) + { + /* returning a class that holds a reference to response + * content just to be sure that GC does not dispose of + * it before we finish doing our work with the stream */ + return (T)(object)new RemoteStreamContent(await responseContent.ReadAsStreamAsync()) + { + 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(stringContent); + } + + public virtual async Task 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 GetApiVersionInfoAsync(HttpProxyExecuterContext context) + { + var apiVersion = await FindBestApiVersionAsync(context); + + //TODO: Make names configurable? + var versionParam = context.Action.Parameters.FirstOrDefault(p => p.Name == "apiVersion" && p.BindingSourceId == ParameterBindingSources.Path) ?? + context.Action.Parameters.FirstOrDefault(p => p.Name == "api-version" && p.BindingSourceId == ParameterBindingSources.Query); + + return new ApiVersionInfo(versionParam?.BindingSourceId, apiVersion); + } + + private async Task FindBestApiVersionAsync(HttpProxyExecuterContext context) + { + var configuredVersion = await GetConfiguredApiVersionAsync(context); + + if (context.Action.SupportedVersions.IsNullOrEmpty()) + { + return configuredVersion ?? "1.0"; + } + + if (context.Action.SupportedVersions.Contains(configuredVersion)) + { + return configuredVersion; + } + + return context.Action.SupportedVersions.Last(); //TODO: Ensure to get the latest version! + } + + private async Task GetConfiguredApiVersionAsync(HttpProxyExecuterContext context) + { + var clientConfig = ClientOptions.HttpClientProxies.GetOrDefault(context.ServiceType) + ?? throw new AbpException($"Could not get DynamicHttpClientProxyConfig for {context.ServiceType.FullName}."); + + return (await RemoteServiceConfigurationProvider + .GetConfigurationOrDefaultOrNullAsync(clientConfig.RemoteServiceName))?.Version; + } + + private async Task ThrowExceptionForResponseAsync(HttpResponseMessage response) + { + if (response.Headers.Contains(AbpHttpConsts.AbpErrorFormat)) + { + var errorResponse = JsonSerializer.Deserialize( + await response.Content.ReadAsStringAsync() + ); + + throw new AbpRemoteCallException(errorResponse.Error) + { + HttpStatusCode = (int) response.StatusCode + }; + } + + throw new AbpRemoteCallException( + new RemoteServiceErrorInfo + { + Message = response.ReasonPhrase, + Code = response.StatusCode.ToString() + } + ) + { + HttpStatusCode = (int) response.StatusCode + }; + } + + protected virtual void AddHeaders( + IReadOnlyDictionary argumentsDictionary, + ActionApiDescriptionModel action, + HttpRequestMessage requestMessage, + ApiVersionInfo apiVersion) + { + //API Version + if (!apiVersion.Version.IsNullOrEmpty()) + { + //TODO: What about other media types? + requestMessage.Headers.Add("accept", $"{MimeTypes.Text.Plain}; v={apiVersion.Version}"); + requestMessage.Headers.Add("accept", $"{MimeTypes.Application.Json}; v={apiVersion.Version}"); + requestMessage.Headers.Add("api-version", apiVersion.Version); + } + + //Header parameters + var headers = action.Parameters.Where(p => p.BindingSourceId == ParameterBindingSources.Header).ToArray(); + foreach (var headerParameter in headers) + { + var value = HttpActionParameterHelper.FindParameterValue(argumentsDictionary, headerParameter); + if (value != null) + { + requestMessage.Headers.Add(headerParameter.Name, value.ToString()); + } + } + + //CorrelationId + requestMessage.Headers.Add(AbpCorrelationIdOptions.HttpHeaderName, CorrelationIdProvider.Get()); + + //TenantId + if (CurrentTenant.Id.HasValue) + { + //TODO: Use AbpAspNetCoreMultiTenancyOptions to get the key + requestMessage.Headers.Add(TenantResolverConsts.DefaultTenantKey, CurrentTenant.Id.Value.ToString()); + } + + //Culture + //TODO: Is that the way we want? Couldn't send the culture (not ui culture) + var currentCulture = CultureInfo.CurrentUICulture.Name ?? CultureInfo.CurrentCulture.Name; + if (!currentCulture.IsNullOrEmpty()) + { + requestMessage.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue(currentCulture)); + } + + //X-Requested-With + requestMessage.Headers.Add("X-Requested-With", "XMLHttpRequest"); + } + + protected virtual StringSegment RemoveQuotes(StringSegment input) + { + if (!StringSegment.IsNullOrEmpty(input) && input.Length >= 2 && input[0] == '"' && input[input.Length - 1] == '"') + { + input = input.Subsegment(1, input.Length - 2); + } + + return input; + } + + protected virtual CancellationToken GetCancellationToken(IReadOnlyDictionary arguments) + { + var cancellationTokenArg = arguments.LastOrDefault(); + + if (cancellationTokenArg.Value is CancellationToken cancellationToken) + { + if (cancellationToken != default) + { + return cancellationToken; + } + } + + return CancellationTokenProvider.Token; + } + } +} diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/HttpProxyExecuterContext.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/HttpProxyExecuterContext.cs new file mode 100644 index 0000000000..5e432202aa --- /dev/null +++ b/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 Arguments { get; } + + [NotNull] + public Type ServiceType { get; } + + public HttpProxyExecuterContext( + [NotNull] ActionApiDescriptionModel action, + [NotNull] IReadOnlyDictionary arguments, + [NotNull] Type serviceType) + { + ServiceType = serviceType; + Action = Check.NotNull(action, nameof(action)); + Arguments = Check.NotNull(arguments, nameof(arguments)); + ServiceType = Check.NotNull(serviceType, nameof(serviceType)); + } + } +} diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/IHttpProxyExecuter.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/IHttpProxyExecuter.cs new file mode 100644 index 0000000000..9e1b56c451 --- /dev/null +++ b/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 MakeRequestAsync(HttpProxyExecuterContext context); + + Task MakeRequestAndGetResultAsync(HttpProxyExecuterContext context); + } +}