From e949503b6bc3eb2737cba85b6db2efc49ae4d3e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E6=98=9F=E7=B9=81?= Date: Thu, 6 Aug 2026 14:36:36 +0800 Subject: [PATCH] feat: add base-url configuration --- .../GenerateOptions.cs | 2 +- .../Generation/ServiceAgentEmitter.cs | 31 ++++++++++++++++--- src/Cnblogs.Architecture.Tool/Program.cs | 12 +++++-- .../Cqrs/ServiceAgentEmitterTests.cs | 24 ++++++++++++-- 4 files changed, 59 insertions(+), 10 deletions(-) diff --git a/src/Cnblogs.Architecture.Tool/GenerateOptions.cs b/src/Cnblogs.Architecture.Tool/GenerateOptions.cs index 669c86b..d2b37d4 100644 --- a/src/Cnblogs.Architecture.Tool/GenerateOptions.cs +++ b/src/Cnblogs.Architecture.Tool/GenerateOptions.cs @@ -1 +1 @@ -internal sealed record GenerateOptions(string ApiProject, string Output, string Namespace, bool Clean); +internal sealed record GenerateOptions(string ApiProject, string Output, string Namespace, bool Clean, string? BaseUrl); diff --git a/src/Cnblogs.Architecture.Tool/Generation/ServiceAgentEmitter.cs b/src/Cnblogs.Architecture.Tool/Generation/ServiceAgentEmitter.cs index d21943b..707bb85 100644 --- a/src/Cnblogs.Architecture.Tool/Generation/ServiceAgentEmitter.cs +++ b/src/Cnblogs.Architecture.Tool/Generation/ServiceAgentEmitter.cs @@ -39,6 +39,13 @@ internal sealed class ServiceAgentEmitter /// public string ApiVersion { get; init; } = "1"; + /// + /// When set, the generated DI extensions bake this base URL into each AddXxxService call and the methods + /// take no baseUri argument. Supplied via the tool's --base-url option; null keeps the + /// parameterized form (the caller passes baseUri). + /// + public string? BaseUrl { get; init; } + /// Emit all source files for the manifest, using for the generated types. public List Emit(EndpointManifest manifest, string @namespace) { @@ -809,24 +816,40 @@ private static string InjectErrorType(string returnType, ManifestGroup group, Cl private string RenderExtensions(EndpointManifest manifest, string @namespace) { + // When --base-url was supplied, bake the URL into each call and drop the baseUri argument; otherwise keep the + // parameterized form so the caller supplies the base URI at registration time. + var bakeBaseUrl = BaseUrl is not null; + var signatureTail = bakeBaseUrl ? string.Empty : ", string baseUri"; + var callArg = bakeBaseUrl ? RenderStringLiteral(BaseUrl!) : "baseUri"; + var sb = new StringBuilder(); sb.Append(AutoGeneratedBanner); sb.Append("using Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent;\n"); sb.Append("using Microsoft.Extensions.DependencyInjection;\n\n"); sb.Append("namespace ").Append(@namespace).Append(";\n\n"); sb.Append("public static partial class ServiceAgentExtensions\n{\n"); - sb.Append( - " public static IServiceCollection AddServiceAgents(this IServiceCollection services, string baseUri)\n {\n"); + // One registration extension per group (e.g. AddCorpService), so callers can register each agent independently. foreach (var group in manifest.Groups) { + sb.Append(" public static IServiceCollection Add").Append(group.Name) + .Append("Service(this IServiceCollection services").Append(signatureTail).Append(")\n {\n"); sb.Append(" services.AddServiceAgent(baseUri);\n"); + .Append("Service>(").Append(callArg).Append(");\n"); + sb.Append(" return services;\n }\n"); } - sb.Append(" return services;\n }\n}\n"); + sb.Append("}\n"); return sb.ToString(); } + private static string RenderStringLiteral(string value) + { + // Escape for a regular C# string literal. URLs rarely contain quotes/backslashes, but guard against it so a + // value with such characters still compiles. + var escaped = value.Replace("\\", "\\\\").Replace("\"", "\\\""); + return "\"" + escaped + "\""; + } + /// /// Emit one POCO per payload contract: a mutable class mirroring the command's settable properties, so the /// generated client references this generated type instead of the command type's assembly. diff --git a/src/Cnblogs.Architecture.Tool/Program.cs b/src/Cnblogs.Architecture.Tool/Program.cs index dbe4aad..24f3f2e 100644 --- a/src/Cnblogs.Architecture.Tool/Program.cs +++ b/src/Cnblogs.Architecture.Tool/Program.cs @@ -50,7 +50,9 @@ static void PrintUsage() serviceagent Generate strongly-typed CQRS service agents. serviceagent generate: - dotnet cnb serviceagent generate --api-project --output --namespace [--clean] + dotnet cnb serviceagent generate --api-project --output --namespace [--base-url ] [--clean] + + --base-url Bake this base URL into the generated AddXxxService extensions (otherwise each takes a baseUri argument). Requires the API project to reference the Cnblogs.Architecture.ServiceAgent.Design package. """); @@ -60,6 +62,7 @@ Requires the API project to reference the Cnblogs.Architecture.ServiceAgent.Desi { string? apiProject = null; string? output = null; + string? baseUrl = null; var ns = "Generated.ServiceAgents"; var clean = false; for (var i = 0; i < args.Length; i++) @@ -75,6 +78,9 @@ Requires the API project to reference the Cnblogs.Architecture.ServiceAgent.Desi case "--namespace": ns = Next(args, ref i) ?? "Generated.ServiceAgents"; break; + case "--base-url": + baseUrl = Next(args, ref i); + break; case "--clean": clean = true; break; @@ -90,7 +96,7 @@ Requires the API project to reference the Cnblogs.Architecture.ServiceAgent.Desi return null; } - return new GenerateOptions(apiProject, output, ns, clean); + return new GenerateOptions(apiProject, output, ns, clean, baseUrl); } static string? Next(string[] args, ref int i) @@ -147,7 +153,7 @@ static async Task RunGenerateAsync(GenerateOptions options) CleanGeneratedFiles(options.Output); } - var emitter = new ServiceAgentEmitter(); + var emitter = new ServiceAgentEmitter { BaseUrl = options.BaseUrl }; var files = emitter.Emit(manifest, options.Namespace); foreach (var diagnostic in emitter.Diagnostics) { diff --git a/test/Cnblogs.Architecture.UnitTests/Cqrs/ServiceAgentEmitterTests.cs b/test/Cnblogs.Architecture.UnitTests/Cqrs/ServiceAgentEmitterTests.cs index 3c09a41..2dc060d 100644 --- a/test/Cnblogs.Architecture.UnitTests/Cqrs/ServiceAgentEmitterTests.cs +++ b/test/Cnblogs.Architecture.UnitTests/Cqrs/ServiceAgentEmitterTests.cs @@ -65,9 +65,14 @@ private static string EmitClass(params ManifestGroup[] groups) } private static string EmitExtensions(params ManifestGroup[] groups) + { + return EmitExtensions(new ServiceAgentEmitter(), groups); + } + + private static string EmitExtensions(ServiceAgentEmitter emitter, params ManifestGroup[] groups) { var manifest = new EndpointManifest { Groups = groups.ToList() }; - var files = new ServiceAgentEmitter().Emit(manifest, "Cnblogs.Vip.ServiceAgent"); + var files = emitter.Emit(manifest, "Cnblogs.Vip.ServiceAgent"); return files.First(f => f.IsExtensionsFile).Content; } @@ -212,9 +217,24 @@ public void Emit_Extensions_RegistersAllGroups() new ManifestGroup { Name = "Vip", ErrorType = Error("VipError"), Endpoints = [] }, new ManifestGroup { Name = "Store", ErrorType = Error("StoreError"), Endpoints = [] }); + // One AddXxxService method per group, each taking a baseUri argument (no --base-url supplied). + Assert.Contains("public static IServiceCollection AddVipService(this IServiceCollection services, string baseUri)", ext); + Assert.Contains("public static IServiceCollection AddStoreService(this IServiceCollection services, string baseUri)", ext); Assert.Contains("AddServiceAgent(baseUri)", ext); Assert.Contains("AddServiceAgent(baseUri)", ext); - Assert.Contains("public static IServiceCollection AddServiceAgents(this IServiceCollection services, string baseUri)", ext); + Assert.DoesNotContain("AddServiceAgents", ext); + } + + [Fact] + public void Emit_Extensions_BaseUrl_BakesUrlAndDropsParam() + { + var ext = EmitExtensions( + new ServiceAgentEmitter { BaseUrl = "http://corp_api" }, + new ManifestGroup { Name = "Corp", ErrorType = Error("CorpError"), Endpoints = [] }); + + Assert.Contains("public static IServiceCollection AddCorpService(this IServiceCollection services)", ext); + Assert.Contains("AddServiceAgent(\"http://corp_api\")", ext); + Assert.DoesNotContain("baseUri", ext); } [Fact]