From cb7f9e859ff129a2aba4a615aac34879cac5f5d1 Mon Sep 17 00:00:00 2001 From: jolov Date: Wed, 1 Apr 2026 20:19:12 -0700 Subject: [PATCH 01/21] Add 'plugin' emitter option for seamless custom visitor registration Add a new 'plugin' option to the CSharp emitter that allows specifying a path to a generator plugin assembly (DLL) or directory directly in tspconfig.yaml. This eliminates the need to create a separate npm package.json and custom emitter-package.json for custom visitors. TypeScript side: - Add 'plugin' to CSharpEmitterOptions and schema - Resolve relative paths to absolute in emitCodeModel() - The option flows through Configuration.json automatically C# side: - GeneratorHandler.LoadGenerator() reads the 'plugin' path from Configuration.AdditionalConfigurationOptions - AddConfiguredPluginDlls() loads the assembly (single DLL or directory scan) into the MEF AggregateCatalog before composition - Discovered GeneratorPlugin implementations are applied via the existing MEF [ImportMany] Plugins pipeline Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../http-client-csharp/emitter/src/emitter.ts | 7 ++- .../http-client-csharp/emitter/src/options.ts | 9 +++ .../src/StartUp/GeneratorHandler.cs | 61 ++++++++++++++++++- 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/emitter/src/emitter.ts b/packages/http-client-csharp/emitter/src/emitter.ts index 936f79dd191..cd79dc51256 100644 --- a/packages/http-client-csharp/emitter/src/emitter.ts +++ b/packages/http-client-csharp/emitter/src/emitter.ts @@ -13,7 +13,7 @@ import { resolvePath, } from "@typespec/compiler"; import fs, { statSync } from "fs"; -import { dirname } from "path"; +import { dirname, resolve } from "path"; import { fileURLToPath } from "url"; import { writeCodeModel, writeConfiguration } from "./code-model-writer.js"; import { @@ -84,6 +84,11 @@ export async function emitCodeModel( const options = resolveOptions(context); const outputFolder = context.emitterOutputDir; + // Resolve plugin path to absolute if specified + if (options["plugin"]) { + options["plugin"] = resolve(outputFolder, options["plugin"]); + } + /* set the log level. */ const logger = new Logger(program, options.logLevel ?? LoggerLevel.INFO); diff --git a/packages/http-client-csharp/emitter/src/options.ts b/packages/http-client-csharp/emitter/src/options.ts index 9ae08884a9e..59b251d4365 100644 --- a/packages/http-client-csharp/emitter/src/options.ts +++ b/packages/http-client-csharp/emitter/src/options.ts @@ -18,6 +18,7 @@ export interface CSharpEmitterOptions { "disable-xml-docs"?: boolean; "generator-name"?: string; "emitter-extension-path"?: string; + plugin?: string; "sdk-context-options"?: CreateSdkContextOptions; "generate-protocol-methods"?: boolean; "generate-convenience-methods"?: boolean; @@ -113,6 +114,14 @@ export const CSharpEmitterOptionsSchema: JSONSchemaType = description: "Allows emitter authors to specify the path to a custom emitter package, allowing you to extend the emitter behavior. This should be set to `import.meta.url` if you are using a custom emitter.", }, + plugin: { + type: "string", + nullable: true, + description: + "Path to a generator plugin assembly (DLL) or a directory containing plugin assemblies. " + + "The plugin must contain a class that extends GeneratorPlugin. " + + "This eliminates the need for a separate npm package and emitter-package.json for custom visitors.", + }, license: { type: "object", additionalProperties: false, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs index 7c68b51533a..d19dc3feeff 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs @@ -24,9 +24,13 @@ public void LoadGenerator(CommandLineOptions options) AddPluginDlls(catalog); + // Load plugins specified via the 'plugin' configuration option + var configuration = Configuration.Load(options.OutputDirectory); + AddConfiguredPluginDlls(catalog, configuration); + using CompositionContainer container = new(catalog); - container.ComposeExportedValue(new GeneratorContext(Configuration.Load(options.OutputDirectory))); + container.ComposeExportedValue(new GeneratorContext(configuration)); container.ComposeParts(this); SelectGenerator(options); @@ -86,6 +90,61 @@ private static void AddPluginDlls(AggregateCatalog catalog) } } + private const string PluginOptionKey = "plugin"; + + /// + /// Loads plugin assemblies from a path specified via the 'plugin' configuration option. + /// Supports both a single DLL path and a directory containing plugin assemblies. + /// + private static void AddConfiguredPluginDlls(AggregateCatalog catalog, Configuration configuration) + { + if (!configuration.AdditionalConfigurationOptions.TryGetValue(PluginOptionKey, out var value)) + { + return; + } + + var pluginPath = value.ToString().Trim('"'); + if (string.IsNullOrEmpty(pluginPath)) + { + return; + } + + using var emitter = new Emitter(Console.OpenStandardOutput()); + + if (File.Exists(pluginPath) && pluginPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) + { + try + { + catalog.Catalogs.Add(new AssemblyCatalog(pluginPath)); + } + catch (Exception ex) + { + emitter.Info($"Warning: Failed to load plugin from {pluginPath}: {ex.Message}"); + } + return; + } + + if (Directory.Exists(pluginPath)) + { + foreach (var dll in Directory.EnumerateFiles(pluginPath, "*.dll")) + { + try + { + catalog.Catalogs.Add(new AssemblyCatalog(dll)); + } + catch + { + // Skip DLLs that can't be loaded as MEF catalogs (e.g. native DLLs) + } + } + return; + } + + throw new InvalidOperationException( + $"Plugin path '{pluginPath}' does not exist. " + + $"Specify a path to a DLL file or a directory containing plugin assemblies."); + } + internal static IList GetOrderedPluginDlls(string pluginDirectoryStart) { var dllPathsInOrder = new List(); From a2effc5ab4ff273c6fc2668fb01f2f2327c066ba Mon Sep 17 00:00:00 2001 From: jolov Date: Wed, 1 Apr 2026 20:23:26 -0700 Subject: [PATCH 02/21] Make plugin a top-level property of Configuration Move the 'plugin' option from AdditionalConfigurationOptions to a first-class PluginPath property on Configuration, consistent with how PackageName, DisableXmlDocs, and other known options are handled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Configuration.cs | 16 ++++++++++++++-- .../src/StartUp/GeneratorHandler.cs | 7 +------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Configuration.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Configuration.cs index 98169440b6f..7c82d758e5e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Configuration.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Configuration.cs @@ -35,7 +35,8 @@ public Configuration( string packageName, bool disableXmlDocs, UnreferencedTypesHandlingOption unreferencedTypesHandling, - LicenseInfo? licenseInfo) + LicenseInfo? licenseInfo, + string? pluginPath = null) { OutputDirectory = outputPath; AdditionalConfigurationOptions = additionalConfigurationOptions; @@ -43,6 +44,7 @@ public Configuration( DisableXmlDocs = disableXmlDocs; UnreferencedTypesHandling = unreferencedTypesHandling; LicenseInfo = licenseInfo; + PluginPath = pluginPath; } /// @@ -53,6 +55,7 @@ private static class Options public const string PackageName = "package-name"; public const string DisableXmlDocs = "disable-xml-docs"; public const string UnreferencedTypesHandling = "unreferenced-types-handling"; + public const string Plugin = "plugin"; } /// @@ -86,6 +89,13 @@ private static class Options public string PackageName { get; } + /// + /// Gets the path to a plugin assembly (DLL) or directory containing plugin assemblies. + /// When specified, the generator loads plugins from this path in addition to any + /// plugins discovered via node_modules. + /// + public string? PluginPath { get; } + /// /// True if a sample project should be generated. /// @@ -123,7 +133,8 @@ internal static Configuration Load(string outputPath, string? json = null) ReadRequiredStringOption(root, Options.PackageName), ReadOption(root, Options.DisableXmlDocs), ReadEnumOption(root, Options.UnreferencedTypesHandling), - ReadLicenseInfo(root)); + ReadLicenseInfo(root), + ReadStringOption(root, Options.Plugin)); } private static LicenseInfo? ReadLicenseInfo(JsonElement root) @@ -164,6 +175,7 @@ internal static Configuration Load(string outputPath, string? json = null) Options.PackageName, Options.DisableXmlDocs, Options.UnreferencedTypesHandling, + Options.Plugin, }; private static bool ReadOption(JsonElement root, string option) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs index d19dc3feeff..79a287a7757 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs @@ -98,12 +98,7 @@ private static void AddPluginDlls(AggregateCatalog catalog) /// private static void AddConfiguredPluginDlls(AggregateCatalog catalog, Configuration configuration) { - if (!configuration.AdditionalConfigurationOptions.TryGetValue(PluginOptionKey, out var value)) - { - return; - } - - var pluginPath = value.ToString().Trim('"'); + var pluginPath = configuration.PluginPath; if (string.IsNullOrEmpty(pluginPath)) { return; From 0a4bcaf6926c883e686ceefcda935dd3bacc9eb3 Mon Sep 17 00:00:00 2001 From: jolov Date: Wed, 1 Apr 2026 20:26:36 -0700 Subject: [PATCH 03/21] Change plugin option from single string to string array Support multiple plugin paths so users can load plugins from different locations independently. Usage: plugin: - path/to/PluginA.dll - path/to/plugin-b/dist Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../http-client-csharp/emitter/src/emitter.ts | 4 +- .../http-client-csharp/emitter/src/options.ts | 9 +-- .../src/Configuration.cs | 31 ++++++++-- .../src/StartUp/GeneratorHandler.cs | 56 +++++++++++-------- 4 files changed, 64 insertions(+), 36 deletions(-) diff --git a/packages/http-client-csharp/emitter/src/emitter.ts b/packages/http-client-csharp/emitter/src/emitter.ts index cd79dc51256..ced759694be 100644 --- a/packages/http-client-csharp/emitter/src/emitter.ts +++ b/packages/http-client-csharp/emitter/src/emitter.ts @@ -84,9 +84,9 @@ export async function emitCodeModel( const options = resolveOptions(context); const outputFolder = context.emitterOutputDir; - // Resolve plugin path to absolute if specified + // Resolve plugin paths to absolute if specified if (options["plugin"]) { - options["plugin"] = resolve(outputFolder, options["plugin"]); + options["plugin"] = options["plugin"].map((p) => resolve(outputFolder, p)); } /* set the log level. */ diff --git a/packages/http-client-csharp/emitter/src/options.ts b/packages/http-client-csharp/emitter/src/options.ts index 59b251d4365..5f766ea6be7 100644 --- a/packages/http-client-csharp/emitter/src/options.ts +++ b/packages/http-client-csharp/emitter/src/options.ts @@ -18,7 +18,7 @@ export interface CSharpEmitterOptions { "disable-xml-docs"?: boolean; "generator-name"?: string; "emitter-extension-path"?: string; - plugin?: string; + plugin?: string[]; "sdk-context-options"?: CreateSdkContextOptions; "generate-protocol-methods"?: boolean; "generate-convenience-methods"?: boolean; @@ -115,11 +115,12 @@ export const CSharpEmitterOptionsSchema: JSONSchemaType = "Allows emitter authors to specify the path to a custom emitter package, allowing you to extend the emitter behavior. This should be set to `import.meta.url` if you are using a custom emitter.", }, plugin: { - type: "string", + type: "array", + items: { type: "string" }, nullable: true, description: - "Path to a generator plugin assembly (DLL) or a directory containing plugin assemblies. " + - "The plugin must contain a class that extends GeneratorPlugin. " + + "Paths to generator plugin assemblies (DLLs) or directories containing plugin assemblies. " + + "Each plugin must contain a class that extends GeneratorPlugin. " + "This eliminates the need for a separate npm package and emitter-package.json for custom visitors.", }, license: { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Configuration.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Configuration.cs index 7c82d758e5e..ebcdb0eb2ad 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Configuration.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Configuration.cs @@ -36,7 +36,7 @@ public Configuration( bool disableXmlDocs, UnreferencedTypesHandlingOption unreferencedTypesHandling, LicenseInfo? licenseInfo, - string? pluginPath = null) + IReadOnlyList? pluginPaths = null) { OutputDirectory = outputPath; AdditionalConfigurationOptions = additionalConfigurationOptions; @@ -44,7 +44,7 @@ public Configuration( DisableXmlDocs = disableXmlDocs; UnreferencedTypesHandling = unreferencedTypesHandling; LicenseInfo = licenseInfo; - PluginPath = pluginPath; + PluginPaths = pluginPaths; } /// @@ -90,11 +90,11 @@ private static class Options public string PackageName { get; } /// - /// Gets the path to a plugin assembly (DLL) or directory containing plugin assemblies. - /// When specified, the generator loads plugins from this path in addition to any + /// Gets the paths to plugin assemblies (DLLs) or directories containing plugin assemblies. + /// When specified, the generator loads plugins from these paths in addition to any /// plugins discovered via node_modules. /// - public string? PluginPath { get; } + public IReadOnlyList? PluginPaths { get; } /// /// True if a sample project should be generated. @@ -134,7 +134,7 @@ internal static Configuration Load(string outputPath, string? json = null) ReadOption(root, Options.DisableXmlDocs), ReadEnumOption(root, Options.UnreferencedTypesHandling), ReadLicenseInfo(root), - ReadStringOption(root, Options.Plugin)); + ReadStringArrayOption(root, Options.Plugin)); } private static LicenseInfo? ReadLicenseInfo(JsonElement root) @@ -203,6 +203,25 @@ private static string ReadRequiredStringOption(JsonElement root, string option) return null; } + private static IReadOnlyList? ReadStringArrayOption(JsonElement root, string option) + { + if (root.TryGetProperty(option, out JsonElement value) && value.ValueKind == JsonValueKind.Array) + { + var list = new List(); + foreach (var item in value.EnumerateArray()) + { + var str = item.GetString(); + if (!string.IsNullOrEmpty(str)) + { + list.Add(str); + } + } + return list.Count > 0 ? list : null; + } + + return null; + } + /// /// Returns the default value for the given option. /// diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs index 79a287a7757..7be3c704f2e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs @@ -93,51 +93,59 @@ private static void AddPluginDlls(AggregateCatalog catalog) private const string PluginOptionKey = "plugin"; /// - /// Loads plugin assemblies from a path specified via the 'plugin' configuration option. - /// Supports both a single DLL path and a directory containing plugin assemblies. + /// Loads plugin assemblies from paths specified via the 'plugin' configuration option. + /// Supports both single DLL paths and directories containing plugin assemblies. /// private static void AddConfiguredPluginDlls(AggregateCatalog catalog, Configuration configuration) { - var pluginPath = configuration.PluginPath; - if (string.IsNullOrEmpty(pluginPath)) + var pluginPaths = configuration.PluginPaths; + if (pluginPaths == null || pluginPaths.Count == 0) { return; } using var emitter = new Emitter(Console.OpenStandardOutput()); - if (File.Exists(pluginPath) && pluginPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) + foreach (var pluginPath in pluginPaths) { - try + if (string.IsNullOrEmpty(pluginPath)) { - catalog.Catalogs.Add(new AssemblyCatalog(pluginPath)); + continue; } - catch (Exception ex) - { - emitter.Info($"Warning: Failed to load plugin from {pluginPath}: {ex.Message}"); - } - return; - } - if (Directory.Exists(pluginPath)) - { - foreach (var dll in Directory.EnumerateFiles(pluginPath, "*.dll")) + if (File.Exists(pluginPath) && pluginPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) { try { - catalog.Catalogs.Add(new AssemblyCatalog(dll)); + catalog.Catalogs.Add(new AssemblyCatalog(pluginPath)); } - catch + catch (Exception ex) { - // Skip DLLs that can't be loaded as MEF catalogs (e.g. native DLLs) + emitter.Info($"Warning: Failed to load plugin from {pluginPath}: {ex.Message}"); } + continue; } - return; - } - throw new InvalidOperationException( - $"Plugin path '{pluginPath}' does not exist. " + - $"Specify a path to a DLL file or a directory containing plugin assemblies."); + if (Directory.Exists(pluginPath)) + { + foreach (var dll in Directory.EnumerateFiles(pluginPath, "*.dll")) + { + try + { + catalog.Catalogs.Add(new AssemblyCatalog(dll)); + } + catch + { + // Skip DLLs that can't be loaded as MEF catalogs (e.g. native DLLs) + } + } + continue; + } + + throw new InvalidOperationException( + $"Plugin path '{pluginPath}' does not exist. " + + $"Specify a path to a DLL file or a directory containing plugin assemblies."); + } } internal static IList GetOrderedPluginDlls(string pluginDirectoryStart) From b8400aff8dd4e0bcf2f4c47f310cf8aec78c2e2b Mon Sep 17 00:00:00 2001 From: jolov Date: Wed, 1 Apr 2026 20:39:07 -0700 Subject: [PATCH 04/21] Rename option from 'plugin' to 'plugins' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/emitter/src/emitter.ts | 4 ++-- packages/http-client-csharp/emitter/src/options.ts | 4 ++-- .../Microsoft.TypeSpec.Generator/src/Configuration.cs | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/http-client-csharp/emitter/src/emitter.ts b/packages/http-client-csharp/emitter/src/emitter.ts index ced759694be..996038f9434 100644 --- a/packages/http-client-csharp/emitter/src/emitter.ts +++ b/packages/http-client-csharp/emitter/src/emitter.ts @@ -85,8 +85,8 @@ export async function emitCodeModel( const outputFolder = context.emitterOutputDir; // Resolve plugin paths to absolute if specified - if (options["plugin"]) { - options["plugin"] = options["plugin"].map((p) => resolve(outputFolder, p)); + if (options["plugins"]) { + options["plugins"] = options["plugins"].map((p) => resolve(outputFolder, p)); } /* set the log level. */ diff --git a/packages/http-client-csharp/emitter/src/options.ts b/packages/http-client-csharp/emitter/src/options.ts index 5f766ea6be7..1c8f9c9b3e9 100644 --- a/packages/http-client-csharp/emitter/src/options.ts +++ b/packages/http-client-csharp/emitter/src/options.ts @@ -18,7 +18,7 @@ export interface CSharpEmitterOptions { "disable-xml-docs"?: boolean; "generator-name"?: string; "emitter-extension-path"?: string; - plugin?: string[]; + plugins?: string[]; "sdk-context-options"?: CreateSdkContextOptions; "generate-protocol-methods"?: boolean; "generate-convenience-methods"?: boolean; @@ -114,7 +114,7 @@ export const CSharpEmitterOptionsSchema: JSONSchemaType = description: "Allows emitter authors to specify the path to a custom emitter package, allowing you to extend the emitter behavior. This should be set to `import.meta.url` if you are using a custom emitter.", }, - plugin: { + plugins: { type: "array", items: { type: "string" }, nullable: true, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Configuration.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Configuration.cs index ebcdb0eb2ad..ee171ab66c6 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Configuration.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Configuration.cs @@ -55,7 +55,7 @@ private static class Options public const string PackageName = "package-name"; public const string DisableXmlDocs = "disable-xml-docs"; public const string UnreferencedTypesHandling = "unreferenced-types-handling"; - public const string Plugin = "plugin"; + public const string Plugins = "plugins"; } /// @@ -134,7 +134,7 @@ internal static Configuration Load(string outputPath, string? json = null) ReadOption(root, Options.DisableXmlDocs), ReadEnumOption(root, Options.UnreferencedTypesHandling), ReadLicenseInfo(root), - ReadStringArrayOption(root, Options.Plugin)); + ReadStringArrayOption(root, Options.Plugins)); } private static LicenseInfo? ReadLicenseInfo(JsonElement root) @@ -175,7 +175,7 @@ internal static Configuration Load(string outputPath, string? json = null) Options.PackageName, Options.DisableXmlDocs, Options.UnreferencedTypesHandling, - Options.Plugin, + Options.Plugins, }; private static bool ReadOption(JsonElement root, string option) From cb57bda95b0998344eb9a41f698d14abf6519f07 Mon Sep 17 00:00:00 2001 From: jolov Date: Wed, 1 Apr 2026 20:41:27 -0700 Subject: [PATCH 05/21] Update comments to use 'plugins' and remove unused constant Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/StartUp/GeneratorHandler.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs index 7be3c704f2e..7b88db2432b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs @@ -24,7 +24,7 @@ public void LoadGenerator(CommandLineOptions options) AddPluginDlls(catalog); - // Load plugins specified via the 'plugin' configuration option + // Load plugins specified via the 'plugins' configuration option var configuration = Configuration.Load(options.OutputDirectory); AddConfiguredPluginDlls(catalog, configuration); @@ -90,10 +90,8 @@ private static void AddPluginDlls(AggregateCatalog catalog) } } - private const string PluginOptionKey = "plugin"; - /// - /// Loads plugin assemblies from paths specified via the 'plugin' configuration option. + /// Loads plugin assemblies from paths specified via the 'plugins' configuration option. /// Supports both single DLL paths and directories containing plugin assemblies. /// private static void AddConfiguredPluginDlls(AggregateCatalog catalog, Configuration configuration) From 7ddc04fab36aeec5b10eb967cb697af1b2eb5ac0 Mon Sep 17 00:00:00 2001 From: jolov Date: Wed, 1 Apr 2026 20:43:45 -0700 Subject: [PATCH 06/21] Add tests for plugins configuration option C# tests (ConfigurationTests): - PluginPaths parsed from JSON array - PluginPaths null when not in config - PluginPaths null when empty array - plugins excluded from AdditionalConfigurationOptions TypeScript tests (options.test.ts): - plugins array passed through to configuration - plugins not included when not set Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../emitter/test/Unit/options.test.ts | 23 ++++++++ .../test/ConfigurationTests.cs | 58 +++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/packages/http-client-csharp/emitter/test/Unit/options.test.ts b/packages/http-client-csharp/emitter/test/Unit/options.test.ts index 77b240ade30..2d82da0c25d 100644 --- a/packages/http-client-csharp/emitter/test/Unit/options.test.ts +++ b/packages/http-client-csharp/emitter/test/Unit/options.test.ts @@ -162,4 +162,27 @@ describe("Configuration tests", async () => { expect(config["generate-protocol-methods"]).toBeUndefined(); expect(config["generate-convenience-methods"]).toBeUndefined(); }); + + it("should pass plugins option to configuration", async () => { + const options: CSharpEmitterOptions = { + "package-name": "test-package", + plugins: ["/path/to/Plugin.dll", "/path/to/plugin-dir"], + }; + const context = createEmitterContext(program, options); + const sdkContext = await createCSharpSdkContext(context); + const config = createConfiguration(options, "namespace", sdkContext); + + expect(config["plugins"]).toEqual(["/path/to/Plugin.dll", "/path/to/plugin-dir"]); + }); + + it("should not include plugins in configuration when not set", async () => { + const options: CSharpEmitterOptions = { + "package-name": "test-package", + }; + const context = createEmitterContext(program, options); + const sdkContext = await createCSharpSdkContext(context); + const config = createConfiguration(options, "namespace", sdkContext); + + expect(config["plugins"]).toBeUndefined(); + }); }); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ConfigurationTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ConfigurationTests.cs index d0ca28deb43..9fef6b61194 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ConfigurationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/ConfigurationTests.cs @@ -253,6 +253,64 @@ public void LicenseInfoIsNullWhenNotInConfig() Assert.IsNull(licenseInfo); } + [Test] + public void PluginPaths_ParsedFromConfig() + { + var mockJson = @"{ + ""output-folder"": ""outputFolder"", + ""package-name"": ""libraryName"", + ""plugins"": [""/path/to/Plugin.dll"", ""/path/to/plugin-dir""] + }"; + + MockHelpers.LoadMockGenerator(configuration: mockJson); + var pluginPaths = CodeModelGenerator.Instance.Configuration.PluginPaths; + Assert.IsNotNull(pluginPaths); + Assert.AreEqual(2, pluginPaths!.Count); + Assert.AreEqual("/path/to/Plugin.dll", pluginPaths[0]); + Assert.AreEqual("/path/to/plugin-dir", pluginPaths[1]); + } + + [Test] + public void PluginPaths_NullWhenNotInConfig() + { + var mockJson = @"{ + ""output-folder"": ""outputFolder"", + ""package-name"": ""libraryName"" + }"; + + MockHelpers.LoadMockGenerator(configuration: mockJson); + var pluginPaths = CodeModelGenerator.Instance.Configuration.PluginPaths; + Assert.IsNull(pluginPaths); + } + + [Test] + public void PluginPaths_NullWhenEmptyArray() + { + var mockJson = @"{ + ""output-folder"": ""outputFolder"", + ""package-name"": ""libraryName"", + ""plugins"": [] + }"; + + MockHelpers.LoadMockGenerator(configuration: mockJson); + var pluginPaths = CodeModelGenerator.Instance.Configuration.PluginPaths; + Assert.IsNull(pluginPaths); + } + + [Test] + public void PluginPaths_NotInAdditionalConfigOptions() + { + var mockJson = @"{ + ""output-folder"": ""outputFolder"", + ""package-name"": ""libraryName"", + ""plugins"": [""/path/to/Plugin.dll""] + }"; + + MockHelpers.LoadMockGenerator(configuration: mockJson); + var additionalOptions = CodeModelGenerator.Instance.Configuration.AdditionalConfigurationOptions; + Assert.IsFalse(additionalOptions.ContainsKey("plugins")); + } + public static IEnumerable ParseConfigOutputFolderTestCases { get From c3d4f72713d85a8e360beccba929bf7df789826c Mon Sep 17 00:00:00 2001 From: jolov Date: Wed, 1 Apr 2026 20:52:38 -0700 Subject: [PATCH 07/21] Remove extraneous description sentence from plugins option Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/emitter/src/options.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/http-client-csharp/emitter/src/options.ts b/packages/http-client-csharp/emitter/src/options.ts index 1c8f9c9b3e9..b980b4121f8 100644 --- a/packages/http-client-csharp/emitter/src/options.ts +++ b/packages/http-client-csharp/emitter/src/options.ts @@ -120,8 +120,7 @@ export const CSharpEmitterOptionsSchema: JSONSchemaType = nullable: true, description: "Paths to generator plugin assemblies (DLLs) or directories containing plugin assemblies. " + - "Each plugin must contain a class that extends GeneratorPlugin. " + - "This eliminates the need for a separate npm package and emitter-package.json for custom visitors.", + "Each plugin must contain a class that extends GeneratorPlugin.", }, license: { type: "object", From 500980c3f78e65fede2ecab89860169be3662cbc Mon Sep 17 00:00:00 2001 From: jolov Date: Wed, 1 Apr 2026 20:57:50 -0700 Subject: [PATCH 08/21] Auto-build plugins from .csproj for both code paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both the node_modules plugin path and the 'plugins' config path now share a BuildPluginIfNeeded helper that searches for a .csproj (recursively) and runs dotnet build if found. This means plugin packages no longer need to ship pre-built DLLs — the generator builds them on demand. For node_modules plugins: if dist/ doesn't exist, falls back to searching for a .csproj in the package directory. For configured plugins: searches for a .csproj first, falls back to scanning for pre-built DLLs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/StartUp/GeneratorHandler.cs | 140 +++++++++++++++--- 1 file changed, 122 insertions(+), 18 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs index 7b88db2432b..1f6e9974e33 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -38,14 +39,56 @@ public void LoadGenerator(CommandLineOptions options) private static void AddPluginDlls(AggregateCatalog catalog) { - var dllPathsInOrder = GetOrderedPluginDlls(AppContext.BaseDirectory); - if (dllPathsInOrder.Count == 0) + string? rootDirectory = FindRootDirectory(AppContext.BaseDirectory); + if (rootDirectory == null) + { + return; + } + + var packagePath = Path.Combine(rootDirectory, "package.json"); + if (!File.Exists(packagePath)) { return; } + + using var doc = JsonDocument.Parse(File.ReadAllText(packagePath)); + if (!doc.RootElement.TryGetProperty("dependencies", out var deps)) + { + return; + } + // We need to construct the emitter independently as the CodeModelGenerator is not yet initialized. using var emitter = new Emitter(Console.OpenStandardOutput()); + var packageNamesInOrder = deps.EnumerateObject().Select(p => p.Name).ToList(); + var dllPathsInOrder = new List(); + + foreach (var package in packageNamesInOrder) + { + var packageDir = Path.Combine(rootDirectory, NodeModulesDir, package); + var packageDistPath = Path.Combine(packageDir, "dist"); + + if (Directory.Exists(packageDistPath)) + { + var dlls = Directory.EnumerateFiles(packageDistPath, "*.dll", SearchOption.AllDirectories); + dllPathsInOrder.AddRange(dlls); + } + else + { + // No pre-built DLLs — look for a .csproj to build + var builtDll = BuildPluginIfNeeded(packageDir, emitter); + if (builtDll != null) + { + dllPathsInOrder.Add(builtDll); + } + } + } + + if (dllPathsInOrder.Count == 0) + { + return; + } + var highestVersions = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var dllPath in dllPathsInOrder) { @@ -91,8 +134,8 @@ private static void AddPluginDlls(AggregateCatalog catalog) } /// - /// Loads plugin assemblies from paths specified via the 'plugins' configuration option. - /// Supports both single DLL paths and directories containing plugin assemblies. + /// Loads plugin assemblies from directory paths specified via the 'plugins' configuration option. + /// If a directory contains a .csproj file, the project is built first to produce the plugin assembly. /// private static void AddConfiguredPluginDlls(AggregateCatalog catalog, Configuration configuration) { @@ -111,21 +154,20 @@ private static void AddConfiguredPluginDlls(AggregateCatalog catalog, Configurat continue; } - if (File.Exists(pluginPath) && pluginPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) + if (!Directory.Exists(pluginPath)) { - try - { - catalog.Catalogs.Add(new AssemblyCatalog(pluginPath)); - } - catch (Exception ex) - { - emitter.Info($"Warning: Failed to load plugin from {pluginPath}: {ex.Message}"); - } - continue; + throw new InvalidOperationException( + $"Plugin path '{pluginPath}' is not a valid directory."); } - if (Directory.Exists(pluginPath)) + var builtDll = BuildPluginIfNeeded(pluginPath, emitter); + if (builtDll != null) + { + catalog.Catalogs.Add(new AssemblyCatalog(builtDll)); + } + else { + // No .csproj found — scan for pre-built DLLs foreach (var dll in Directory.EnumerateFiles(pluginPath, "*.dll")) { try @@ -137,13 +179,75 @@ private static void AddConfiguredPluginDlls(AggregateCatalog catalog, Configurat // Skip DLLs that can't be loaded as MEF catalogs (e.g. native DLLs) } } - continue; } + } + } + + /// + /// Looks for a .csproj in the given directory (recursively) and builds it if found. + /// Returns the path to the built DLL, or null if no .csproj was found. + /// + private static string? BuildPluginIfNeeded(string directory, Emitter emitter) + { + var csprojFiles = Directory.GetFiles(directory, "*.csproj", SearchOption.AllDirectories); + if (csprojFiles.Length == 0) + { + return null; + } + + return BuildPlugin(csprojFiles[0], emitter); + } + + /// + /// Builds a plugin .csproj and returns the path to the output DLL. + /// + private static string? BuildPlugin(string csprojPath, Emitter emitter) + { + emitter.Info($"Building plugin: {csprojPath}"); + + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = $"build \"{csprojPath}\" -c Release", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + process.Start(); + var stdout = process.StandardOutput.ReadToEnd(); + var stderr = process.StandardError.ReadToEnd(); + process.WaitForExit(); + + if (process.ExitCode != 0) + { throw new InvalidOperationException( - $"Plugin path '{pluginPath}' does not exist. " + - $"Specify a path to a DLL file or a directory containing plugin assemblies."); + $"Failed to build plugin '{csprojPath}'. Exit code: {process.ExitCode}\n{stderr}"); } + + // Parse the build output to find the produced DLL path. + // dotnet build outputs a line like: " MyPlugin -> /path/to/bin/Release/net10.0/MyPlugin.dll" + foreach (var line in stdout.Split('\n')) + { + var trimmed = line.Trim(); + var arrowIndex = trimmed.IndexOf(" -> ", StringComparison.Ordinal); + if (arrowIndex >= 0) + { + var dllPath = trimmed[(arrowIndex + 4)..].Trim(); + if (dllPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) && File.Exists(dllPath)) + { + emitter.Info($"Plugin built: {dllPath}"); + return dllPath; + } + } + } + + emitter.Info($"Warning: Build succeeded but could not determine output DLL path for '{csprojPath}'"); + return null; } internal static IList GetOrderedPluginDlls(string pluginDirectoryStart) From 878676aa76ac3a641613f0ea8a7f14748ac1ef64 Mon Sep 17 00:00:00 2001 From: jolov Date: Wed, 1 Apr 2026 21:01:27 -0700 Subject: [PATCH 09/21] Add tests for BuildPlugin - BuildPlugin_BuildsProjectAndReturnsDllPath: creates a minimal .csproj + .cs file in a temp directory, builds it, and verifies the output DLL path is returned and exists on disk. - BuildPlugin_ThrowsOnInvalidProject: verifies that an invalid .csproj throws InvalidOperationException. Also made BuildPlugin internal (was private) so it can be tested directly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/StartUp/GeneratorHandler.cs | 2 +- .../test/StartUp/GeneratorHandlerTests.cs | 62 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs index 1f6e9974e33..d6e4dcba883 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs @@ -201,7 +201,7 @@ private static void AddConfiguredPluginDlls(AggregateCatalog catalog, Configurat /// /// Builds a plugin .csproj and returns the path to the output DLL. /// - private static string? BuildPlugin(string csprojPath, Emitter emitter) + internal static string? BuildPlugin(string csprojPath, Emitter emitter) { emitter.Info($"Building plugin: {csprojPath}"); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs index 45f98adace2..3249c039b6c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs @@ -7,6 +7,7 @@ using System.ComponentModel.Composition.Hosting; using System.IO; using System.Linq; +using Microsoft.TypeSpec.Generator.EmitterRpc; using Moq; using NUnit.Framework; @@ -184,5 +185,66 @@ public void GetOrderedPluginDlls() File.Delete(Path.Combine(plugin2Directory, "Plugin2.dll")); } } + + [Test] + public void BuildPlugin_BuildsProjectAndReturnsDllPath() + { + var testDir = Path.Combine(Path.GetTempPath(), "typespec-test-plugin-" + Guid.NewGuid().ToString("N")[..8]); + try + { + Directory.CreateDirectory(testDir); + + // Create a minimal .csproj + File.WriteAllText(Path.Combine(testDir, "TestPlugin.csproj"), @" + + net10.0 + +"); + + // Create a minimal .cs file (doesn't need to be a real plugin for the build test) + File.WriteAllText(Path.Combine(testDir, "TestPlugin.cs"), @" +namespace TestPlugin +{ + public class Dummy { } +}"); + + using var emitter = new Emitter(Stream.Null); + var result = GeneratorHandler.BuildPlugin( + Path.Combine(testDir, "TestPlugin.csproj"), + emitter); + + Assert.IsNotNull(result, "BuildPlugin should return a DLL path"); + Assert.IsTrue(result!.EndsWith("TestPlugin.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(File.Exists(result), $"Built DLL should exist at {result}"); + } + finally + { + try { Directory.Delete(testDir, true); } catch { } + } + } + + [Test] + public void BuildPlugin_ThrowsOnInvalidProject() + { + var testDir = Path.Combine(Path.GetTempPath(), "typespec-test-plugin-" + Guid.NewGuid().ToString("N")[..8]); + try + { + Directory.CreateDirectory(testDir); + + // Create an invalid .csproj + File.WriteAllText(Path.Combine(testDir, "Bad.csproj"), "not valid xml"); + + using var emitter = new Emitter(Stream.Null); + + Assert.Throws(() => + GeneratorHandler.BuildPlugin( + Path.Combine(testDir, "Bad.csproj"), + emitter)); + } + finally + { + try { Directory.Delete(testDir, true); } catch { } + } + } } } From db41e4328369d6e8fab4a911b2d37a11cdaa19be Mon Sep 17 00:00:00 2001 From: jolov Date: Wed, 1 Apr 2026 21:03:19 -0700 Subject: [PATCH 10/21] Add more tests for plugin build and discovery - BuildPluginIfNeeded_ReturnsNullWhenNoCsproj: directory with no .csproj returns null - BuildPluginIfNeeded_FindsCsprojInSubdirectory: .csproj nested in a subdirectory is found and built - BuildPlugin_OutputDllContainsCompiledType: verifies the built DLL actually contains the compiled types via reflection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/StartUp/GeneratorHandler.cs | 2 +- .../test/StartUp/GeneratorHandlerTests.cs | 83 +++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs index d6e4dcba883..f5aa297eca6 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs @@ -187,7 +187,7 @@ private static void AddConfiguredPluginDlls(AggregateCatalog catalog, Configurat /// Looks for a .csproj in the given directory (recursively) and builds it if found. /// Returns the path to the built DLL, or null if no .csproj was found. /// - private static string? BuildPluginIfNeeded(string directory, Emitter emitter) + internal static string? BuildPluginIfNeeded(string directory, Emitter emitter) { var csprojFiles = Directory.GetFiles(directory, "*.csproj", SearchOption.AllDirectories); if (csprojFiles.Length == 0) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs index 3249c039b6c..9e56a5cc490 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs @@ -246,5 +246,88 @@ public void BuildPlugin_ThrowsOnInvalidProject() try { Directory.Delete(testDir, true); } catch { } } } + + [Test] + public void BuildPluginIfNeeded_ReturnsNullWhenNoCsproj() + { + var testDir = Path.Combine(Path.GetTempPath(), "typespec-test-plugin-" + Guid.NewGuid().ToString("N")[..8]); + try + { + Directory.CreateDirectory(testDir); + File.WriteAllText(Path.Combine(testDir, "readme.txt"), "no csproj here"); + + using var emitter = new Emitter(Stream.Null); + var result = GeneratorHandler.BuildPluginIfNeeded(testDir, emitter); + + Assert.IsNull(result); + } + finally + { + try { Directory.Delete(testDir, true); } catch { } + } + } + + [Test] + public void BuildPluginIfNeeded_FindsCsprojInSubdirectory() + { + var testDir = Path.Combine(Path.GetTempPath(), "typespec-test-plugin-" + Guid.NewGuid().ToString("N")[..8]); + var srcDir = Path.Combine(testDir, "src"); + try + { + Directory.CreateDirectory(srcDir); + + File.WriteAllText(Path.Combine(srcDir, "SubPlugin.csproj"), @" + + net10.0 + +"); + + File.WriteAllText(Path.Combine(srcDir, "SubPlugin.cs"), @" +namespace SubPlugin { public class Dummy { } }"); + + using var emitter = new Emitter(Stream.Null); + var result = GeneratorHandler.BuildPluginIfNeeded(testDir, emitter); + + Assert.IsNotNull(result); + Assert.IsTrue(result!.EndsWith("SubPlugin.dll", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(File.Exists(result)); + } + finally + { + try { Directory.Delete(testDir, true); } catch { } + } + } + + [Test] + public void BuildPlugin_OutputDllContainsCompiledType() + { + var testDir = Path.Combine(Path.GetTempPath(), "typespec-test-plugin-" + Guid.NewGuid().ToString("N")[..8]); + try + { + Directory.CreateDirectory(testDir); + + File.WriteAllText(Path.Combine(testDir, "TypedPlugin.csproj"), @" + + net10.0 + +"); + + File.WriteAllText(Path.Combine(testDir, "MyType.cs"), @" +namespace TypedPlugin { public class MyType { public int Value => 42; } }"); + + using var emitter = new Emitter(Stream.Null); + var dllPath = GeneratorHandler.BuildPlugin( + Path.Combine(testDir, "TypedPlugin.csproj"), emitter); + + Assert.IsNotNull(dllPath); + var asm = System.Reflection.Assembly.LoadFrom(dllPath!); + var type = asm.GetType("TypedPlugin.MyType"); + Assert.IsNotNull(type, "Compiled assembly should contain MyType"); + } + finally + { + try { Directory.Delete(testDir, true); } catch { } + } + } } } From 57df004515ff5a8a0b31f206f0a6f82af25f4b54 Mon Sep 17 00:00:00 2001 From: jolov Date: Wed, 1 Apr 2026 21:05:26 -0700 Subject: [PATCH 11/21] Add tests for AddConfiguredPluginDlls - NoPluginPaths: null PluginPaths is a no-op (0 catalogs added) - InvalidDirectory: nonexistent path throws InvalidOperationException - DirectoryWithPreBuiltDlls: loads pre-built DLLs when no .csproj - DirectoryWithCsproj: auto-builds and loads the plugin - MultiplePluginPaths: handles mixed pre-built + .csproj paths Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/StartUp/GeneratorHandler.cs | 2 +- .../test/StartUp/GeneratorHandlerTests.cs | 148 ++++++++++++++++++ 2 files changed, 149 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs index f5aa297eca6..a15eb975699 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs @@ -137,7 +137,7 @@ private static void AddPluginDlls(AggregateCatalog catalog) /// Loads plugin assemblies from directory paths specified via the 'plugins' configuration option. /// If a directory contains a .csproj file, the project is built first to produce the plugin assembly. /// - private static void AddConfiguredPluginDlls(AggregateCatalog catalog, Configuration configuration) + internal static void AddConfiguredPluginDlls(AggregateCatalog catalog, Configuration configuration) { var pluginPaths = configuration.PluginPaths; if (pluginPaths == null || pluginPaths.Count == 0) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs index 9e56a5cc490..c55f38165c0 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs @@ -329,5 +329,153 @@ namespace TypedPlugin { public class MyType { public int Value => 42; } }"); try { Directory.Delete(testDir, true); } catch { } } } + + [Test] + public void AddConfiguredPluginDlls_NoPluginPaths_DoesNothing() + { + var config = new Configuration( + Path.GetTempPath(), + new Dictionary(), + "TestPackage", + false, + Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize, + null, + pluginPaths: null); + + using var catalog = new AggregateCatalog(); + GeneratorHandler.AddConfiguredPluginDlls(catalog, config); + + Assert.AreEqual(0, catalog.Catalogs.Count); + } + + [Test] + public void AddConfiguredPluginDlls_InvalidDirectory_Throws() + { + var config = new Configuration( + Path.GetTempPath(), + new Dictionary(), + "TestPackage", + false, + Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize, + null, + pluginPaths: ["/nonexistent/path"]); + + using var catalog = new AggregateCatalog(); + + Assert.Throws(() => + GeneratorHandler.AddConfiguredPluginDlls(catalog, config)); + } + + [Test] + public void AddConfiguredPluginDlls_DirectoryWithPreBuiltDlls_LoadsThem() + { + var testDir = Path.Combine(Path.GetTempPath(), "typespec-test-plugin-" + Guid.NewGuid().ToString("N")[..8]); + try + { + Directory.CreateDirectory(testDir); + + // Copy the test assembly as a pre-built plugin DLL + var testAssembly = typeof(GeneratorHandlerTests).Assembly.Location; + File.Copy(testAssembly, Path.Combine(testDir, "PreBuiltPlugin.dll")); + + var config = new Configuration( + Path.GetTempPath(), + new Dictionary(), + "TestPackage", + false, + Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize, + null, + pluginPaths: [testDir]); + + using var catalog = new AggregateCatalog(); + GeneratorHandler.AddConfiguredPluginDlls(catalog, config); + + Assert.IsTrue(catalog.Catalogs.Count > 0, "Should have loaded at least one catalog"); + } + finally + { + try { Directory.Delete(testDir, true); } catch { } + } + } + + [Test] + public void AddConfiguredPluginDlls_DirectoryWithCsproj_BuildsAndLoads() + { + var testDir = Path.Combine(Path.GetTempPath(), "typespec-test-plugin-" + Guid.NewGuid().ToString("N")[..8]); + try + { + Directory.CreateDirectory(testDir); + + File.WriteAllText(Path.Combine(testDir, "AutoBuildPlugin.csproj"), @" + + net10.0 + +"); + + File.WriteAllText(Path.Combine(testDir, "Plugin.cs"), @" +namespace AutoBuildPlugin { public class Dummy { } }"); + + var config = new Configuration( + Path.GetTempPath(), + new Dictionary(), + "TestPackage", + false, + Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize, + null, + pluginPaths: [testDir]); + + using var catalog = new AggregateCatalog(); + GeneratorHandler.AddConfiguredPluginDlls(catalog, config); + + Assert.IsTrue(catalog.Catalogs.Count > 0, "Should have built and loaded the plugin"); + } + finally + { + try { Directory.Delete(testDir, true); } catch { } + } + } + + [Test] + public void AddConfiguredPluginDlls_MultiplePluginPaths() + { + var testDir1 = Path.Combine(Path.GetTempPath(), "typespec-test-plugin-" + Guid.NewGuid().ToString("N")[..8]); + var testDir2 = Path.Combine(Path.GetTempPath(), "typespec-test-plugin-" + Guid.NewGuid().ToString("N")[..8]); + try + { + // Plugin 1: pre-built DLL + Directory.CreateDirectory(testDir1); + var testAssembly = typeof(GeneratorHandlerTests).Assembly.Location; + File.Copy(testAssembly, Path.Combine(testDir1, "Plugin1.dll")); + + // Plugin 2: .csproj to build + Directory.CreateDirectory(testDir2); + File.WriteAllText(Path.Combine(testDir2, "Plugin2.csproj"), @" + + net10.0 + +"); + File.WriteAllText(Path.Combine(testDir2, "Plugin2.cs"), @" +namespace Plugin2 { public class Dummy { } }"); + + var config = new Configuration( + Path.GetTempPath(), + new Dictionary(), + "TestPackage", + false, + Configuration.UnreferencedTypesHandlingOption.RemoveOrInternalize, + null, + pluginPaths: [testDir1, testDir2]); + + using var catalog = new AggregateCatalog(); + GeneratorHandler.AddConfiguredPluginDlls(catalog, config); + + Assert.IsTrue(catalog.Catalogs.Count >= 2, "Should have loaded catalogs from both plugin paths"); + } + finally + { + try { Directory.Delete(testDir1, true); } catch { } + try { Directory.Delete(testDir2, true); } catch { } + } + } } } From d6605c09ae404749de9d5e1c8ecb5c7283d22479 Mon Sep 17 00:00:00 2001 From: jolov Date: Thu, 2 Apr 2026 11:29:05 -0700 Subject: [PATCH 12/21] Regenerate emitter docs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../http-client-csharp/docs/decorators.md | 39 +++++++++ packages/http-client-csharp/docs/emitter.md | 87 +++++++++++++++++++ packages/http-client-csharp/docs/index.mdx | 33 +++++++ packages/http-client-csharp/readme.md | 55 ++---------- 4 files changed, 165 insertions(+), 49 deletions(-) create mode 100644 packages/http-client-csharp/docs/decorators.md create mode 100644 packages/http-client-csharp/docs/emitter.md create mode 100644 packages/http-client-csharp/docs/index.mdx diff --git a/packages/http-client-csharp/docs/decorators.md b/packages/http-client-csharp/docs/decorators.md new file mode 100644 index 00000000000..892297a2833 --- /dev/null +++ b/packages/http-client-csharp/docs/decorators.md @@ -0,0 +1,39 @@ +--- +title: "Decorators" +description: "Decorators exported by @typespec/http-client-csharp" +toc_min_heading_level: 2 +toc_max_heading_level: 3 +--- +## TypeSpec.HttpClient.CSharp +### `@dynamicModel` {#@TypeSpec.HttpClient.CSharp.dynamicModel} + +Marks a model or namespace as dynamic, indicating it should generate dynamic model code. +Can be applied to Model or Namespace types. +```typespec +@TypeSpec.HttpClient.CSharp.dynamicModel +``` + +#### Target + +`Model | Namespace` + +#### Parameters +None + +#### Examples + +```tsp +@dynamicModel +model Pet { + name: string; + kind: string; +} + +@dynamicModel +namespace PetStore { + model Dog extends Pet { + breed: string; + } +} +``` + diff --git a/packages/http-client-csharp/docs/emitter.md b/packages/http-client-csharp/docs/emitter.md new file mode 100644 index 00000000000..6f608380235 --- /dev/null +++ b/packages/http-client-csharp/docs/emitter.md @@ -0,0 +1,87 @@ +--- +title: "Emitter usage" +--- +## Emitter usage +1. Via the command line +```bash +tsp compile . --emit=@typespec/http-client-csharp +``` +2. Via the config +```yaml +emit: + - "@typespec/http-client-csharp" +``` +The config can be extended with options as follows: +```yaml +emit: + - "@typespec/http-client-csharp" +options: + "@typespec/http-client-csharp": + option: value +``` +## Emitter options +### `emitter-output-dir` +**Type:** `absolutePath` + +Defines the emitter output directory. Defaults to `{output-dir}/@typespec/http-client-csharp` +See [Configuring output directory for more info](https://typespec.io/docs/handbook/configuration/configuration/#configuring-output-directory) +### `api-version` +**Type:** `string` + +For TypeSpec files using the [`@versioned`](https://typespec.io/docs/libraries/versioning/reference/decorators/#@TypeSpec.Versioning.versioned) decorator, set this option to the version that should be used to generate against. +### `generate-protocol-methods` +**Type:** `boolean` + +Set to `false` to skip generation of protocol methods. The default value is `true`. +### `generate-convenience-methods` +**Type:** `boolean` + +Set to `false` to skip generation of convenience methods. The default value is `true`. +### `unreferenced-types-handling` +**Type:** `"removeOrInternalize" | "internalize" | "keepAll"` + +Defines the strategy on how to handle unreferenced types. The default value is `removeOrInternalize`. +### `new-project` +**Type:** `boolean` + +Set to `true` to overwrite the csproj if it already exists. The default value is `false`. +### `save-inputs` +**Type:** `boolean` + +Set to `true` to save the `tspCodeModel.json` and `Configuration.json` files that are emitted and used as inputs to the generator. The default value is `false`. +### `package-name` +**Type:** `string` + +Define the package name. If not specified, the first namespace defined in the TypeSpec is used as the package name. +### `debug` +**Type:** `boolean` + +Set to `true` to automatically attempt to attach to a debugger when executing the C# generator. The default value is `false`. +### `logLevel` +**Type:** `"info" | "debug" | "verbose"` + +Set the log level for which to collect traces. The default value is `info`. +### `disable-xml-docs` +**Type:** `boolean` + +Set to `true` to disable XML documentation generation. The default value is `false`. +### `generator-name` +**Type:** `string` + +The name of the generator. By default this is set to `ScmCodeModelGenerator`. Generator authors can set this to the name of a generator that inherits from `ScmCodeModelGenerator`. +### `emitter-extension-path` +**Type:** `string` + +Allows emitter authors to specify the path to a custom emitter package, allowing you to extend the emitter behavior. This should be set to `import.meta.url` if you are using a custom emitter. +### `plugins` +**Type:** `array` + +Paths to generator plugin assemblies (DLLs) or directories containing plugin assemblies. Each plugin must contain a class that extends GeneratorPlugin. +### `license` +**Type:** `object` + +License information for the generated client code. +### `sdk-context-options` +**Type:** `object` + +The SDK context options that implement the `CreateSdkContextOptions` interface from the [`@azure-tools/typespec-client-generator-core`](https://www.npmjs.com/package/@azure-tools/typespec-client-generator-core) package to be used by the CSharp emitter. \ No newline at end of file diff --git a/packages/http-client-csharp/docs/index.mdx b/packages/http-client-csharp/docs/index.mdx new file mode 100644 index 00000000000..98fd44e9a3c --- /dev/null +++ b/packages/http-client-csharp/docs/index.mdx @@ -0,0 +1,33 @@ +--- +title: Overview +sidebar_position: 0 +toc_min_heading_level: 2 +toc_max_heading_level: 3 +--- +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +TypeSpec library for emitting Http Client libraries for C#. +## Install + + + +```bash +npm install @typespec/http-client-csharp +``` + + + + +```bash +npm install --save-peer @typespec/http-client-csharp +``` + + + + +## Emitter usage +[See documentation](./emitter.md) +## TypeSpec.HttpClient +## TypeSpec.HttpClient.CSharp +### Decorators + - [`@dynamicModel`](./decorators.md#@TypeSpec.HttpClient.CSharp.dynamicModel) \ No newline at end of file diff --git a/packages/http-client-csharp/readme.md b/packages/http-client-csharp/readme.md index 30e8bea7724..ea2f80dc732 100644 --- a/packages/http-client-csharp/readme.md +++ b/packages/http-client-csharp/readme.md @@ -1,15 +1,10 @@ # @typespec/http-client-csharp - TypeSpec library for emitting Http Client libraries for C#. - ## Install - ```bash npm install @typespec/http-client-csharp ``` - ## Usage - ### Prerequisite - Install [Node.js](https://nodejs.org/download/) 20 or above. (Verify by running `node --version`) @@ -20,22 +15,16 @@ npm install @typespec/http-client-csharp For detailed instructions on how to customize the generated C# code, see the [Customization Guide](https://github.com/microsoft/typespec/blob/main/packages/http-client-csharp/.tspd/docs/customization.md). ## Emitter usage - 1. Via the command line - ```bash tsp compile . --emit=@typespec/http-client-csharp ``` - 2. Via the config - ```yaml emit: - - "@typespec/http-client-csharp" + - "@typespec/http-client-csharp" ``` - The config can be extended with options as follows: - ```yaml emit: - "@typespec/http-client-csharp" @@ -43,111 +32,79 @@ options: "@typespec/http-client-csharp": option: value ``` - ## Emitter options - ### `emitter-output-dir` - **Type:** `absolutePath` Defines the emitter output directory. Defaults to `{output-dir}/@typespec/http-client-csharp` See [Configuring output directory for more info](https://typespec.io/docs/handbook/configuration/configuration/#configuring-output-directory) - ### `api-version` - **Type:** `string` For TypeSpec files using the [`@versioned`](https://typespec.io/docs/libraries/versioning/reference/decorators/#@TypeSpec.Versioning.versioned) decorator, set this option to the version that should be used to generate against. - ### `generate-protocol-methods` - **Type:** `boolean` Set to `false` to skip generation of protocol methods. The default value is `true`. - ### `generate-convenience-methods` - **Type:** `boolean` Set to `false` to skip generation of convenience methods. The default value is `true`. - ### `unreferenced-types-handling` - **Type:** `"removeOrInternalize" | "internalize" | "keepAll"` Defines the strategy on how to handle unreferenced types. The default value is `removeOrInternalize`. - ### `new-project` - **Type:** `boolean` Set to `true` to overwrite the csproj if it already exists. The default value is `false`. - ### `save-inputs` - **Type:** `boolean` Set to `true` to save the `tspCodeModel.json` and `Configuration.json` files that are emitted and used as inputs to the generator. The default value is `false`. - ### `package-name` - **Type:** `string` Define the package name. If not specified, the first namespace defined in the TypeSpec is used as the package name. - ### `debug` - **Type:** `boolean` Set to `true` to automatically attempt to attach to a debugger when executing the C# generator. The default value is `false`. - ### `logLevel` - **Type:** `"info" | "debug" | "verbose"` Set the log level for which to collect traces. The default value is `info`. - ### `disable-xml-docs` - **Type:** `boolean` Set to `true` to disable XML documentation generation. The default value is `false`. - ### `generator-name` - **Type:** `string` The name of the generator. By default this is set to `ScmCodeModelGenerator`. Generator authors can set this to the name of a generator that inherits from `ScmCodeModelGenerator`. - ### `emitter-extension-path` - **Type:** `string` Allows emitter authors to specify the path to a custom emitter package, allowing you to extend the emitter behavior. This should be set to `import.meta.url` if you are using a custom emitter. +### `plugins` +**Type:** `array` +Paths to generator plugin assemblies (DLLs) or directories containing plugin assemblies. Each plugin must contain a class that extends GeneratorPlugin. ### `license` - **Type:** `object` License information for the generated client code. - ### `sdk-context-options` - **Type:** `object` The SDK context options that implement the `CreateSdkContextOptions` interface from the [`@azure-tools/typespec-client-generator-core`](https://www.npmjs.com/package/@azure-tools/typespec-client-generator-core) package to be used by the CSharp emitter. - ## Decorators - ### TypeSpec.HttpClient.CSharp - -- [`@dynamicModel`](#@dynamicmodel) - + - [`@dynamicModel`](#@dynamicmodel) #### `@dynamicModel` Marks a model or namespace as dynamic, indicating it should generate dynamic model code. Can be applied to Model or Namespace types. - ```typespec @TypeSpec.HttpClient.CSharp.dynamicModel ``` @@ -157,7 +114,6 @@ Can be applied to Model or Namespace types. `Model | Namespace` ##### Parameters - None ##### Examples @@ -176,3 +132,4 @@ namespace PetStore { } } ``` + From ee41b7598ec6a25b5ebf8365af0387bc6facb5cf Mon Sep 17 00:00:00 2001 From: jolov Date: Thu, 2 Apr 2026 11:30:30 -0700 Subject: [PATCH 13/21] Revert "Regenerate emitter docs" This reverts commit d6605c09ae404749de9d5e1c8ecb5c7283d22479. --- .../http-client-csharp/docs/decorators.md | 39 --------- packages/http-client-csharp/docs/emitter.md | 87 ------------------- packages/http-client-csharp/docs/index.mdx | 33 ------- packages/http-client-csharp/readme.md | 55 ++++++++++-- 4 files changed, 49 insertions(+), 165 deletions(-) delete mode 100644 packages/http-client-csharp/docs/decorators.md delete mode 100644 packages/http-client-csharp/docs/emitter.md delete mode 100644 packages/http-client-csharp/docs/index.mdx diff --git a/packages/http-client-csharp/docs/decorators.md b/packages/http-client-csharp/docs/decorators.md deleted file mode 100644 index 892297a2833..00000000000 --- a/packages/http-client-csharp/docs/decorators.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: "Decorators" -description: "Decorators exported by @typespec/http-client-csharp" -toc_min_heading_level: 2 -toc_max_heading_level: 3 ---- -## TypeSpec.HttpClient.CSharp -### `@dynamicModel` {#@TypeSpec.HttpClient.CSharp.dynamicModel} - -Marks a model or namespace as dynamic, indicating it should generate dynamic model code. -Can be applied to Model or Namespace types. -```typespec -@TypeSpec.HttpClient.CSharp.dynamicModel -``` - -#### Target - -`Model | Namespace` - -#### Parameters -None - -#### Examples - -```tsp -@dynamicModel -model Pet { - name: string; - kind: string; -} - -@dynamicModel -namespace PetStore { - model Dog extends Pet { - breed: string; - } -} -``` - diff --git a/packages/http-client-csharp/docs/emitter.md b/packages/http-client-csharp/docs/emitter.md deleted file mode 100644 index 6f608380235..00000000000 --- a/packages/http-client-csharp/docs/emitter.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -title: "Emitter usage" ---- -## Emitter usage -1. Via the command line -```bash -tsp compile . --emit=@typespec/http-client-csharp -``` -2. Via the config -```yaml -emit: - - "@typespec/http-client-csharp" -``` -The config can be extended with options as follows: -```yaml -emit: - - "@typespec/http-client-csharp" -options: - "@typespec/http-client-csharp": - option: value -``` -## Emitter options -### `emitter-output-dir` -**Type:** `absolutePath` - -Defines the emitter output directory. Defaults to `{output-dir}/@typespec/http-client-csharp` -See [Configuring output directory for more info](https://typespec.io/docs/handbook/configuration/configuration/#configuring-output-directory) -### `api-version` -**Type:** `string` - -For TypeSpec files using the [`@versioned`](https://typespec.io/docs/libraries/versioning/reference/decorators/#@TypeSpec.Versioning.versioned) decorator, set this option to the version that should be used to generate against. -### `generate-protocol-methods` -**Type:** `boolean` - -Set to `false` to skip generation of protocol methods. The default value is `true`. -### `generate-convenience-methods` -**Type:** `boolean` - -Set to `false` to skip generation of convenience methods. The default value is `true`. -### `unreferenced-types-handling` -**Type:** `"removeOrInternalize" | "internalize" | "keepAll"` - -Defines the strategy on how to handle unreferenced types. The default value is `removeOrInternalize`. -### `new-project` -**Type:** `boolean` - -Set to `true` to overwrite the csproj if it already exists. The default value is `false`. -### `save-inputs` -**Type:** `boolean` - -Set to `true` to save the `tspCodeModel.json` and `Configuration.json` files that are emitted and used as inputs to the generator. The default value is `false`. -### `package-name` -**Type:** `string` - -Define the package name. If not specified, the first namespace defined in the TypeSpec is used as the package name. -### `debug` -**Type:** `boolean` - -Set to `true` to automatically attempt to attach to a debugger when executing the C# generator. The default value is `false`. -### `logLevel` -**Type:** `"info" | "debug" | "verbose"` - -Set the log level for which to collect traces. The default value is `info`. -### `disable-xml-docs` -**Type:** `boolean` - -Set to `true` to disable XML documentation generation. The default value is `false`. -### `generator-name` -**Type:** `string` - -The name of the generator. By default this is set to `ScmCodeModelGenerator`. Generator authors can set this to the name of a generator that inherits from `ScmCodeModelGenerator`. -### `emitter-extension-path` -**Type:** `string` - -Allows emitter authors to specify the path to a custom emitter package, allowing you to extend the emitter behavior. This should be set to `import.meta.url` if you are using a custom emitter. -### `plugins` -**Type:** `array` - -Paths to generator plugin assemblies (DLLs) or directories containing plugin assemblies. Each plugin must contain a class that extends GeneratorPlugin. -### `license` -**Type:** `object` - -License information for the generated client code. -### `sdk-context-options` -**Type:** `object` - -The SDK context options that implement the `CreateSdkContextOptions` interface from the [`@azure-tools/typespec-client-generator-core`](https://www.npmjs.com/package/@azure-tools/typespec-client-generator-core) package to be used by the CSharp emitter. \ No newline at end of file diff --git a/packages/http-client-csharp/docs/index.mdx b/packages/http-client-csharp/docs/index.mdx deleted file mode 100644 index 98fd44e9a3c..00000000000 --- a/packages/http-client-csharp/docs/index.mdx +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Overview -sidebar_position: 0 -toc_min_heading_level: 2 -toc_max_heading_level: 3 ---- -import { Tabs, TabItem } from '@astrojs/starlight/components'; - -TypeSpec library for emitting Http Client libraries for C#. -## Install - - - -```bash -npm install @typespec/http-client-csharp -``` - - - - -```bash -npm install --save-peer @typespec/http-client-csharp -``` - - - - -## Emitter usage -[See documentation](./emitter.md) -## TypeSpec.HttpClient -## TypeSpec.HttpClient.CSharp -### Decorators - - [`@dynamicModel`](./decorators.md#@TypeSpec.HttpClient.CSharp.dynamicModel) \ No newline at end of file diff --git a/packages/http-client-csharp/readme.md b/packages/http-client-csharp/readme.md index ea2f80dc732..30e8bea7724 100644 --- a/packages/http-client-csharp/readme.md +++ b/packages/http-client-csharp/readme.md @@ -1,10 +1,15 @@ # @typespec/http-client-csharp + TypeSpec library for emitting Http Client libraries for C#. + ## Install + ```bash npm install @typespec/http-client-csharp ``` + ## Usage + ### Prerequisite - Install [Node.js](https://nodejs.org/download/) 20 or above. (Verify by running `node --version`) @@ -15,16 +20,22 @@ npm install @typespec/http-client-csharp For detailed instructions on how to customize the generated C# code, see the [Customization Guide](https://github.com/microsoft/typespec/blob/main/packages/http-client-csharp/.tspd/docs/customization.md). ## Emitter usage + 1. Via the command line + ```bash tsp compile . --emit=@typespec/http-client-csharp ``` + 2. Via the config + ```yaml emit: - - "@typespec/http-client-csharp" + - "@typespec/http-client-csharp" ``` + The config can be extended with options as follows: + ```yaml emit: - "@typespec/http-client-csharp" @@ -32,79 +43,111 @@ options: "@typespec/http-client-csharp": option: value ``` + ## Emitter options + ### `emitter-output-dir` + **Type:** `absolutePath` Defines the emitter output directory. Defaults to `{output-dir}/@typespec/http-client-csharp` See [Configuring output directory for more info](https://typespec.io/docs/handbook/configuration/configuration/#configuring-output-directory) + ### `api-version` + **Type:** `string` For TypeSpec files using the [`@versioned`](https://typespec.io/docs/libraries/versioning/reference/decorators/#@TypeSpec.Versioning.versioned) decorator, set this option to the version that should be used to generate against. + ### `generate-protocol-methods` + **Type:** `boolean` Set to `false` to skip generation of protocol methods. The default value is `true`. + ### `generate-convenience-methods` + **Type:** `boolean` Set to `false` to skip generation of convenience methods. The default value is `true`. + ### `unreferenced-types-handling` + **Type:** `"removeOrInternalize" | "internalize" | "keepAll"` Defines the strategy on how to handle unreferenced types. The default value is `removeOrInternalize`. + ### `new-project` + **Type:** `boolean` Set to `true` to overwrite the csproj if it already exists. The default value is `false`. + ### `save-inputs` + **Type:** `boolean` Set to `true` to save the `tspCodeModel.json` and `Configuration.json` files that are emitted and used as inputs to the generator. The default value is `false`. + ### `package-name` + **Type:** `string` Define the package name. If not specified, the first namespace defined in the TypeSpec is used as the package name. + ### `debug` + **Type:** `boolean` Set to `true` to automatically attempt to attach to a debugger when executing the C# generator. The default value is `false`. + ### `logLevel` + **Type:** `"info" | "debug" | "verbose"` Set the log level for which to collect traces. The default value is `info`. + ### `disable-xml-docs` + **Type:** `boolean` Set to `true` to disable XML documentation generation. The default value is `false`. + ### `generator-name` + **Type:** `string` The name of the generator. By default this is set to `ScmCodeModelGenerator`. Generator authors can set this to the name of a generator that inherits from `ScmCodeModelGenerator`. + ### `emitter-extension-path` + **Type:** `string` Allows emitter authors to specify the path to a custom emitter package, allowing you to extend the emitter behavior. This should be set to `import.meta.url` if you are using a custom emitter. -### `plugins` -**Type:** `array` -Paths to generator plugin assemblies (DLLs) or directories containing plugin assemblies. Each plugin must contain a class that extends GeneratorPlugin. ### `license` + **Type:** `object` License information for the generated client code. + ### `sdk-context-options` + **Type:** `object` The SDK context options that implement the `CreateSdkContextOptions` interface from the [`@azure-tools/typespec-client-generator-core`](https://www.npmjs.com/package/@azure-tools/typespec-client-generator-core) package to be used by the CSharp emitter. + ## Decorators + ### TypeSpec.HttpClient.CSharp - - [`@dynamicModel`](#@dynamicmodel) + +- [`@dynamicModel`](#@dynamicmodel) + #### `@dynamicModel` Marks a model or namespace as dynamic, indicating it should generate dynamic model code. Can be applied to Model or Namespace types. + ```typespec @TypeSpec.HttpClient.CSharp.dynamicModel ``` @@ -114,6 +157,7 @@ Can be applied to Model or Namespace types. `Model | Namespace` ##### Parameters + None ##### Examples @@ -132,4 +176,3 @@ namespace PetStore { } } ``` - From 2be714419452f07da10606e3d740336a6fd832ae Mon Sep 17 00:00:00 2001 From: jolov Date: Thu, 2 Apr 2026 11:31:11 -0700 Subject: [PATCH 14/21] Regenerate readme.md docs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/readme.md | 55 +++------------------------ 1 file changed, 6 insertions(+), 49 deletions(-) diff --git a/packages/http-client-csharp/readme.md b/packages/http-client-csharp/readme.md index 30e8bea7724..ea2f80dc732 100644 --- a/packages/http-client-csharp/readme.md +++ b/packages/http-client-csharp/readme.md @@ -1,15 +1,10 @@ # @typespec/http-client-csharp - TypeSpec library for emitting Http Client libraries for C#. - ## Install - ```bash npm install @typespec/http-client-csharp ``` - ## Usage - ### Prerequisite - Install [Node.js](https://nodejs.org/download/) 20 or above. (Verify by running `node --version`) @@ -20,22 +15,16 @@ npm install @typespec/http-client-csharp For detailed instructions on how to customize the generated C# code, see the [Customization Guide](https://github.com/microsoft/typespec/blob/main/packages/http-client-csharp/.tspd/docs/customization.md). ## Emitter usage - 1. Via the command line - ```bash tsp compile . --emit=@typespec/http-client-csharp ``` - 2. Via the config - ```yaml emit: - - "@typespec/http-client-csharp" + - "@typespec/http-client-csharp" ``` - The config can be extended with options as follows: - ```yaml emit: - "@typespec/http-client-csharp" @@ -43,111 +32,79 @@ options: "@typespec/http-client-csharp": option: value ``` - ## Emitter options - ### `emitter-output-dir` - **Type:** `absolutePath` Defines the emitter output directory. Defaults to `{output-dir}/@typespec/http-client-csharp` See [Configuring output directory for more info](https://typespec.io/docs/handbook/configuration/configuration/#configuring-output-directory) - ### `api-version` - **Type:** `string` For TypeSpec files using the [`@versioned`](https://typespec.io/docs/libraries/versioning/reference/decorators/#@TypeSpec.Versioning.versioned) decorator, set this option to the version that should be used to generate against. - ### `generate-protocol-methods` - **Type:** `boolean` Set to `false` to skip generation of protocol methods. The default value is `true`. - ### `generate-convenience-methods` - **Type:** `boolean` Set to `false` to skip generation of convenience methods. The default value is `true`. - ### `unreferenced-types-handling` - **Type:** `"removeOrInternalize" | "internalize" | "keepAll"` Defines the strategy on how to handle unreferenced types. The default value is `removeOrInternalize`. - ### `new-project` - **Type:** `boolean` Set to `true` to overwrite the csproj if it already exists. The default value is `false`. - ### `save-inputs` - **Type:** `boolean` Set to `true` to save the `tspCodeModel.json` and `Configuration.json` files that are emitted and used as inputs to the generator. The default value is `false`. - ### `package-name` - **Type:** `string` Define the package name. If not specified, the first namespace defined in the TypeSpec is used as the package name. - ### `debug` - **Type:** `boolean` Set to `true` to automatically attempt to attach to a debugger when executing the C# generator. The default value is `false`. - ### `logLevel` - **Type:** `"info" | "debug" | "verbose"` Set the log level for which to collect traces. The default value is `info`. - ### `disable-xml-docs` - **Type:** `boolean` Set to `true` to disable XML documentation generation. The default value is `false`. - ### `generator-name` - **Type:** `string` The name of the generator. By default this is set to `ScmCodeModelGenerator`. Generator authors can set this to the name of a generator that inherits from `ScmCodeModelGenerator`. - ### `emitter-extension-path` - **Type:** `string` Allows emitter authors to specify the path to a custom emitter package, allowing you to extend the emitter behavior. This should be set to `import.meta.url` if you are using a custom emitter. +### `plugins` +**Type:** `array` +Paths to generator plugin assemblies (DLLs) or directories containing plugin assemblies. Each plugin must contain a class that extends GeneratorPlugin. ### `license` - **Type:** `object` License information for the generated client code. - ### `sdk-context-options` - **Type:** `object` The SDK context options that implement the `CreateSdkContextOptions` interface from the [`@azure-tools/typespec-client-generator-core`](https://www.npmjs.com/package/@azure-tools/typespec-client-generator-core) package to be used by the CSharp emitter. - ## Decorators - ### TypeSpec.HttpClient.CSharp - -- [`@dynamicModel`](#@dynamicmodel) - + - [`@dynamicModel`](#@dynamicmodel) #### `@dynamicModel` Marks a model or namespace as dynamic, indicating it should generate dynamic model code. Can be applied to Model or Namespace types. - ```typespec @TypeSpec.HttpClient.CSharp.dynamicModel ``` @@ -157,7 +114,6 @@ Can be applied to Model or Namespace types. `Model | Namespace` ##### Parameters - None ##### Examples @@ -176,3 +132,4 @@ namespace PetStore { } } ``` + From 605645c46a1ce5c6e96022f5c0c095a213538980 Mon Sep 17 00:00:00 2001 From: jolov Date: Thu, 2 Apr 2026 11:49:32 -0700 Subject: [PATCH 15/21] Run prettier on readme.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/http-client-csharp/readme.md | 55 +++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/packages/http-client-csharp/readme.md b/packages/http-client-csharp/readme.md index ea2f80dc732..2acc3d7cbaf 100644 --- a/packages/http-client-csharp/readme.md +++ b/packages/http-client-csharp/readme.md @@ -1,10 +1,15 @@ # @typespec/http-client-csharp + TypeSpec library for emitting Http Client libraries for C#. + ## Install + ```bash npm install @typespec/http-client-csharp ``` + ## Usage + ### Prerequisite - Install [Node.js](https://nodejs.org/download/) 20 or above. (Verify by running `node --version`) @@ -15,16 +20,22 @@ npm install @typespec/http-client-csharp For detailed instructions on how to customize the generated C# code, see the [Customization Guide](https://github.com/microsoft/typespec/blob/main/packages/http-client-csharp/.tspd/docs/customization.md). ## Emitter usage + 1. Via the command line + ```bash tsp compile . --emit=@typespec/http-client-csharp ``` + 2. Via the config + ```yaml emit: - - "@typespec/http-client-csharp" + - "@typespec/http-client-csharp" ``` + The config can be extended with options as follows: + ```yaml emit: - "@typespec/http-client-csharp" @@ -32,79 +43,117 @@ options: "@typespec/http-client-csharp": option: value ``` + ## Emitter options + ### `emitter-output-dir` + **Type:** `absolutePath` Defines the emitter output directory. Defaults to `{output-dir}/@typespec/http-client-csharp` See [Configuring output directory for more info](https://typespec.io/docs/handbook/configuration/configuration/#configuring-output-directory) + ### `api-version` + **Type:** `string` For TypeSpec files using the [`@versioned`](https://typespec.io/docs/libraries/versioning/reference/decorators/#@TypeSpec.Versioning.versioned) decorator, set this option to the version that should be used to generate against. + ### `generate-protocol-methods` + **Type:** `boolean` Set to `false` to skip generation of protocol methods. The default value is `true`. + ### `generate-convenience-methods` + **Type:** `boolean` Set to `false` to skip generation of convenience methods. The default value is `true`. + ### `unreferenced-types-handling` + **Type:** `"removeOrInternalize" | "internalize" | "keepAll"` Defines the strategy on how to handle unreferenced types. The default value is `removeOrInternalize`. + ### `new-project` + **Type:** `boolean` Set to `true` to overwrite the csproj if it already exists. The default value is `false`. + ### `save-inputs` + **Type:** `boolean` Set to `true` to save the `tspCodeModel.json` and `Configuration.json` files that are emitted and used as inputs to the generator. The default value is `false`. + ### `package-name` + **Type:** `string` Define the package name. If not specified, the first namespace defined in the TypeSpec is used as the package name. + ### `debug` + **Type:** `boolean` Set to `true` to automatically attempt to attach to a debugger when executing the C# generator. The default value is `false`. + ### `logLevel` + **Type:** `"info" | "debug" | "verbose"` Set the log level for which to collect traces. The default value is `info`. + ### `disable-xml-docs` + **Type:** `boolean` Set to `true` to disable XML documentation generation. The default value is `false`. + ### `generator-name` + **Type:** `string` The name of the generator. By default this is set to `ScmCodeModelGenerator`. Generator authors can set this to the name of a generator that inherits from `ScmCodeModelGenerator`. + ### `emitter-extension-path` + **Type:** `string` Allows emitter authors to specify the path to a custom emitter package, allowing you to extend the emitter behavior. This should be set to `import.meta.url` if you are using a custom emitter. + ### `plugins` + **Type:** `array` Paths to generator plugin assemblies (DLLs) or directories containing plugin assemblies. Each plugin must contain a class that extends GeneratorPlugin. + ### `license` + **Type:** `object` License information for the generated client code. + ### `sdk-context-options` + **Type:** `object` The SDK context options that implement the `CreateSdkContextOptions` interface from the [`@azure-tools/typespec-client-generator-core`](https://www.npmjs.com/package/@azure-tools/typespec-client-generator-core) package to be used by the CSharp emitter. + ## Decorators + ### TypeSpec.HttpClient.CSharp - - [`@dynamicModel`](#@dynamicmodel) + +- [`@dynamicModel`](#@dynamicmodel) + #### `@dynamicModel` Marks a model or namespace as dynamic, indicating it should generate dynamic model code. Can be applied to Model or Namespace types. + ```typespec @TypeSpec.HttpClient.CSharp.dynamicModel ``` @@ -114,6 +163,7 @@ Can be applied to Model or Namespace types. `Model | Namespace` ##### Parameters + None ##### Examples @@ -132,4 +182,3 @@ namespace PetStore { } } ``` - From 5286637f395be91367d3f940026070bd23e0805d Mon Sep 17 00:00:00 2001 From: jolov Date: Thu, 2 Apr 2026 12:33:35 -0700 Subject: [PATCH 16/21] Add generated docs files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../http-client-csharp/docs/decorators.md | 39 +++++++++ packages/http-client-csharp/docs/emitter.md | 87 +++++++++++++++++++ packages/http-client-csharp/docs/index.mdx | 33 +++++++ 3 files changed, 159 insertions(+) create mode 100644 packages/http-client-csharp/docs/decorators.md create mode 100644 packages/http-client-csharp/docs/emitter.md create mode 100644 packages/http-client-csharp/docs/index.mdx diff --git a/packages/http-client-csharp/docs/decorators.md b/packages/http-client-csharp/docs/decorators.md new file mode 100644 index 00000000000..892297a2833 --- /dev/null +++ b/packages/http-client-csharp/docs/decorators.md @@ -0,0 +1,39 @@ +--- +title: "Decorators" +description: "Decorators exported by @typespec/http-client-csharp" +toc_min_heading_level: 2 +toc_max_heading_level: 3 +--- +## TypeSpec.HttpClient.CSharp +### `@dynamicModel` {#@TypeSpec.HttpClient.CSharp.dynamicModel} + +Marks a model or namespace as dynamic, indicating it should generate dynamic model code. +Can be applied to Model or Namespace types. +```typespec +@TypeSpec.HttpClient.CSharp.dynamicModel +``` + +#### Target + +`Model | Namespace` + +#### Parameters +None + +#### Examples + +```tsp +@dynamicModel +model Pet { + name: string; + kind: string; +} + +@dynamicModel +namespace PetStore { + model Dog extends Pet { + breed: string; + } +} +``` + diff --git a/packages/http-client-csharp/docs/emitter.md b/packages/http-client-csharp/docs/emitter.md new file mode 100644 index 00000000000..6f608380235 --- /dev/null +++ b/packages/http-client-csharp/docs/emitter.md @@ -0,0 +1,87 @@ +--- +title: "Emitter usage" +--- +## Emitter usage +1. Via the command line +```bash +tsp compile . --emit=@typespec/http-client-csharp +``` +2. Via the config +```yaml +emit: + - "@typespec/http-client-csharp" +``` +The config can be extended with options as follows: +```yaml +emit: + - "@typespec/http-client-csharp" +options: + "@typespec/http-client-csharp": + option: value +``` +## Emitter options +### `emitter-output-dir` +**Type:** `absolutePath` + +Defines the emitter output directory. Defaults to `{output-dir}/@typespec/http-client-csharp` +See [Configuring output directory for more info](https://typespec.io/docs/handbook/configuration/configuration/#configuring-output-directory) +### `api-version` +**Type:** `string` + +For TypeSpec files using the [`@versioned`](https://typespec.io/docs/libraries/versioning/reference/decorators/#@TypeSpec.Versioning.versioned) decorator, set this option to the version that should be used to generate against. +### `generate-protocol-methods` +**Type:** `boolean` + +Set to `false` to skip generation of protocol methods. The default value is `true`. +### `generate-convenience-methods` +**Type:** `boolean` + +Set to `false` to skip generation of convenience methods. The default value is `true`. +### `unreferenced-types-handling` +**Type:** `"removeOrInternalize" | "internalize" | "keepAll"` + +Defines the strategy on how to handle unreferenced types. The default value is `removeOrInternalize`. +### `new-project` +**Type:** `boolean` + +Set to `true` to overwrite the csproj if it already exists. The default value is `false`. +### `save-inputs` +**Type:** `boolean` + +Set to `true` to save the `tspCodeModel.json` and `Configuration.json` files that are emitted and used as inputs to the generator. The default value is `false`. +### `package-name` +**Type:** `string` + +Define the package name. If not specified, the first namespace defined in the TypeSpec is used as the package name. +### `debug` +**Type:** `boolean` + +Set to `true` to automatically attempt to attach to a debugger when executing the C# generator. The default value is `false`. +### `logLevel` +**Type:** `"info" | "debug" | "verbose"` + +Set the log level for which to collect traces. The default value is `info`. +### `disable-xml-docs` +**Type:** `boolean` + +Set to `true` to disable XML documentation generation. The default value is `false`. +### `generator-name` +**Type:** `string` + +The name of the generator. By default this is set to `ScmCodeModelGenerator`. Generator authors can set this to the name of a generator that inherits from `ScmCodeModelGenerator`. +### `emitter-extension-path` +**Type:** `string` + +Allows emitter authors to specify the path to a custom emitter package, allowing you to extend the emitter behavior. This should be set to `import.meta.url` if you are using a custom emitter. +### `plugins` +**Type:** `array` + +Paths to generator plugin assemblies (DLLs) or directories containing plugin assemblies. Each plugin must contain a class that extends GeneratorPlugin. +### `license` +**Type:** `object` + +License information for the generated client code. +### `sdk-context-options` +**Type:** `object` + +The SDK context options that implement the `CreateSdkContextOptions` interface from the [`@azure-tools/typespec-client-generator-core`](https://www.npmjs.com/package/@azure-tools/typespec-client-generator-core) package to be used by the CSharp emitter. \ No newline at end of file diff --git a/packages/http-client-csharp/docs/index.mdx b/packages/http-client-csharp/docs/index.mdx new file mode 100644 index 00000000000..98fd44e9a3c --- /dev/null +++ b/packages/http-client-csharp/docs/index.mdx @@ -0,0 +1,33 @@ +--- +title: Overview +sidebar_position: 0 +toc_min_heading_level: 2 +toc_max_heading_level: 3 +--- +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +TypeSpec library for emitting Http Client libraries for C#. +## Install + + + +```bash +npm install @typespec/http-client-csharp +``` + + + + +```bash +npm install --save-peer @typespec/http-client-csharp +``` + + + + +## Emitter usage +[See documentation](./emitter.md) +## TypeSpec.HttpClient +## TypeSpec.HttpClient.CSharp +### Decorators + - [`@dynamicModel`](./decorators.md#@TypeSpec.HttpClient.CSharp.dynamicModel) \ No newline at end of file From 2e6b5b3aa8ff8efb09f4732dfdaa7d0080a8965e Mon Sep 17 00:00:00 2001 From: jolov Date: Thu, 2 Apr 2026 13:01:17 -0700 Subject: [PATCH 17/21] Regenerate website docs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../clients/http-client-csharp/reference/emitter.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/website/src/content/docs/docs/emitters/clients/http-client-csharp/reference/emitter.md b/website/src/content/docs/docs/emitters/clients/http-client-csharp/reference/emitter.md index 7a989e85368..f20d6edee33 100644 --- a/website/src/content/docs/docs/emitters/clients/http-client-csharp/reference/emitter.md +++ b/website/src/content/docs/docs/emitters/clients/http-client-csharp/reference/emitter.md @@ -108,6 +108,12 @@ The name of the generator. By default this is set to `ScmCodeModelGenerator`. Ge Allows emitter authors to specify the path to a custom emitter package, allowing you to extend the emitter behavior. This should be set to `import.meta.url` if you are using a custom emitter. +### `plugins` + +**Type:** `array` + +Paths to generator plugin assemblies (DLLs) or directories containing plugin assemblies. Each plugin must contain a class that extends GeneratorPlugin. + ### `license` **Type:** `object` From a7c75661f120fc65b39b58607f47165e71820f9e Mon Sep 17 00:00:00 2001 From: jolov Date: Thu, 2 Apr 2026 19:16:45 -0700 Subject: [PATCH 18/21] Run prettier on generated docs and add to regen-docs script Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../http-client-csharp/docs/decorators.md | 5 ++- packages/http-client-csharp/docs/emitter.md | 44 ++++++++++++++++++- packages/http-client-csharp/docs/index.mdx | 12 ++++- packages/http-client-csharp/package.json | 2 +- 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/packages/http-client-csharp/docs/decorators.md b/packages/http-client-csharp/docs/decorators.md index 892297a2833..46589b06c79 100644 --- a/packages/http-client-csharp/docs/decorators.md +++ b/packages/http-client-csharp/docs/decorators.md @@ -4,11 +4,14 @@ description: "Decorators exported by @typespec/http-client-csharp" toc_min_heading_level: 2 toc_max_heading_level: 3 --- + ## TypeSpec.HttpClient.CSharp + ### `@dynamicModel` {#@TypeSpec.HttpClient.CSharp.dynamicModel} Marks a model or namespace as dynamic, indicating it should generate dynamic model code. Can be applied to Model or Namespace types. + ```typespec @TypeSpec.HttpClient.CSharp.dynamicModel ``` @@ -18,6 +21,7 @@ Can be applied to Model or Namespace types. `Model | Namespace` #### Parameters + None #### Examples @@ -36,4 +40,3 @@ namespace PetStore { } } ``` - diff --git a/packages/http-client-csharp/docs/emitter.md b/packages/http-client-csharp/docs/emitter.md index 6f608380235..f20d6edee33 100644 --- a/packages/http-client-csharp/docs/emitter.md +++ b/packages/http-client-csharp/docs/emitter.md @@ -1,17 +1,24 @@ --- title: "Emitter usage" --- + ## Emitter usage + 1. Via the command line + ```bash tsp compile . --emit=@typespec/http-client-csharp ``` + 2. Via the config + ```yaml emit: - - "@typespec/http-client-csharp" + - "@typespec/http-client-csharp" ``` + The config can be extended with options as follows: + ```yaml emit: - "@typespec/http-client-csharp" @@ -19,69 +26,102 @@ options: "@typespec/http-client-csharp": option: value ``` + ## Emitter options + ### `emitter-output-dir` + **Type:** `absolutePath` Defines the emitter output directory. Defaults to `{output-dir}/@typespec/http-client-csharp` See [Configuring output directory for more info](https://typespec.io/docs/handbook/configuration/configuration/#configuring-output-directory) + ### `api-version` + **Type:** `string` For TypeSpec files using the [`@versioned`](https://typespec.io/docs/libraries/versioning/reference/decorators/#@TypeSpec.Versioning.versioned) decorator, set this option to the version that should be used to generate against. + ### `generate-protocol-methods` + **Type:** `boolean` Set to `false` to skip generation of protocol methods. The default value is `true`. + ### `generate-convenience-methods` + **Type:** `boolean` Set to `false` to skip generation of convenience methods. The default value is `true`. + ### `unreferenced-types-handling` + **Type:** `"removeOrInternalize" | "internalize" | "keepAll"` Defines the strategy on how to handle unreferenced types. The default value is `removeOrInternalize`. + ### `new-project` + **Type:** `boolean` Set to `true` to overwrite the csproj if it already exists. The default value is `false`. + ### `save-inputs` + **Type:** `boolean` Set to `true` to save the `tspCodeModel.json` and `Configuration.json` files that are emitted and used as inputs to the generator. The default value is `false`. + ### `package-name` + **Type:** `string` Define the package name. If not specified, the first namespace defined in the TypeSpec is used as the package name. + ### `debug` + **Type:** `boolean` Set to `true` to automatically attempt to attach to a debugger when executing the C# generator. The default value is `false`. + ### `logLevel` + **Type:** `"info" | "debug" | "verbose"` Set the log level for which to collect traces. The default value is `info`. + ### `disable-xml-docs` + **Type:** `boolean` Set to `true` to disable XML documentation generation. The default value is `false`. + ### `generator-name` + **Type:** `string` The name of the generator. By default this is set to `ScmCodeModelGenerator`. Generator authors can set this to the name of a generator that inherits from `ScmCodeModelGenerator`. + ### `emitter-extension-path` + **Type:** `string` Allows emitter authors to specify the path to a custom emitter package, allowing you to extend the emitter behavior. This should be set to `import.meta.url` if you are using a custom emitter. + ### `plugins` + **Type:** `array` Paths to generator plugin assemblies (DLLs) or directories containing plugin assemblies. Each plugin must contain a class that extends GeneratorPlugin. + ### `license` + **Type:** `object` License information for the generated client code. + ### `sdk-context-options` + **Type:** `object` -The SDK context options that implement the `CreateSdkContextOptions` interface from the [`@azure-tools/typespec-client-generator-core`](https://www.npmjs.com/package/@azure-tools/typespec-client-generator-core) package to be used by the CSharp emitter. \ No newline at end of file +The SDK context options that implement the `CreateSdkContextOptions` interface from the [`@azure-tools/typespec-client-generator-core`](https://www.npmjs.com/package/@azure-tools/typespec-client-generator-core) package to be used by the CSharp emitter. diff --git a/packages/http-client-csharp/docs/index.mdx b/packages/http-client-csharp/docs/index.mdx index 98fd44e9a3c..ca373e97796 100644 --- a/packages/http-client-csharp/docs/index.mdx +++ b/packages/http-client-csharp/docs/index.mdx @@ -4,10 +4,13 @@ sidebar_position: 0 toc_min_heading_level: 2 toc_max_heading_level: 3 --- -import { Tabs, TabItem } from '@astrojs/starlight/components'; + +import { Tabs, TabItem } from "@astrojs/starlight/components"; TypeSpec library for emitting Http Client libraries for C#. + ## Install + @@ -26,8 +29,13 @@ npm install --save-peer @typespec/http-client-csharp ## Emitter usage + [See documentation](./emitter.md) + ## TypeSpec.HttpClient + ## TypeSpec.HttpClient.CSharp + ### Decorators - - [`@dynamicModel`](./decorators.md#@TypeSpec.HttpClient.CSharp.dynamicModel) \ No newline at end of file + +- [`@dynamicModel`](./decorators.md#@TypeSpec.HttpClient.CSharp.dynamicModel) diff --git a/packages/http-client-csharp/package.json b/packages/http-client-csharp/package.json index e0bbfda7540..d7e2d7198c4 100644 --- a/packages/http-client-csharp/package.json +++ b/packages/http-client-csharp/package.json @@ -43,7 +43,7 @@ "lint:fix": "eslint . --fix", "format": "pnpm -w format:dir packages/http-client-csharp", "extract-api": "npx api-extractor run --local --verbose", - "regen-docs": "npm run build:emitter && tspd doc . --enable-experimental --output-dir ../../website/src/content/docs/docs/emitters/clients/http-client-csharp/reference --skip-js" + "regen-docs": "npm run build:emitter && tspd doc . --enable-experimental --output-dir ../../website/src/content/docs/docs/emitters/clients/http-client-csharp/reference --skip-js && npx prettier --write docs/ readme.md ../../website/src/content/docs/docs/emitters/clients/http-client-csharp/reference/" }, "files": [ "dist/emitter/src/**", From 44ae62caa62885b75c91c32ce13b77ef83c92672 Mon Sep 17 00:00:00 2001 From: jolov Date: Thu, 2 Apr 2026 20:50:08 -0700 Subject: [PATCH 19/21] Fix prettier formatting in website index.mdx Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../emitters/clients/http-client-csharp/reference/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/content/docs/docs/emitters/clients/http-client-csharp/reference/index.mdx b/website/src/content/docs/docs/emitters/clients/http-client-csharp/reference/index.mdx index e7a4de67da2..ca373e97796 100644 --- a/website/src/content/docs/docs/emitters/clients/http-client-csharp/reference/index.mdx +++ b/website/src/content/docs/docs/emitters/clients/http-client-csharp/reference/index.mdx @@ -5,7 +5,7 @@ toc_min_heading_level: 2 toc_max_heading_level: 3 --- -import { Tabs, TabItem } from '@astrojs/starlight/components'; +import { Tabs, TabItem } from "@astrojs/starlight/components"; TypeSpec library for emitting Http Client libraries for C#. From 7c4a09c0dc5276719381ceeecfcd960d0583eef9 Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 6 Apr 2026 09:15:54 -0700 Subject: [PATCH 20/21] refactor: construct plugin output path from csproj properties instead of parsing build output Address review feedback from @jorgerangel-msft: instead of parsing the 'dotnet build' stdout for the arrow line (which is fragile and locale-dependent), construct the expected output path deterministically by reading TargetFramework and AssemblyName from the csproj XML. The new GetExpectedOutputPath method builds the path as: [ProjectDirectory]/bin/Release/[TargetFramework]/[AssemblyName].dll Added unit tests for GetExpectedOutputPath covering: - Default assembly name (from project file name) - Explicit AssemblyName property - Missing TargetFramework returns null - Invalid XML returns null Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/StartUp/GeneratorHandler.cs | 60 ++++++++--- .../test/StartUp/GeneratorHandlerTests.cs | 99 +++++++++++++++++++ 2 files changed, 144 insertions(+), 15 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs index a15eb975699..d5b2239c988 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs @@ -200,6 +200,7 @@ internal static void AddConfiguredPluginDlls(AggregateCatalog catalog, Configura /// /// Builds a plugin .csproj and returns the path to the output DLL. + /// The output path is constructed from the csproj properties rather than parsing build output. /// internal static string? BuildPlugin(string csprojPath, Emitter emitter) { @@ -219,7 +220,8 @@ internal static void AddConfiguredPluginDlls(AggregateCatalog catalog, Configura }; process.Start(); - var stdout = process.StandardOutput.ReadToEnd(); + // Read both streams to avoid deadlocks, even though we only use stderr for error reporting. + process.StandardOutput.ReadToEnd(); var stderr = process.StandardError.ReadToEnd(); process.WaitForExit(); @@ -229,27 +231,55 @@ internal static void AddConfiguredPluginDlls(AggregateCatalog catalog, Configura $"Failed to build plugin '{csprojPath}'. Exit code: {process.ExitCode}\n{stderr}"); } - // Parse the build output to find the produced DLL path. - // dotnet build outputs a line like: " MyPlugin -> /path/to/bin/Release/net10.0/MyPlugin.dll" - foreach (var line in stdout.Split('\n')) + var dllPath = GetExpectedOutputPath(csprojPath); + if (dllPath != null && File.Exists(dllPath)) { - var trimmed = line.Trim(); - var arrowIndex = trimmed.IndexOf(" -> ", StringComparison.Ordinal); - if (arrowIndex >= 0) - { - var dllPath = trimmed[(arrowIndex + 4)..].Trim(); - if (dllPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) && File.Exists(dllPath)) - { - emitter.Info($"Plugin built: {dllPath}"); - return dllPath; - } - } + emitter.Info($"Plugin built: {dllPath}"); + return dllPath; } emitter.Info($"Warning: Build succeeded but could not determine output DLL path for '{csprojPath}'"); return null; } + /// + /// Constructs the expected output DLL path from the csproj properties: + /// [ProjectDirectory]/bin/Release/[TargetFramework]/[AssemblyName].dll + /// + internal static string? GetExpectedOutputPath(string csprojPath) + { + var projectDir = Path.GetDirectoryName(csprojPath)!; + var projectName = Path.GetFileNameWithoutExtension(csprojPath); + + try + { + using var stream = File.OpenRead(csprojPath); + var doc = System.Xml.Linq.XDocument.Load(stream); + + var propertyGroups = doc.Descendants("PropertyGroup"); + string? targetFramework = null; + string? assemblyName = null; + + foreach (var pg in propertyGroups) + { + targetFramework ??= pg.Element("TargetFramework")?.Value; + assemblyName ??= pg.Element("AssemblyName")?.Value; + } + + if (string.IsNullOrEmpty(targetFramework)) + { + return null; + } + + var effectiveAssemblyName = string.IsNullOrEmpty(assemblyName) ? projectName : assemblyName; + return Path.Combine(projectDir, "bin", "Release", targetFramework, $"{effectiveAssemblyName}.dll"); + } + catch + { + return null; + } + } + internal static IList GetOrderedPluginDlls(string pluginDirectoryStart) { var dllPathsInOrder = new List(); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs index c55f38165c0..14a454f6164 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs @@ -477,5 +477,104 @@ namespace Plugin2 { public class Dummy { } }"); try { Directory.Delete(testDir2, true); } catch { } } } + [Test] + public void GetExpectedOutputPath_ConstructsPathFromCsprojProperties() + { + var testDir = Path.Combine(Path.GetTempPath(), "typespec-test-plugin-" + Guid.NewGuid().ToString("N")[..8]); + try + { + Directory.CreateDirectory(testDir); + + File.WriteAllText(Path.Combine(testDir, "MyPlugin.csproj"), @" + + net10.0 + +"); + + var result = GeneratorHandler.GetExpectedOutputPath( + Path.Combine(testDir, "MyPlugin.csproj")); + + Assert.IsNotNull(result); + var expected = Path.Combine(testDir, "bin", "Release", "net10.0", "MyPlugin.dll"); + Assert.AreEqual(expected, result); + } + finally + { + try { Directory.Delete(testDir, true); } catch { } + } + } + + [Test] + public void GetExpectedOutputPath_UsesAssemblyNameWhenSpecified() + { + var testDir = Path.Combine(Path.GetTempPath(), "typespec-test-plugin-" + Guid.NewGuid().ToString("N")[..8]); + try + { + Directory.CreateDirectory(testDir); + + File.WriteAllText(Path.Combine(testDir, "MyPlugin.csproj"), @" + + net10.0 + CustomName + +"); + + var result = GeneratorHandler.GetExpectedOutputPath( + Path.Combine(testDir, "MyPlugin.csproj")); + + Assert.IsNotNull(result); + var expected = Path.Combine(testDir, "bin", "Release", "net10.0", "CustomName.dll"); + Assert.AreEqual(expected, result); + } + finally + { + try { Directory.Delete(testDir, true); } catch { } + } + } + + [Test] + public void GetExpectedOutputPath_ReturnsNullWhenNoTargetFramework() + { + var testDir = Path.Combine(Path.GetTempPath(), "typespec-test-plugin-" + Guid.NewGuid().ToString("N")[..8]); + try + { + Directory.CreateDirectory(testDir); + + File.WriteAllText(Path.Combine(testDir, "Bad.csproj"), @" + + +"); + + var result = GeneratorHandler.GetExpectedOutputPath( + Path.Combine(testDir, "Bad.csproj")); + + Assert.IsNull(result); + } + finally + { + try { Directory.Delete(testDir, true); } catch { } + } + } + + [Test] + public void GetExpectedOutputPath_ReturnsNullForInvalidXml() + { + var testDir = Path.Combine(Path.GetTempPath(), "typespec-test-plugin-" + Guid.NewGuid().ToString("N")[..8]); + try + { + Directory.CreateDirectory(testDir); + + File.WriteAllText(Path.Combine(testDir, "Bad.csproj"), "not valid xml"); + + var result = GeneratorHandler.GetExpectedOutputPath( + Path.Combine(testDir, "Bad.csproj")); + + Assert.IsNull(result); + } + finally + { + try { Directory.Delete(testDir, true); } catch { } + } + } } } From 7cfe8dd60529395e1a69770c47573db91b90a313 Mon Sep 17 00:00:00 2001 From: jolov Date: Mon, 6 Apr 2026 09:26:23 -0700 Subject: [PATCH 21/21] refactor: add using statement for System.Xml.Linq and support TargetFrameworks Address additional review feedback from @jorgerangel-msft: - Add 'using System.Xml.Linq' instead of inline-qualifying XDocument - Support multi-targeting projects by falling back to TargetFrameworks (semicolon-separated) when TargetFramework is not found, using the first listed framework - Add test for TargetFrameworks multi-targeting scenario Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/StartUp/GeneratorHandler.cs | 17 +++++++++++- .../test/StartUp/GeneratorHandlerTests.cs | 27 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs index d5b2239c988..bee78c0034c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/StartUp/GeneratorHandler.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Reflection; using System.Text.Json; +using System.Xml.Linq; using Microsoft.TypeSpec.Generator.EmitterRpc; namespace Microsoft.TypeSpec.Generator @@ -254,7 +255,7 @@ internal static void AddConfiguredPluginDlls(AggregateCatalog catalog, Configura try { using var stream = File.OpenRead(csprojPath); - var doc = System.Xml.Linq.XDocument.Load(stream); + var doc = XDocument.Load(stream); var propertyGroups = doc.Descendants("PropertyGroup"); string? targetFramework = null; @@ -266,6 +267,20 @@ internal static void AddConfiguredPluginDlls(AggregateCatalog catalog, Configura assemblyName ??= pg.Element("AssemblyName")?.Value; } + // For multi-targeting projects, use the first target framework + if (string.IsNullOrEmpty(targetFramework)) + { + foreach (var pg in propertyGroups) + { + var frameworks = pg.Element("TargetFrameworks")?.Value; + if (!string.IsNullOrEmpty(frameworks)) + { + targetFramework = frameworks.Split(';', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); + break; + } + } + } + if (string.IsNullOrEmpty(targetFramework)) { return null; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs index 14a454f6164..18589224bdf 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/StartUp/GeneratorHandlerTests.cs @@ -576,5 +576,32 @@ public void GetExpectedOutputPath_ReturnsNullForInvalidXml() try { Directory.Delete(testDir, true); } catch { } } } + + [Test] + public void GetExpectedOutputPath_UsesFirstTargetFrameworkFromMultiTargeting() + { + var testDir = Path.Combine(Path.GetTempPath(), "typespec-test-plugin-" + Guid.NewGuid().ToString("N")[..8]); + try + { + Directory.CreateDirectory(testDir); + + File.WriteAllText(Path.Combine(testDir, "MultiTarget.csproj"), @" + + net8.0;net10.0 + +"); + + var result = GeneratorHandler.GetExpectedOutputPath( + Path.Combine(testDir, "MultiTarget.csproj")); + + Assert.IsNotNull(result); + var expected = Path.Combine(testDir, "bin", "Release", "net8.0", "MultiTarget.dll"); + Assert.AreEqual(expected, result); + } + finally + { + try { Directory.Delete(testDir, true); } catch { } + } + } } }