Browse Source

增强cli功能

1:labp
增加命令行**generate-proxy**,仅限于CSharp客户端代理,增加对于dapr客户端代理的支持,详情见**labp help generate-proxy**
pull/684/head
cKey 3 years ago
parent
commit
6a81c0922f
  1. 5
      aspnet-core/modules/cli/LINGYUN.Abp.Cli/LINGYUN/Abp/Cli/AbpCliModule.cs
  2. 4
      aspnet-core/modules/cli/LINGYUN.Abp.Cli/LINGYUN/Abp/Cli/Commands/CreateCommand.cs
  3. 146
      aspnet-core/modules/cli/LINGYUN.Abp.Cli/LINGYUN/Abp/Cli/Commands/GenerateProxyCommand.cs
  4. 4
      aspnet-core/modules/cli/LINGYUN.Abp.Cli/LINGYUN/Abp/Cli/Commands/HelpCommand.cs
  5. 422
      aspnet-core/modules/cli/LINGYUN.Abp.Cli/LINGYUN/Abp/Cli/ServiceProxying/CSharp/CSharpServiceProxyGenerator.cs
  6. 26
      aspnet-core/modules/cli/LINGYUN.Abp.Cli/LINGYUN/Abp/Cli/ServiceProxying/GenerateProxyArgs.cs

5
aspnet-core/modules/cli/LINGYUN.Abp.Cli/LINGYUN/Abp/Cli/AbpCliModule.cs

@ -16,8 +16,9 @@ namespace LINGYUN.Abp.Cli
Configure<AbpCliOptions>(options =>
{
options.Commands.Clear();
options.Commands["help"] = typeof(HelpCommand);
options.Commands["create"] = typeof(CreateCommand);
options.Commands[HelpCommand.Name] = typeof(HelpCommand);
options.Commands[CreateCommand.Name] = typeof(CreateCommand);
options.Commands[GenerateProxyCommand.Name] = typeof(GenerateProxyCommand);
});
}
}

4
aspnet-core/modules/cli/LINGYUN.Abp.Cli/LINGYUN/Abp/Cli/Commands/CreateCommand.cs

@ -19,6 +19,10 @@ namespace LINGYUN.Abp.Cli.Commands
{
public class CreateCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "create";
protected string CommandName => Name;
public class FindFile
{
public string Path { get; }

146
aspnet-core/modules/cli/LINGYUN.Abp.Cli/LINGYUN/Abp/Cli/Commands/GenerateProxyCommand.cs

@ -0,0 +1,146 @@
using LINGYUN.Abp.Cli.ServiceProxying;
using LINGYUN.Abp.Cli.ServiceProxying.CSharp;
using Microsoft.Extensions.DependencyInjection;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.Commands;
using Volo.Abp.DependencyInjection;
using VoloGenerateProxyArgs = Volo.Abp.Cli.ServiceProxying.GenerateProxyArgs;
namespace LINGYUN.Abp.Cli.Commands;
public class GenerateProxyCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "generate-proxy";
protected string CommandName => Name;
protected IServiceScopeFactory ServiceScopeFactory { get; }
public GenerateProxyCommand(
IServiceScopeFactory serviceScopeFactory)
{
ServiceScopeFactory = serviceScopeFactory;
}
public async Task ExecuteAsync(CommandLineArgs commandLineArgs)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var serviceProxyGenerator = scope.ServiceProvider.GetRequiredService<CSharpServiceProxyGenerator>();
await serviceProxyGenerator.GenerateProxyAsync(BuildArgs(commandLineArgs));
}
}
private VoloGenerateProxyArgs BuildArgs(CommandLineArgs commandLineArgs)
{
var provider = commandLineArgs.Options.GetOrNull(Options.Provider.Short, Options.Provider.Long);
var url = commandLineArgs.Options.GetOrNull(Options.Url.Short, Options.Url.Long);
var target = commandLineArgs.Options.GetOrNull(Options.Target.Long);
var module = commandLineArgs.Options.GetOrNull(Options.Module.Short, Options.Module.Long) ?? "app";
var output = commandLineArgs.Options.GetOrNull(Options.Output.Short, Options.Output.Long);
var apiName = commandLineArgs.Options.GetOrNull(Options.ApiName.Short, Options.ApiName.Long);
var source = commandLineArgs.Options.GetOrNull(Options.Source.Short, Options.Source.Long);
var workDirectory = commandLineArgs.Options.GetOrNull(Options.WorkDirectory.Short, Options.WorkDirectory.Long) ?? Directory.GetCurrentDirectory();
var folder = commandLineArgs.Options.GetOrNull(Options.Folder.Long);
return new GenerateProxyArgs(CommandName, workDirectory, module, url, output, target, apiName, source, folder, provider, commandLineArgs.Options);
}
public string GetUsageInfo()
{
var sb = new StringBuilder();
sb.AppendLine("");
sb.AppendLine("Usage:");
sb.AppendLine("");
sb.AppendLine($" labp {CommandName}");
sb.AppendLine("");
sb.AppendLine("Options:");
sb.AppendLine("");
sb.AppendLine("-m|--module <module-name> (default: 'app') The name of the backend module you wish to generate proxies for.");
sb.AppendLine("-wd|--working-directory <directory-path> Execution directory.");
sb.AppendLine("-u|--url <url> API definition URL from.");
sb.AppendLine("-p|--provider <client-proxy-provider> The client proxy provider(http, dapr).");
sb.AppendLine("See the documentation for more info: https://docs.abp.io/en/abp/latest/CLI");
sb.AppendLine("");
sb.AppendLine("Examples:");
sb.AppendLine("");
sb.AppendLine(" labp generate-proxy");
sb.AppendLine(" labp generate-proxy -p dapr");
sb.AppendLine(" labp generate-proxy -m identity -o Pages/Identity/client-proxies.js -url https://localhost:44302/");
sb.AppendLine(" labp generate-proxy --folder MyProxies/InnerFolder -url https://localhost:44302/");
return sb.ToString();
}
public string GetShortDescription()
{
return "Generates client service proxies and DTOs to consume HTTP APIs.";
}
public static class Options
{
public static class Provider
{
public const string Short = "p";
public const string Long = "provider";
}
public static class Module
{
public const string Short = "m";
public const string Long = "module";
}
public static class ApiName
{
public const string Short = "a";
public const string Long = "api-name";
}
public static class Source
{
public const string Short = "s";
public const string Long = "source";
}
public static class Output
{
public const string Short = "o";
public const string Long = "output";
}
public static class Target
{
public const string Long = "target";
}
public static class Prompt
{
public const string Short = "p";
public const string Long = "prompt";
}
public static class Folder
{
public const string Long = "folder";
}
public static class Url
{
public const string Short = "u";
public const string Long = "url";
}
public static class WorkDirectory
{
public const string Short = "wd";
public const string Long = "working-directory";
}
}
}

4
aspnet-core/modules/cli/LINGYUN.Abp.Cli/LINGYUN/Abp/Cli/Commands/HelpCommand.cs

@ -14,6 +14,10 @@ namespace LINGYUN.Abp.Cli.Commands
{
public class HelpCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "help";
protected string CommandName => Name;
public ILogger<HelpCommand> Logger { get; set; }
protected AbpCliOptions AbpCliOptions { get; }
protected IServiceScopeFactory ServiceScopeFactory { get; }

422
aspnet-core/modules/cli/LINGYUN.Abp.Cli/LINGYUN/Abp/Cli/ServiceProxying/CSharp/CSharpServiceProxyGenerator.cs

@ -0,0 +1,422 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp.Cli;
using Volo.Abp.Cli.Commands;
using Volo.Abp.Cli.Http;
using Volo.Abp.Cli.ServiceProxying;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Modeling;
using Volo.Abp.Json;
using VoloGenerateProxyArgs = Volo.Abp.Cli.ServiceProxying.GenerateProxyArgs;
namespace LINGYUN.Abp.Cli.ServiceProxying.CSharp;
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IServiceProxyGenerator), typeof(CSharpServiceProxyGenerator))]
public class CSharpServiceProxyGenerator : ServiceProxyGeneratorBase<CSharpServiceProxyGenerator>, ITransientDependency
{
public const string Name = "CSHARP";
private const string UsingPlaceholder = "<using placeholder>";
private const string MethodPlaceholder = "<method placeholder>";
private const string ClassName = "<className>";
private const string ServiceInterface = "<serviceInterface>";
private readonly static string[] ServicePostfixes = { "AppService", "ApplicationService" , "Service"};
private const string DefaultNamespace = "ClientProxies";
private const string Namespace = "<namespace>";
private const string DefaultProvider = "ClientProxyBase";
private const string Provider = "<provider>";
private const string AppServicePrefix = "Volo.Abp.Application.Services";
private readonly string _clientProxyGeneratedTemplate = "// This file is automatically generated by ABP framework to use MVC Controllers from CSharp" +
$"{Environment.NewLine}<using placeholder>" +
$"{Environment.NewLine}" +
$"{Environment.NewLine}// ReSharper disable once CheckNamespace" +
$"{Environment.NewLine}namespace <namespace>;" +
$"{Environment.NewLine}" +
$"{Environment.NewLine}[Dependency(ReplaceServices = true)]" +
$"{Environment.NewLine}[ExposeServices(typeof(<serviceInterface>), typeof(<className>))]" +
$"{Environment.NewLine}public partial class <className> : <provider><<serviceInterface>>, <serviceInterface>" +
$"{Environment.NewLine}{{" +
$"{Environment.NewLine} <method placeholder>" +
$"{Environment.NewLine}}}" +
$"{Environment.NewLine}";
private readonly string _clientProxyTemplate = "// This file is part of <className>, you can customize it here" +
$"{Environment.NewLine}// ReSharper disable once CheckNamespace" +
$"{Environment.NewLine}namespace <namespace>;" +
$"{Environment.NewLine}" +
$"{Environment.NewLine}public partial class <className>" +
$"{Environment.NewLine}{{" +
$"{Environment.NewLine}}}" +
$"{Environment.NewLine}";
private readonly List<string> _usingNamespaceList = new()
{
"using System;",
"using System.Threading.Tasks;",
"using Volo.Abp.Application.Dtos;",
"using Volo.Abp.Http.Client;",
"using Volo.Abp.Http.Modeling;",
"using Volo.Abp.DependencyInjection;",
"using Volo.Abp.Http.Client.ClientProxying;",
"using LINGYUN.Abp.Dapr;",
"using LINGYUN.Abp.Dapr.Client;",
"using LINGYUN.Abp.Dapr.Client.ClientProxying;"
};
public CSharpServiceProxyGenerator(
CliHttpClientFactory cliHttpClientFactory,
IJsonSerializer jsonSerializer) :
base(cliHttpClientFactory, jsonSerializer)
{
}
public async override Task GenerateProxyAsync(VoloGenerateProxyArgs args)
{
CheckWorkDirectory(args.WorkDirectory);
CheckFolder(args.Folder);
if (args.CommandName == RemoveProxyCommand.Name)
{
RemoveClientProxyFile(args);
return;
}
var applicationApiDescriptionModel = await GetApplicationApiDescriptionModelAsync(args);
foreach (var controller in applicationApiDescriptionModel.Modules.Values.SelectMany(x => x.Controllers))
{
if (ShouldGenerateProxy(controller.Value))
{
await GenerateClientProxyFileAsync(args, controller.Value);
}
}
await CreateGenerateProxyJsonFile(args, applicationApiDescriptionModel);
}
private async Task CreateGenerateProxyJsonFile(VoloGenerateProxyArgs args, ApplicationApiDescriptionModel applicationApiDescriptionModel)
{
var folder = args.Folder.IsNullOrWhiteSpace() ? DefaultNamespace : args.Folder;
var filePath = Path.Combine(args.WorkDirectory, folder, $"{args.Module}-generate-proxy.json");
using (var writer = new StreamWriter(filePath))
{
await writer.WriteAsync(JsonSerializer.Serialize(applicationApiDescriptionModel, indented: true));
}
}
private void RemoveClientProxyFile(VoloGenerateProxyArgs args)
{
var folder = args.Folder.IsNullOrWhiteSpace() ? DefaultNamespace : args.Folder;
var folderPath = Path.Combine(args.WorkDirectory, folder);
if (Directory.Exists(folderPath))
{
Directory.Delete(folderPath, true);
}
Logger.LogInformation($"Delete {GetLoggerOutputPath(folderPath, args.WorkDirectory)}");
}
private async Task GenerateClientProxyFileAsync(
VoloGenerateProxyArgs args,
ControllerApiDescriptionModel controllerApiDescription)
{
var folder = args.Folder.IsNullOrWhiteSpace() ? DefaultNamespace : args.Folder;
var appServiceTypeFullName = controllerApiDescription.Interfaces.Last().Type;
var appServiceTypeName = appServiceTypeFullName.Split('.').Last();
var clientProxyName = $"{controllerApiDescription.ControllerName}ClientProxy";
var clientProvider = args.As<GenerateProxyArgs>().Provider;
var rootNamespace = $"{GetTypeNamespace(controllerApiDescription.Type)}.{folder.Replace('/', '.')}";
var clientProxyBuilder = new StringBuilder(_clientProxyTemplate);
clientProxyBuilder.Replace(ClassName, clientProxyName);
clientProxyBuilder.Replace(Namespace, rootNamespace);
clientProxyBuilder.Replace(Provider, clientProvider);
var filePath = Path.Combine(args.WorkDirectory, folder, $"{clientProxyName}.cs");
Directory.CreateDirectory(Path.GetDirectoryName(filePath));
if (!File.Exists(filePath))
{
using (var writer = new StreamWriter(filePath))
{
await writer.WriteAsync(clientProxyBuilder.ToString());
}
Logger.LogInformation($"Create {GetLoggerOutputPath(filePath, args.WorkDirectory)}");
}
await GenerateClientProxyGeneratedFileAsync(
args,
controllerApiDescription,
clientProxyName,
appServiceTypeName,
appServiceTypeFullName,
rootNamespace,
folder);
}
private async Task GenerateClientProxyGeneratedFileAsync(
VoloGenerateProxyArgs args,
ControllerApiDescriptionModel controllerApiDescription,
string clientProxyName,
string appServiceTypeName,
string appServiceTypeFullName,
string rootNamespace,
string folder)
{
var clientProxyBuilder = new StringBuilder(_clientProxyGeneratedTemplate);
var usingNamespaceList = new List<string>(_usingNamespaceList)
{
$"using {GetTypeNamespace(appServiceTypeFullName)};"
};
clientProxyBuilder.Replace(ClassName, clientProxyName);
clientProxyBuilder.Replace(Namespace, rootNamespace);
clientProxyBuilder.Replace(ServiceInterface, appServiceTypeName);
foreach (var action in controllerApiDescription.Actions.Values)
{
if (!ShouldGenerateMethod(appServiceTypeFullName, action))
{
continue;
}
GenerateMethod(action, 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}{Environment.NewLine} {MethodPlaceholder}", string.Empty);
var filePath = Path.Combine(args.WorkDirectory, folder, $"{clientProxyName}.Generated.cs");
using (var writer = new StreamWriter(filePath))
{
await writer.WriteAsync(clientProxyBuilder.ToString());
Logger.LogInformation($"Create {GetLoggerOutputPath(filePath, args.WorkDirectory)}");
}
}
private void GenerateMethod(
ActionApiDescriptionModel action,
StringBuilder clientProxyBuilder,
List<string> usingNamespaceList)
{
var methodBuilder = new StringBuilder();
var returnTypeName = GetRealTypeName(action.ReturnValue.Type, usingNamespaceList);
if (!action.Name.EndsWith("Async"))
{
GenerateSynchronizationMethod(action, returnTypeName, methodBuilder, usingNamespaceList);
clientProxyBuilder.Replace(MethodPlaceholder, $"{methodBuilder}{Environment.NewLine} {MethodPlaceholder}");
return;
}
GenerateAsynchronousMethod(action, returnTypeName, methodBuilder, usingNamespaceList);
clientProxyBuilder.Replace(MethodPlaceholder, $"{methodBuilder}{Environment.NewLine} {MethodPlaceholder}");
}
private void GenerateSynchronizationMethod(ActionApiDescriptionModel action, string returnTypeName, StringBuilder methodBuilder, List<string> usingNamespaceList)
{
methodBuilder.AppendLine($"public virtual {returnTypeName} {action.Name}(<args>)");
foreach (var parameter in action.Parameters.GroupBy(x => x.Name).Select(x => x.First()))
{
methodBuilder.Replace("<args>", $"{GetRealTypeName(parameter.Type, usingNamespaceList)} {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 action,
string returnTypeName,
StringBuilder methodBuilder,
List<string> usingNamespaceList)
{
var returnSign = returnTypeName == "void" ? "Task" : $"Task<{returnTypeName}>";
methodBuilder.AppendLine($"public virtual async {returnSign} {action.Name}(<args>)");
foreach (var parameter in action.ParametersOnMethod)
{
methodBuilder.Replace("<args>", $"{GetRealTypeName(parameter.Type, usingNamespaceList)} {parameter.Name}, <args>");
}
methodBuilder.Replace("<args>", string.Empty);
methodBuilder.Replace(", )", ")");
methodBuilder.AppendLine(" {");
var argsTemplate = "new ClientProxyRequestTypeValue" +
$"{Environment.NewLine} {{<args>" +
$"{Environment.NewLine} }}";
var args = action.ParametersOnMethod.Any() ? argsTemplate : string.Empty;
if (returnTypeName == "void")
{
methodBuilder.AppendLine($" await RequestAsync(nameof({action.Name}), {args});");
}
else
{
methodBuilder.AppendLine($" return await RequestAsync<{returnTypeName}>(nameof({action.Name}), {args});");
}
foreach (var parameter in action.ParametersOnMethod)
{
methodBuilder.Replace("<args>", $"{Environment.NewLine} {{ typeof({GetRealTypeName(parameter.Type)}), {parameter.Name} }},<args>");
}
methodBuilder.Replace(",<args>", string.Empty);
methodBuilder.Replace(", )", ")");
methodBuilder.AppendLine(" }");
}
private bool ShouldGenerateProxy(ControllerApiDescriptionModel controllerApiDescription)
{
if (!controllerApiDescription.Interfaces.Any())
{
return false;
}
var serviceInterface = controllerApiDescription.Interfaces.Last();
return ServicePostfixes.Any(x => serviceInterface.Type.EndsWith(x));
}
private bool ShouldGenerateMethod(string appServiceTypeName, ActionApiDescriptionModel action)
{
return action.ImplementFrom.StartsWith(AppServicePrefix) || action.ImplementFrom.StartsWith(appServiceTypeName);
}
private string GetTypeNamespace(string typeFullName)
{
return typeFullName.Substring(0, typeFullName.LastIndexOf('.'));
}
private string GetRealTypeName(string typeName, List<string> usingNamespaceList = null)
{
var filter = new[] { "<", ",", ">" };
var stringBuilder = new StringBuilder();
var typeNames = typeName.Split('.');
if (typeNames.All(x => !filter.Any(x.Contains)))
{
if (usingNamespaceList != null)
{
AddUsingNamespace(usingNamespaceList, typeName);
}
return NormalizeTypeName(typeNames.Last());
}
var fullName = string.Empty;
foreach (var item in typeNames)
{
if (filter.Any(x => item.Contains(x)))
{
if (usingNamespaceList != null)
{
AddUsingNamespace(usingNamespaceList, $"{fullName}.{item}".TrimStart('.'));
}
fullName = string.Empty;
if (item.Contains('<') || item.Contains(','))
{
stringBuilder.Append(item.Substring(0, item.IndexOf(item.Contains('<') ? '<' : ',') + 1));
fullName = item.Substring(item.IndexOf(item.Contains('<') ? '<' : ',') + 1);
}
else
{
stringBuilder.Append(item);
}
}
else
{
fullName = $"{fullName}.{item}";
}
}
return stringBuilder.ToString();
}
private void AddUsingNamespace(List<string> usingNamespaceList, string typeName)
{
var rootNamespace = $"using {GetTypeNamespace(typeName)};";
if (usingNamespaceList.Contains(rootNamespace))
{
return;
}
usingNamespaceList.Add(rootNamespace);
}
private string NormalizeTypeName(string typeName)
{
var nullable = string.Empty;
if (typeName.EndsWith("?"))
{
typeName = typeName.TrimEnd('?');
nullable = "?";
}
typeName = typeName switch
{
"Void" => "void",
"Boolean" => "bool",
"String" => "string",
"Int32" => "int",
"Int64" => "long",
"Double" => "double",
"Object" => "object",
"Byte" => "byte",
"Char" => "char",
_ => typeName
};
return $"{typeName}{nullable}";
}
private void CheckWorkDirectory(string directory)
{
if (!Directory.Exists(directory))
{
throw new CliUsageException("Specified directory does not exist.");
}
var projectFiles = Directory.GetFiles(directory, "*.csproj");
if (!projectFiles.Any())
{
throw new CliUsageException("No project file(csproj) found in the directory.");
}
}
private void CheckFolder(string folder)
{
if (!folder.IsNullOrWhiteSpace() && Path.HasExtension(folder))
{
throw new CliUsageException("Option folder should be a directory.");
}
}
}

26
aspnet-core/modules/cli/LINGYUN.Abp.Cli/LINGYUN/Abp/Cli/ServiceProxying/GenerateProxyArgs.cs

@ -0,0 +1,26 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace LINGYUN.Abp.Cli.ServiceProxying;
public class GenerateProxyArgs : Volo.Abp.Cli.ServiceProxying.GenerateProxyArgs
{
public string Provider { get; }
public GenerateProxyArgs(
[NotNull] string commandName,
[NotNull] string workDirectory,
string module,
string url,
string output,
string target,
string apiName,
string source,
string folder,
string provider,
Dictionary<string, string> extraProperties = null)
: base(commandName, workDirectory, module, url, output, target, apiName, source, folder, extraProperties)
{
Provider = provider;
}
}
Loading…
Cancel
Save