Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Cnblogs.Architecture.Tool/GenerateOptions.cs
Original file line number Diff line number Diff line change
@@ -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);
31 changes: 27 additions & 4 deletions src/Cnblogs.Architecture.Tool/Generation/ServiceAgentEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ internal sealed class ServiceAgentEmitter
/// </summary>
public string ApiVersion { get; init; } = "1";

/// <summary>
/// When set, the generated DI extensions bake this base URL into each <c>AddXxxService</c> call and the methods
/// take no <c>baseUri</c> argument. Supplied via the tool's <c>--base-url</c> option; <c>null</c> keeps the
/// parameterized form (the caller passes <c>baseUri</c>).
/// </summary>
public string? BaseUrl { get; init; }

/// <summary>Emit all source files for the manifest, using <paramref name="namespace" /> for the generated types.</summary>
public List<EmittedFile> Emit(EndpointManifest manifest, string @namespace)
{
Expand Down Expand Up @@ -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<I").Append(group.Name).Append("Service, ").Append(group.Name)
.Append("Service>(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 + "\"";
}

/// <summary>
/// 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.
Expand Down
12 changes: 9 additions & 3 deletions src/Cnblogs.Architecture.Tool/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ static void PrintUsage()
serviceagent Generate strongly-typed CQRS service agents.

serviceagent generate:
dotnet cnb serviceagent generate --api-project <api-csproj-or-dir> --output <client-dir> --namespace <ns> [--clean]
dotnet cnb serviceagent generate --api-project <api-csproj-or-dir> --output <client-dir> --namespace <ns> [--base-url <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.
""");
Expand All @@ -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++)
Expand All @@ -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;
Expand All @@ -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)
Expand Down Expand Up @@ -147,7 +153,7 @@ static async Task<int> 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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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<IVipService, VipService>(baseUri)", ext);
Assert.Contains("AddServiceAgent<IStoreService, StoreService>(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<ICorpService, CorpService>(\"http://corp_api\")", ext);
Assert.DoesNotContain("baseUri", ext);
}

[Fact]
Expand Down
Loading