From 46e6ff5f67cb39d079dedcae5f73a2fdc0b4b8d5 Mon Sep 17 00:00:00 2001 From: nick863 <30440255+nick863@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:41:04 -0700 Subject: [PATCH 01/21] Fix resolution of the external dependencies. --- .../ExternalTypeReferenceResolver.cs | 30 ++++++++++++++++--- .../src/Utilities/NugetPackageResolver.cs | 16 ++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/ExternalTypeReferenceResolver.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/ExternalTypeReferenceResolver.cs index e66fbdc2c5a..eaed372b225 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/ExternalTypeReferenceResolver.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/ExternalTypeReferenceResolver.cs @@ -5,6 +5,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; @@ -12,6 +13,7 @@ using Microsoft.CodeAnalysis; using Microsoft.TypeSpec.Generator.Input; using NuGet.Configuration; +using NuGet.Versioning; namespace Microsoft.TypeSpec.Generator.Utilities { @@ -238,10 +240,30 @@ private static async Task ResolveResultAsync(InputExternalType { try { - var resolvedVersion = !string.IsNullOrEmpty(external.MinVersion) - ? external.MinVersion! - : await NugetPackageResolver.ResolveLatestPackageVersion(external.Package!, nugetSettings); - + string? resolvedVersion; + // Search for the compatible version. + if (!string.IsNullOrEmpty(external.MinVersion)) + { + // If min version was provided, we + // 1. Search if it is in our repositories; + // 2. Get the latest one if it is not. + // 3. If our version is a pre release, include pre released versions in our search. + NuGetVersion minVersion = new(external.MinVersion); + IList versions = await NugetPackageResolver.GetAllVersions(external.Package!, nugetSettings, allowPrerelease: minVersion.IsPrerelease); + if (versions.Any(x => x == minVersion)) + { + resolvedVersion = external.MinVersion; + } + else + { + resolvedVersion = versions.Max()?.ToString(); + } + } + else + { + // If min version was not provided, get the latest stable version. + resolvedVersion = await NugetPackageResolver.ResolveLatestPackageVersion(external.Package!, nugetSettings); + } if (!string.IsNullOrEmpty(resolvedVersion)) { var downloader = new NugetPackageDownloader(external.Package!, resolvedVersion!, null, nugetSettings); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/NugetPackageResolver.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/NugetPackageResolver.cs index 2c2883a3534..7174b75a8c2 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/NugetPackageResolver.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/NugetPackageResolver.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using NuGet.Configuration; using NuGet.Frameworks; +using NuGet.Packaging; using NuGet.Protocol; using NuGet.Protocol.Core.Types; using NuGet.Repositories; @@ -202,6 +203,21 @@ private static NuGetFramework ResolveCurrentFramework() return null; } + public static async Task> GetAllVersions(string packageName, ISettings nugetSettings, bool allowPrerelease) + { + List versions = []; + var sources = SettingsUtility.GetEnabledSources(nugetSettings); + using var cacheContext = new SourceCacheContext(); + foreach (var source in sources) + { + var repository = Repository.Factory.GetCoreV3(source.Source); + var resource = await repository.GetResourceAsync(); + IEnumerable versionsFromOneRepo = (await resource.GetAllVersionsAsync(packageName, cacheContext, NuGet.Common.NullLogger.Instance, CancellationToken.None)).Where(v => !v.IsPrerelease || allowPrerelease); + versions.AddRange(versionsFromOneRepo); + } + return versions; + } + /// /// Queries the configured NuGet feeds for the latest stable version of . /// When is provided, the latest stable version greater than or equal to it From 6a3f53067febfd09de8f01077c1c37df33cc9e85 Mon Sep 17 00:00:00 2001 From: nick863 <30440255+nick863@users.noreply.github.com> Date: Fri, 21 Aug 2026 17:22:44 -0700 Subject: [PATCH 02/21] Fix --- .../src/Utilities/NugetPackageResolver.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/NugetPackageResolver.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/NugetPackageResolver.cs index 7174b75a8c2..ab8de1f4e93 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/NugetPackageResolver.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/NugetPackageResolver.cs @@ -205,17 +205,24 @@ private static NuGetFramework ResolveCurrentFramework() public static async Task> GetAllVersions(string packageName, ISettings nugetSettings, bool allowPrerelease) { - List versions = []; + HashSet versions = []; var sources = SettingsUtility.GetEnabledSources(nugetSettings); using var cacheContext = new SourceCacheContext(); foreach (var source in sources) { - var repository = Repository.Factory.GetCoreV3(source.Source); - var resource = await repository.GetResourceAsync(); - IEnumerable versionsFromOneRepo = (await resource.GetAllVersionsAsync(packageName, cacheContext, NuGet.Common.NullLogger.Instance, CancellationToken.None)).Where(v => !v.IsPrerelease || allowPrerelease); - versions.AddRange(versionsFromOneRepo); + try + { + var repository = Repository.Factory.GetCoreV3(source.Source); + var resource = await repository.GetResourceAsync(); + IEnumerable versionsFromOneRepo = (await resource.GetAllVersionsAsync(packageName, cacheContext, NuGet.Common.NullLogger.Instance, CancellationToken.None)).Where(v => !v.IsPrerelease || allowPrerelease); + versions.AddRange(versionsFromOneRepo); + } + catch + { + // Skip failed source. + } } - return versions; + return [..versions]; } /// From 970c1652c6d0b777836f3992ec1f291661d26491 Mon Sep 17 00:00:00 2001 From: nick863 Date: Mon, 24 Aug 2026 13:54:06 -0700 Subject: [PATCH 03/21] Remove the code, downloading packages, add restore --- .../PostProcessing/GeneratedCodeWorkspace.cs | 33 ++++++++++++- .../ExternalTypeReferenceResolver.cs | 48 ------------------- 2 files changed, 31 insertions(+), 50 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 9d0c903ef53..83f0abd2e9b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -275,9 +275,38 @@ internal static async Task AddPackageReferencesFromProject() { return; } - + // Use the dotnet restore mechanism to get all the dependent packages. + Process restore = new(); + ProcessStartInfo info = new() + { + UseShellExecute = false, + WindowStyle = ProcessWindowStyle.Hidden, + FileName = "dotnet", + Arguments = $"restore {projectFilePath}", + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + restore.StartInfo = info; + if (restore.Start()) + { + await restore.WaitForExitAsync(); + if (restore.ExitCode != 0) + { + string output = await restore.StandardOutput.ReadToEndAsync(); + string error = await restore.StandardError.ReadToEndAsync(); + CodeModelGenerator.Instance.Emitter.Debug( + $"The dotnet restore {projectFilePath} command exited with {restore.ExitCode}.]\n" + + $"Standard output: {output}\n" + + $"Error output: {error}" + ); + } + } + else + { + CodeModelGenerator.Instance.Emitter.Debug( + $"Unable to run dotnet restore on the project {projectFilePath}"); + } var projectRoot = ProjectRootElement.Open(projectFilePath, new MSBuildProjectCollection()); - var nugetSettings = Settings.LoadDefaultSettings(projectFilePath); var globalPackagesFolder = SettingsUtility.GetGlobalPackagesFolder(nugetSettings); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/ExternalTypeReferenceResolver.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/ExternalTypeReferenceResolver.cs index eaed372b225..79b4ac32cdb 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/ExternalTypeReferenceResolver.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/ExternalTypeReferenceResolver.cs @@ -5,7 +5,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; @@ -13,7 +12,6 @@ using Microsoft.CodeAnalysis; using Microsoft.TypeSpec.Generator.Input; using NuGet.Configuration; -using NuGet.Versioning; namespace Microsoft.TypeSpec.Generator.Utilities { @@ -236,52 +234,6 @@ private static async Task ResolveResultAsync(InputExternalType string? assemblyPath = NugetPackageResolver.FindPackageAssembly( globalPackagesFolder, external.Package!, external.MinVersion); - if (assemblyPath == null) - { - try - { - string? resolvedVersion; - // Search for the compatible version. - if (!string.IsNullOrEmpty(external.MinVersion)) - { - // If min version was provided, we - // 1. Search if it is in our repositories; - // 2. Get the latest one if it is not. - // 3. If our version is a pre release, include pre released versions in our search. - NuGetVersion minVersion = new(external.MinVersion); - IList versions = await NugetPackageResolver.GetAllVersions(external.Package!, nugetSettings, allowPrerelease: minVersion.IsPrerelease); - if (versions.Any(x => x == minVersion)) - { - resolvedVersion = external.MinVersion; - } - else - { - resolvedVersion = versions.Max()?.ToString(); - } - } - else - { - // If min version was not provided, get the latest stable version. - resolvedVersion = await NugetPackageResolver.ResolveLatestPackageVersion(external.Package!, nugetSettings); - } - if (!string.IsNullOrEmpty(resolvedVersion)) - { - var downloader = new NugetPackageDownloader(external.Package!, resolvedVersion!, null, nugetSettings); - var downloadedPath = await downloader.DownloadAndInstallPackage(); - var downloadedAssembly = Path.Combine(downloadedPath, $"{external.Package}.dll"); - if (File.Exists(downloadedAssembly)) - { - assemblyPath = downloadedAssembly; - } - } - } - catch (Exception ex) - { - generator.Emitter?.Debug( - $"Could not download package '{external.Package}' for external type '{external.Identity}': {ex.Message}"); - } - } - if (assemblyPath == null || !File.Exists(assemblyPath)) { var versionQualifier = string.IsNullOrEmpty(external.MinVersion) From 5207c494fdd71d0a632c3311578bc0fd96fecf3d Mon Sep 17 00:00:00 2001 From: nick863 Date: Mon, 24 Aug 2026 14:15:45 -0700 Subject: [PATCH 04/21] Fix --- .../PostProcessing/GeneratedCodeWorkspace.cs | 2 +- .../src/Utilities/NugetPackageResolver.cs | 22 ------------------- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 83f0abd2e9b..fc05d48ca27 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -282,7 +282,7 @@ internal static async Task AddPackageReferencesFromProject() UseShellExecute = false, WindowStyle = ProcessWindowStyle.Hidden, FileName = "dotnet", - Arguments = $"restore {projectFilePath}", + ArgumentList = {"restore", projectFilePath}, RedirectStandardOutput = true, RedirectStandardError = true, }; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/NugetPackageResolver.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/NugetPackageResolver.cs index ab8de1f4e93..2423785b87b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/NugetPackageResolver.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/NugetPackageResolver.cs @@ -203,28 +203,6 @@ private static NuGetFramework ResolveCurrentFramework() return null; } - public static async Task> GetAllVersions(string packageName, ISettings nugetSettings, bool allowPrerelease) - { - HashSet versions = []; - var sources = SettingsUtility.GetEnabledSources(nugetSettings); - using var cacheContext = new SourceCacheContext(); - foreach (var source in sources) - { - try - { - var repository = Repository.Factory.GetCoreV3(source.Source); - var resource = await repository.GetResourceAsync(); - IEnumerable versionsFromOneRepo = (await resource.GetAllVersionsAsync(packageName, cacheContext, NuGet.Common.NullLogger.Instance, CancellationToken.None)).Where(v => !v.IsPrerelease || allowPrerelease); - versions.AddRange(versionsFromOneRepo); - } - catch - { - // Skip failed source. - } - } - return [..versions]; - } - /// /// Queries the configured NuGet feeds for the latest stable version of . /// When is provided, the latest stable version greater than or equal to it From 4ff24dd166eeec9ffcc52b41287c5c3e9d96f904 Mon Sep 17 00:00:00 2001 From: nick863 Date: Mon, 24 Aug 2026 14:48:29 -0700 Subject: [PATCH 05/21] Fix II --- .../PostProcessing/GeneratedCodeWorkspace.cs | 27 +++++-------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index fc05d48ca27..329df8d0f88 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -289,11 +289,13 @@ internal static async Task AddPackageReferencesFromProject() restore.StartInfo = info; if (restore.Start()) { + Task outputTask = restore.StandardOutput.ReadToEndAsync(); + Task errorTask = restore.StandardError.ReadToEndAsync(); await restore.WaitForExitAsync(); + string output = await outputTask; + string error = await errorTask; if (restore.ExitCode != 0) { - string output = await restore.StandardOutput.ReadToEndAsync(); - string error = await restore.StandardError.ReadToEndAsync(); CodeModelGenerator.Instance.Emitter.Debug( $"The dotnet restore {projectFilePath} command exited with {restore.ExitCode}.]\n" + $"Standard output: {output}\n" + @@ -339,25 +341,8 @@ internal static async Task AddPackageReferencesFromProject() // If not found in cache, download the latest version from NuGet feeds if (resolvedAssemblyPath == null) { - try - { - var latestVersion = await NugetPackageResolver.ResolveLatestPackageVersion(refPackageName, nugetSettings); - if (latestVersion != null) - { - var downloader = new NugetPackageDownloader(refPackageName, latestVersion, null, nugetSettings); - var downloadedPath = await downloader.DownloadAndInstallPackage(); - var downloadedAssembly = Path.Combine(downloadedPath, $"{refPackageName}.dll"); - if (File.Exists(downloadedAssembly)) - { - resolvedAssemblyPath = downloadedAssembly; - } - } - } - catch (Exception ex) - { - CodeModelGenerator.Instance.Emitter.Debug( - $"Could not download package {refPackageName}: {ex.Message}"); - } + CodeModelGenerator.Instance.Emitter.Debug( + $"The package {refPackageName} was not restored."); } if (resolvedAssemblyPath != null) From 8930a629074d1969a946e4ce0f64c5f40a63316f Mon Sep 17 00:00:00 2001 From: nick863 Date: Mon, 24 Aug 2026 15:07:35 -0700 Subject: [PATCH 06/21] Fix III --- .../src/PostProcessing/GeneratedCodeWorkspace.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 329df8d0f88..4faecf443ea 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -297,7 +297,7 @@ internal static async Task AddPackageReferencesFromProject() if (restore.ExitCode != 0) { CodeModelGenerator.Instance.Emitter.Debug( - $"The dotnet restore {projectFilePath} command exited with {restore.ExitCode}.]\n" + + $"The dotnet restore {projectFilePath} command exited with {restore.ExitCode}.\n" + $"Standard output: {output}\n" + $"Error output: {error}" ); @@ -306,7 +306,7 @@ internal static async Task AddPackageReferencesFromProject() else { CodeModelGenerator.Instance.Emitter.Debug( - $"Unable to run dotnet restore on the project {projectFilePath}"); + $"Unable to run dotnet restore on the project {projectFilePath}"); } var projectRoot = ProjectRootElement.Open(projectFilePath, new MSBuildProjectCollection()); var nugetSettings = Settings.LoadDefaultSettings(projectFilePath); From 46dbeeab5f37cb5941d25cc10e034a6372052373 Mon Sep 17 00:00:00 2001 From: nick863 Date: Mon, 24 Aug 2026 15:58:03 -0700 Subject: [PATCH 07/21] Fix IV --- .../src/Utilities/NugetPackageResolver.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/NugetPackageResolver.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/NugetPackageResolver.cs index 2423785b87b..521f2ed56e9 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/NugetPackageResolver.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/NugetPackageResolver.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; @@ -10,7 +9,6 @@ using System.Threading.Tasks; using NuGet.Configuration; using NuGet.Frameworks; -using NuGet.Packaging; using NuGet.Protocol; using NuGet.Protocol.Core.Types; using NuGet.Repositories; From d712d9425793043a84b89659ee9c3f823ecb0539 Mon Sep 17 00:00:00 2001 From: nick863 Date: Tue, 25 Aug 2026 16:08:16 -0700 Subject: [PATCH 08/21] Fix --- .../PostProcessing/GeneratedCodeWorkspace.cs | 93 ++++++++++++++++--- .../test/GeneratedCodeWorkspaceTests.cs | 19 +++- 2 files changed, 93 insertions(+), 19 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 4faecf443ea..aab0e36ba11 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -6,6 +6,9 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Reflection; +using System.Runtime.Versioning; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Build.Construction; @@ -18,6 +21,7 @@ using Microsoft.TypeSpec.Generator.SourceInput; using Microsoft.TypeSpec.Generator.Utilities; using NuGet.Configuration; +using NuGet.Versioning; using MSBuildProjectCollection = Microsoft.Build.Evaluation.ProjectCollection; namespace Microsoft.TypeSpec.Generator @@ -296,29 +300,73 @@ internal static async Task AddPackageReferencesFromProject() string error = await errorTask; if (restore.ExitCode != 0) { - CodeModelGenerator.Instance.Emitter.Debug( - $"The dotnet restore {projectFilePath} command exited with {restore.ExitCode}.\n" + + CodeModelGenerator.Instance.Emitter.ReportDiagnostic( + code: "unable-to-restore-target-package", + message: $"The dotnet restore {projectFilePath} command exited with {restore.ExitCode}.\n" + $"Standard output: {output}\n" + - $"Error output: {error}" - ); + $"Error output: {error}", + severity: EmitterRpc.EmitterDiagnosticSeverity.Error + ); } } else { - CodeModelGenerator.Instance.Emitter.Debug( - $"Unable to run dotnet restore on the project {projectFilePath}"); + CodeModelGenerator.Instance.Emitter.ReportDiagnostic( + code: "unable-to-run-dotnet-restore", + message: $"Unable to run dotnet restore on the project {projectFilePath}", + severity: EmitterRpc.EmitterDiagnosticSeverity.Error + ); } var projectRoot = ProjectRootElement.Open(projectFilePath, new MSBuildProjectCollection()); var nugetSettings = Settings.LoadDefaultSettings(projectFilePath); var globalPackagesFolder = SettingsUtility.GetGlobalPackagesFolder(nugetSettings); + // Read in the resolved direct dependencies + DirectoryInfo? directory = (new DirectoryInfo(CodeModelGenerator.Instance.Configuration.OutputDirectory)).Parent?.Parent?.Parent; + Dictionary hshNameVersion = []; + string framework = NugetPackageResolver.CurrentFramework.GetShortFolderName(); + if (directory != null) + { + string assetsJson = Path.Combine(directory.FullName, "artifacts", "obj", CodeModelGenerator.Instance.Configuration.PackageName, "project.assets.json"); + if (!File.Exists(assetsJson)) + { + // Try to get file from the project directory. + assetsJson = Path.Combine(CodeModelGenerator.Instance.Configuration.ProjectDirectory, "obj", "project.assets.json"); + } + if (File.Exists(assetsJson)) + { + Utf8JsonReader reader = new Utf8JsonReader(File.ReadAllBytes(assetsJson)); + using JsonDocument document = JsonDocument.ParseValue(ref reader); + foreach (JsonProperty prop in document.RootElement.EnumerateObject()) + { + if (prop.Value.ValueKind == JsonValueKind.Object && prop.NameEquals("targets")) + { + foreach (JsonProperty targetFramework in prop.Value.EnumerateObject()) + { + if (targetFramework.Value.ValueKind == JsonValueKind.Object && string.Equals(targetFramework.Name, framework, StringComparison.InvariantCultureIgnoreCase)) + { + // Parse dependencies. They are structured as SomePackage/package.version + foreach (JsonProperty packageAndVersion in targetFramework.Value.EnumerateObject()) + { + string[] nameAndVersion = packageAndVersion.Name.Split('/'); + if (nameAndVersion.Length == 2) + { + hshNameVersion[nameAndVersion[0]] = nameAndVersion[1]; + } + } + } + } + } + } + } + } // Build a set of assembly names already registered so we can skip them var existingRefs = new HashSet( - CodeModelGenerator.Instance.AdditionalMetadataReferences - .Where(r => r.Display is not null) - .Select(r => Path.GetFileNameWithoutExtension(r.Display!)) - .Where(n => !string.IsNullOrEmpty(n)), - StringComparer.OrdinalIgnoreCase); + CodeModelGenerator.Instance.AdditionalMetadataReferences + .Where(r => r.Display is not null) + .Select(r => Path.GetFileNameWithoutExtension(r.Display!)) + .Where(n => !string.IsNullOrEmpty(n)), + StringComparer.OrdinalIgnoreCase); foreach (var item in projectRoot.Items.Where(i => i.ItemType == "PackageReference")) { @@ -336,13 +384,28 @@ internal static async Task AddPackageReferencesFromProject() } // Search the NuGet global packages folder for any cached version of this package. - string? resolvedAssemblyPath = NugetPackageResolver.FindPackageAssembly(globalPackagesFolder, refPackageName); - - // If not found in cache, download the latest version from NuGet feeds + string? version = default; + hshNameVersion.TryGetValue(refPackageName, out version); + string? resolvedAssemblyPath = version is null + ? NugetPackageResolver.FindPackageAssembly(globalPackagesFolder, refPackageName) + : NugetPackageResolver.FindPackageAssemblyInVersion(globalPackagesFolder, refPackageName, version); if (resolvedAssemblyPath == null) { CodeModelGenerator.Instance.Emitter.Debug( - $"The package {refPackageName} was not restored."); + $"The package {refPackageName}{(version != null ? "v. "+ version : "")} was not restored."); + } + else if (version is null) + { + string packageDir = Path.Combine(globalPackagesFolder, refPackageName.ToLowerInvariant()); + string[] allDirs = Directory.GetDirectories(packageDir); + NuGetVersion? maxVersion = allDirs.Select(dir => NuGetVersion.TryParse(Path.GetFileName(dir), out var v) ? v : null) + .Where(t => t != null) + .Max(); + if (maxVersion != null) + { + CodeModelGenerator.Instance.Emitter.Debug( + $"Using cached {refPackageName} v. {maxVersion.Version}."); + } } if (resolvedAssemblyPath != null) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs index b4eaa0446c3..1e59acbcdcc 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs @@ -299,13 +299,18 @@ public async Task AddPackageReferencesFromProject_SkipsAlreadyAddedReferences() } [Test] - public async Task AddPackageReferencesFromProject_AddsMultiplePackageReferences() + [TestCase(true)] + [TestCase(false)] + public async Task AddPackageReferencesFromProject_AddsMultiplePackageReferences(bool badPackage) { var ns = "TestNamespace"; var nugetCacheDir = Path.Combine(_tempDirectory!, "NuGetCache"); // Create two fake packages in the cache - CreateFakeNuGetPackage(nugetCacheDir, "First.Package", "1.0.0"); + if (!badPackage) + { + CreateFakeNuGetPackage(nugetCacheDir, "First.Package", "1.0.0"); + } CreateFakeNuGetPackage(nugetCacheDir, "Second.Package", "3.5.0"); var csprojContent = @" @@ -331,8 +336,14 @@ public async Task AddPackageReferencesFromProject_AddsMultiplePackageReferences( var refCountBefore = CodeModelGenerator.Instance.AdditionalMetadataReferences.Count; await GeneratedCodeWorkspace.AddPackageReferencesFromProject(); var refCountAfter = CodeModelGenerator.Instance.AdditionalMetadataReferences.Count; - - Assert.AreEqual(refCountBefore + 2, refCountAfter, "Should have added two metadata references"); + if (badPackage) + { + Assert.AreEqual(refCountBefore + 1, refCountAfter, "Should have added one metadata reference as the second one was intentionally broken"); + } + else + { + Assert.AreEqual(refCountBefore + 2, refCountAfter, "Should have added two metadata references"); + } } /// From 8870df3d3d269f33cbbbb2a178f480696f407fd6 Mon Sep 17 00:00:00 2001 From: nick863 Date: Wed, 26 Aug 2026 16:59:03 -0700 Subject: [PATCH 09/21] Fix --- .../PostProcessing/GeneratedCodeWorkspace.cs | 134 +++++--- .../test/GeneratedCodeWorkspaceTests.cs | 315 +++++++++++++++++- 2 files changed, 406 insertions(+), 43 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index aab0e36ba11..612cd50e6bc 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -6,9 +6,8 @@ using System.Diagnostics; using System.IO; using System.Linq; -using System.Reflection; -using System.Runtime.Versioning; using System.Text.Json; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Build.Construction; @@ -21,6 +20,7 @@ using Microsoft.TypeSpec.Generator.SourceInput; using Microsoft.TypeSpec.Generator.Utilities; using NuGet.Configuration; +using NuGet.Frameworks; using NuGet.Versioning; using MSBuildProjectCollection = Microsoft.Build.Evaluation.ProjectCollection; @@ -265,6 +265,93 @@ internal static Project AddDirectory(Project project, string directory, Func> ReadProjectAssetsMayBe() + { + Dictionary> hshFrameworks = []; + // Read in the resolved direct dependencies + DirectoryInfo? directory = (new DirectoryInfo(CodeModelGenerator.Instance.Configuration.OutputDirectory)).Parent?.Parent?.Parent; + if (directory == null) + { + return hshFrameworks; + } + string assetsJson = Path.Combine(directory.FullName, "artifacts", "obj", CodeModelGenerator.Instance.Configuration.PackageName, "project.assets.json"); + if (!File.Exists(assetsJson)) + { + // Try to get file from the project directory. + assetsJson = Path.Combine(CodeModelGenerator.Instance.Configuration.ProjectDirectory, "obj", "project.assets.json"); + } + if (!File.Exists(assetsJson)) + { + return hshFrameworks; + } + Utf8JsonReader reader = new Utf8JsonReader(File.ReadAllBytes(assetsJson)); + using JsonDocument document = JsonDocument.ParseValue(ref reader); + foreach (JsonProperty prop in document.RootElement.EnumerateObject()) + { + if (prop.Value.ValueKind == JsonValueKind.Object && prop.NameEquals("projectFileDependencyGroups")) + { + foreach (JsonProperty targetFramework in prop.Value.EnumerateObject()) + { + NuGetFramework currentFramework = new(targetFramework.Name); + if (!hshFrameworks.ContainsKey(currentFramework.Framework)) + { + hshFrameworks[currentFramework.Framework] = []; + } + if (targetFramework.Value.ValueKind == JsonValueKind.Array) + { + // Parse dependencies. They are structured as SomePackage/package.version + foreach (JsonElement packageAndVersion in targetFramework.Value.EnumerateArray()) + { + if (packageAndVersion.ValueKind == JsonValueKind.String) + { + string[] packageVersionRelation = (packageAndVersion.GetString() ?? "").Split(); + // We only support moreor greater relation. + // Example: "Azure.Core >= 1.62.0" + if (packageVersionRelation.Length == 3 && string.Equals(packageVersionRelation[1], ">=")) + { + hshFrameworks[currentFramework.Framework][packageVersionRelation[0].ToLower()] = packageVersionRelation[2]; + } + } + } + } + } + } + } + return hshFrameworks; + } + + internal static string GetLatestTargetFramework(IEnumerable shortNames) + { + //NuGetFramework? maxFramework = shortNames.Select(x => new NuGetFramework(x)).Max(); + // Assume framework order as follows: + // netstandardX.X, net462, netX.X + double maxFramework=0.0; + string maxFrameworkName = string.Empty; + foreach (string name in shortNames) + { + double current=0.0; + Match numeral = Regex.Match(name, "\\d+[.]*\\d*$"); + if (numeral.Success) + { + current = double.Parse(numeral.Value); + } + if (string.Equals(name, "net462", StringComparison.InvariantCultureIgnoreCase)) + { + current += 1000.0; + } + else if (!name.StartsWith("netstandard", StringComparison.InvariantCultureIgnoreCase)) + { + current += 2000.0; + } + if (current >= maxFramework) + { + maxFramework = current; + maxFrameworkName = name; + } + } + return maxFrameworkName; + } + /// /// Resolves PackageReference items from the project's .csproj file and adds their assemblies /// as metadata references so that custom code referencing external NuGet types compiles correctly. @@ -321,44 +408,13 @@ internal static async Task AddPackageReferencesFromProject() var nugetSettings = Settings.LoadDefaultSettings(projectFilePath); var globalPackagesFolder = SettingsUtility.GetGlobalPackagesFolder(nugetSettings); - // Read in the resolved direct dependencies - DirectoryInfo? directory = (new DirectoryInfo(CodeModelGenerator.Instance.Configuration.OutputDirectory)).Parent?.Parent?.Parent; + // Read in the resolved direct dependencies for all frameworks + Dictionary> hshFrameworks = ReadProjectAssetsMayBe(); + // Get the latestr framework. Dictionary hshNameVersion = []; - string framework = NugetPackageResolver.CurrentFramework.GetShortFolderName(); - if (directory != null) + if (hshFrameworks.Count > 0) { - string assetsJson = Path.Combine(directory.FullName, "artifacts", "obj", CodeModelGenerator.Instance.Configuration.PackageName, "project.assets.json"); - if (!File.Exists(assetsJson)) - { - // Try to get file from the project directory. - assetsJson = Path.Combine(CodeModelGenerator.Instance.Configuration.ProjectDirectory, "obj", "project.assets.json"); - } - if (File.Exists(assetsJson)) - { - Utf8JsonReader reader = new Utf8JsonReader(File.ReadAllBytes(assetsJson)); - using JsonDocument document = JsonDocument.ParseValue(ref reader); - foreach (JsonProperty prop in document.RootElement.EnumerateObject()) - { - if (prop.Value.ValueKind == JsonValueKind.Object && prop.NameEquals("targets")) - { - foreach (JsonProperty targetFramework in prop.Value.EnumerateObject()) - { - if (targetFramework.Value.ValueKind == JsonValueKind.Object && string.Equals(targetFramework.Name, framework, StringComparison.InvariantCultureIgnoreCase)) - { - // Parse dependencies. They are structured as SomePackage/package.version - foreach (JsonProperty packageAndVersion in targetFramework.Value.EnumerateObject()) - { - string[] nameAndVersion = packageAndVersion.Name.Split('/'); - if (nameAndVersion.Length == 2) - { - hshNameVersion[nameAndVersion[0]] = nameAndVersion[1]; - } - } - } - } - } - } - } + hshNameVersion = hshFrameworks[GetLatestTargetFramework(hshFrameworks.Keys.AsEnumerable())]; } // Build a set of assembly names already registered so we can skip them var existingRefs = new HashSet( @@ -385,7 +441,7 @@ internal static async Task AddPackageReferencesFromProject() // Search the NuGet global packages folder for any cached version of this package. string? version = default; - hshNameVersion.TryGetValue(refPackageName, out version); + hshNameVersion.TryGetValue(refPackageName.ToLower(), out version); string? resolvedAssemblyPath = version is null ? NugetPackageResolver.FindPackageAssembly(globalPackagesFolder, refPackageName) : NugetPackageResolver.FindPackageAssemblyInVersion(globalPackagesFolder, refPackageName, version); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs index 1e59acbcdcc..b351e933ae3 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs @@ -2,7 +2,7 @@ // Licensed under the MIT License. using System; -using System.ComponentModel; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -10,6 +10,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.TypeSpec.Generator.Tests.Common; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; using NUnit.Framework; namespace Microsoft.TypeSpec.Generator.Tests @@ -299,9 +300,11 @@ public async Task AddPackageReferencesFromProject_SkipsAlreadyAddedReferences() } [Test] - [TestCase(true)] - [TestCase(false)] - public async Task AddPackageReferencesFromProject_AddsMultiplePackageReferences(bool badPackage) + [TestCase(true, true)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(false, false)] + public async Task AddPackageReferencesFromProject_AddsMultiplePackageReferences(bool badPackage, bool addExtraVersions) { var ns = "TestNamespace"; var nugetCacheDir = Path.Combine(_tempDirectory!, "NuGetCache"); @@ -312,6 +315,12 @@ public async Task AddPackageReferencesFromProject_AddsMultiplePackageReferences( CreateFakeNuGetPackage(nugetCacheDir, "First.Package", "1.0.0"); } CreateFakeNuGetPackage(nugetCacheDir, "Second.Package", "3.5.0"); + if (addExtraVersions) + { + // Add two versions, one newer, one older. + CreateFakeNuGetPackage(nugetCacheDir, "Second.Package", "3.4.0"); + CreateFakeNuGetPackage(nugetCacheDir, "Second.Package", "3.6.0"); + } var csprojContent = @" @@ -336,13 +345,271 @@ public async Task AddPackageReferencesFromProject_AddsMultiplePackageReferences( var refCountBefore = CodeModelGenerator.Instance.AdditionalMetadataReferences.Count; await GeneratedCodeWorkspace.AddPackageReferencesFromProject(); var refCountAfter = CodeModelGenerator.Instance.AdditionalMetadataReferences.Count; + // Extreact versions and packages; make sure there is only one version. + Dictionary packages = []; + // Dislply is a dll path C:\Users\%susername%\AppData\Local\Temp\TestArtifacts\%guid%\NuGetCache\first.package\1.0.0\lib\netstandard2.0\First.Package.dll + // Get just {first.package, 1.0.0, lib, netstandard2.0, First.Package.dll} + // Parse as Tuple: (Name: first.package, Version: 1.0.0, TargetFramework: netstandard2.0) + IEnumerable<(string Name, string Version, string TargetFramework)> resolvedPackages = CodeModelGenerator.Instance.AdditionalMetadataReferences + .Where(x => x.Properties.Kind == MetadataImageKind.Assembly && x.Display is not null && x.Display.Contains("NuGetCache")) + .Select(x => x.Display ?? "") + .Select(x => x.Substring(x.IndexOf("NuGetCache") + "NuGetCache".Length + 1).Split(Path.DirectorySeparatorChar)) + .Where(x => x?.Length == 5) + .Select(x => (Name: x[0], Version: x[1], TargetFramework: x[3])); + foreach (var resolvedPackage in resolvedPackages) + { + Assert.That(resolvedPackage.TargetFramework, Is.EqualTo("netstandard2.0")); + if(packages.TryGetValue(resolvedPackage.Name, out string? version)) + { + Assert.Fail($"Found more then one versions for package {resolvedPackage.Name}: {version} and {resolvedPackage.Version}"); + } + packages[resolvedPackage.Name] = resolvedPackage.Version; + } if (badPackage) { Assert.AreEqual(refCountBefore + 1, refCountAfter, "Should have added one metadata reference as the second one was intentionally broken"); + AssertPackageVersion(packages, "Second.Package", "3.5.0"); + Assert.That(packages, Does.Not.ContainKey("first.package")); } else { Assert.AreEqual(refCountBefore + 2, refCountAfter, "Should have added two metadata references"); + AssertPackageVersion(packages, "First.Package", "1.0.0"); + AssertPackageVersion(packages, "Second.Package", "3.5.0"); + } + } + + [Test] + [TestCase(true, true)] + [TestCase(true, false)] + [TestCase(false, true)] + public async Task TestGetLatestFramework(bool includeGoodVersions, bool includeBadVersions) + { + string[] good = { "net10.0", "net462", "net8.0", "net9.0" }; + string[] bad = { "Michelangelo", "Leonardo", "Raphael", "Donatello" }; + List frameworks = []; + for (int i = 0; i < 4; i++) + { + if (includeGoodVersions) + { + frameworks.Add(good[i]); + } + if (includeBadVersions) + { + frameworks.Add(bad[i]); + } + } + if (includeGoodVersions) + { + Assert.That(GeneratedCodeWorkspace.GetLatestTargetFramework(frameworks), Is.EqualTo("net10.0")); + } + else + { + Assert.That(GeneratedCodeWorkspace.GetLatestTargetFramework(frameworks), Is.EqualTo("Donatello")); + } + } + + private static void AssertPackageVersion(IDictionary resolvedPackages, string package, string version) + { + package = package.ToLower(); + Assert.That(resolvedPackages, Does.ContainKey(package), $"The package {package} was not resolved."); + Assert.That(resolvedPackages[package], Is.EqualTo(version)); + } + + [Test] + [TestCase(new string[]{ "net10.0", "net8.0", "net9.0" }, "net10.0")] + [TestCase(new string[] { "net.10.0", "net.8.0", "net9.0" }, "net.10.0")] + [TestCase(new string[] { "net10.0", "net.8.0", "net9.0" }, "net10.0")] + [TestCase(new string[] { "netstandard2.0", "netstandard1.0", "netstandard3.11", "netstandard3.10" }, "netstandard3.11")] + [TestCase(new string[] { "netstandard.2.0", "netstandard1.0", "netstandard3.11", "netstandard3.10" }, "netstandard3.11")] + [TestCase(new string[] { "netstandard2.0", "netstandard1.0", "netstandard.3.11", "netstandard3.10" }, "netstandard.3.11")] + [TestCase(new string[] { "netstandard2.0", "netstandard1.0", "netstandard3.11", "net462" }, "net462")] + [TestCase(new string[] { "net9.0", "netstandard2.0", "netstandard1.0", "netstandard3.11" }, "net9.0")] + public async Task TestGetLatestFrameworkDifferentNames(string[] frameworks, string expected) + { + Assert.That(GeneratedCodeWorkspace.GetLatestTargetFramework(frameworks), Is.EqualTo(expected)); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public async Task TestReadProjectAssetsMayBe(bool isSdkFramework) + { + var ns = "TestNamespace"; + var nugetCacheDir = Path.Combine(_tempDirectory!, "NuGetCache"); + var csprojContent = @" + + netstandard2.0,net10.0 + + + + 1.0.0 + + + 3.5.0 + + +"; + string minimalProjectAssets = """ + { + "version": 4, + "targets": {}, + "projectFileDependencyGroups": { + "netstandard2.0": [ + "First.Package >= 1.0.0", + "Second.Package >= 3.5.0" + ], + "net462": [ + "First.Package >= 1.0.0", + "Second.Package >= 3.5.0" + ], + "net10.0": [ + "First.Package >= 1.0.0", + "Second.Package >= 3.5.0" + ] + } + } + """; + + string projectDir; + if (isSdkFramework) + { + Assert.That(_tempDirectory, Is.Not.Null.And.Not.Empty); + Directory.CreateDirectory(Path.Combine(_tempDirectory!, "sdk")); + Directory.CreateDirectory(Path.Combine(_tempDirectory!, "sdk", "mysvc")); + projectDir = Path.Combine(_tempDirectory!, "sdk", "mysvc", "ProjectDir"); + Directory.CreateDirectory(projectDir); + Directory.CreateDirectory(Path.Combine(projectDir, "src")); + Directory.CreateDirectory(Path.Combine(_tempDirectory!, "artifacts")); + Directory.CreateDirectory(Path.Combine(_tempDirectory!, "artifacts", "obj")); + Directory.CreateDirectory(Path.Combine(_tempDirectory!, "artifacts", "obj", ns)); + File.WriteAllText(Path.Combine(_tempDirectory!, "artifacts", "obj", ns, "project.assets.json"), minimalProjectAssets); + } + else + { + Assert.That(_projectDir, Is.Not.Null.And.Not.Empty); + projectDir = _projectDir ?? ""; + Directory.CreateDirectory(Path.Combine(projectDir, "src")); + Directory.CreateDirectory(Path.Combine(projectDir, "src", "obj")); + File.WriteAllText(Path.Combine(projectDir, "src", "obj", "project.assets.json"), minimalProjectAssets); + } + File.WriteAllText(Path.Combine(projectDir, "src", $"{ns}.csproj"), csprojContent); + + MockHelpers.LoadMockGenerator( + inputNamespaceName: ns, + outputPath: projectDir, + configuration: $"{{\"package-name\": \"{ns}\"}}"); + Dictionary> dtFrameworks = GeneratedCodeWorkspace.ReadProjectAssetsMayBe(); + Assert.That(dtFrameworks, Has.Count.EqualTo(3)); + foreach (string framework in new string[]{ "netstandard2.0", "net10.0", "net462" }) + { + if(dtFrameworks.TryGetValue(framework, out Dictionary? dtPackages)) + { + Assert.That(dtPackages, Has.Count.EqualTo(2)); + AssertPackageVersion(dtPackages, "First.Package", "1.0.0"); + AssertPackageVersion(dtPackages, "Second.Package", "3.5.0"); + } + else + { + Assert.Fail($"No information on Framework {framework} was found."); + } + } + } + + [Test] + public async Task TestReadProjectAssetsMayBeNoFile() + { + var ns = "TestNamespace"; + var nugetCacheDir = Path.Combine(_tempDirectory!, "NuGetCache"); + var csprojContent = @" + + netstandard2.0,net10.0 + + + + 1.0.0 + + + 3.5.0 + + +"; + File.WriteAllText(Path.Combine(_projectDir!, "src", $"{ns}.csproj"), csprojContent); + + MockHelpers.LoadMockGenerator( + inputNamespaceName: ns, + outputPath: _projectDir, + configuration: $"{{\"package-name\": \"{ns}\"}}"); + Dictionary> dtFrameworks = GeneratedCodeWorkspace.ReadProjectAssetsMayBe(); + Assert.That(dtFrameworks, Has.Count.EqualTo(0)); + } + + [Test] + public async Task TestReadProjectAssetsMayBeUnsupportedPackageNames() + { + var ns = "TestNamespace"; + var nugetCacheDir = Path.Combine(_tempDirectory!, "NuGetCache"); + var csprojContent = @" + + netstandard2.0,net10.0 + + + + 1.0.0 + + + 3.5.0 + + +"; + File.WriteAllText(Path.Combine(_projectDir!, "src", $"{ns}.csproj"), csprojContent); + string minimalProjectAssets = """ + { + "version": 4, + "targets": {}, + "projectFileDependencyGroups": { + "netstandard2.0": [ + "First.Package >= 1.0.0", + "Second.Package" + ], + "net462": [ + "First.Package >= 1.1.0", + "Second.Package == 3.5.0" + ], + "net10.0": [ + "First.Package >= 1.2.0", + "Second.Package < 3.5.0" + ] + } + } + """; + Directory.CreateDirectory(Path.Combine(_projectDir!, "src", "obj")); + File.WriteAllText(Path.Combine(_projectDir!, "src", "obj", "project.assets.json"), minimalProjectAssets); + + MockHelpers.LoadMockGenerator( + inputNamespaceName: ns, + outputPath: _projectDir, + configuration: $"{{\"package-name\": \"{ns}\"}}"); + Dictionary> dtFrameworks = GeneratedCodeWorkspace.ReadProjectAssetsMayBe(); + Assert.That(dtFrameworks, Has.Count.EqualTo(3)); + foreach (string framework in new string[] { "netstandard2.0", "net10.0", "net462" }) + { + string version = framework switch + { + "netstandard2.0" => "1.0.0", + "net462" => "1.1.0", + "net10.0" => "1.2.0", + _ => throw new InvalidOperationException($"Invalid value {framework}") + }; + if (dtFrameworks.TryGetValue(framework, out Dictionary? dtPackages)) + { + Assert.That(dtPackages, Has.Count.EqualTo(1)); + AssertPackageVersion(dtPackages, "First.Package", version); + } + else + { + Assert.Fail($"No information on Framework {framework} was found."); + } } } @@ -368,6 +635,46 @@ public class Placeholder {{ }} var dllPath = Path.Combine(pkgDir, $"{packageName}.dll"); var result = compilation.Emit(dllPath); + string metadataPath = Path.Combine(nugetCacheDir, packageName.ToLowerInvariant(), version); + File.WriteAllText(Path.Combine(metadataPath, ".nupkg.metadata"), + """ + { + "version": 2, + "contentHash": "OPrzAveg9k9KMJ4PmDoWCNlNRDiwpFsTJoo2gRWtO4RnJ9DrJ/7NOSLkNmXWORewNDc+2WVcbEhFJ8exdIzA8A==", + "source": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/package/nuget/v3/index.json" + } + """ + ); + // + File.WriteAllText(Path.Combine(metadataPath, $"{packageName}.nuspec"), $""" + + + + {packageName} + {version} + Microsoft + true + MIT + https://licenses.nuget.org/MIT + azureicon.png + README.md + Test + © Microsoft Corporation. All rights reserved. + {packageName} + + + + + + + + + + + + + + """); Assert.IsTrue(result.Success, $"Failed to emit fake assembly for {packageName}"); return dllPath; } From b15a23ee2d3ee3dbf513a278feacf3b55e001503 Mon Sep 17 00:00:00 2001 From: nick863 Date: Wed, 26 Aug 2026 17:56:47 -0700 Subject: [PATCH 10/21] Fix --- .../PostProcessing/GeneratedCodeWorkspace.cs | 34 +------ .../test/GeneratedCodeWorkspaceTests.cs | 91 ++++++++++--------- 2 files changed, 53 insertions(+), 72 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 612cd50e6bc..519e945f1fa 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -305,7 +305,7 @@ internal static Dictionary> ReadProjectAssets if (packageAndVersion.ValueKind == JsonValueKind.String) { string[] packageVersionRelation = (packageAndVersion.GetString() ?? "").Split(); - // We only support moreor greater relation. + // We only support the greater-than-or-equal relation. // Example: "Azure.Core >= 1.62.0" if (packageVersionRelation.Length == 3 && string.Equals(packageVersionRelation[1], ">=")) { @@ -322,34 +322,8 @@ internal static Dictionary> ReadProjectAssets internal static string GetLatestTargetFramework(IEnumerable shortNames) { - //NuGetFramework? maxFramework = shortNames.Select(x => new NuGetFramework(x)).Max(); - // Assume framework order as follows: - // netstandardX.X, net462, netX.X - double maxFramework=0.0; - string maxFrameworkName = string.Empty; - foreach (string name in shortNames) - { - double current=0.0; - Match numeral = Regex.Match(name, "\\d+[.]*\\d*$"); - if (numeral.Success) - { - current = double.Parse(numeral.Value); - } - if (string.Equals(name, "net462", StringComparison.InvariantCultureIgnoreCase)) - { - current += 1000.0; - } - else if (!name.StartsWith("netstandard", StringComparison.InvariantCultureIgnoreCase)) - { - current += 2000.0; - } - if (current >= maxFramework) - { - maxFramework = current; - maxFrameworkName = name; - } - } - return maxFrameworkName; + NuGetFramework? maxFramework = shortNames.Select(x => new NuGetFramework(x)).MaxBy(x => x.Version); + return maxFramework?.Framework ?? string.Empty; } /// @@ -410,7 +384,7 @@ internal static async Task AddPackageReferencesFromProject() // Read in the resolved direct dependencies for all frameworks Dictionary> hshFrameworks = ReadProjectAssetsMayBe(); - // Get the latestr framework. + // Get the latest framework. Dictionary hshNameVersion = []; if (hshFrameworks.Count > 0) { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs index b351e933ae3..d34301f6660 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs @@ -10,7 +10,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.TypeSpec.Generator.Tests.Common; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; using NUnit.Framework; namespace Microsoft.TypeSpec.Generator.Tests @@ -149,6 +148,46 @@ public class ExternalCredential { } var externalDllPath = Path.Combine(externalPkgDir, $"{externalPkgName}.dll"); var emitResult = externalCompilation.Emit(externalDllPath); Assert.IsTrue(emitResult.Success, "Failed to emit external test assembly"); + string metadataPath = Path.Combine(nugetCacheDir, externalPkgName.ToLowerInvariant(), externalPkgVersion); + File.WriteAllText(Path.Combine(metadataPath, ".nupkg.metadata"), + """ + { + "version": 2, + "contentHash": "OPrzAveg9k9KMJ4PmDoWCNlNRDiwpFsTJoo2gRWtO4RnJ9DrJ/7NOSLkNmXWORewNDc+2WVcbEhFJ8exdIzA8A==", + "source": "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/package/nuget/v3/index.json" + } + """ + ); + // + File.WriteAllText(Path.Combine(metadataPath, $"{externalPkgName}.nuspec"), $""" + + + + {externalPkgName} + {externalPkgVersion} + Microsoft + true + MIT + https://licenses.nuget.org/MIT + azureicon.png + README.md + Test + © Microsoft Corporation. All rights reserved. + {externalPkgName} + + + + + + + + + + + + + + """); // Create a .csproj with a PackageReference to the external package var csprojContent = $@" @@ -345,7 +384,7 @@ public async Task AddPackageReferencesFromProject_AddsMultiplePackageReferences( var refCountBefore = CodeModelGenerator.Instance.AdditionalMetadataReferences.Count; await GeneratedCodeWorkspace.AddPackageReferencesFromProject(); var refCountAfter = CodeModelGenerator.Instance.AdditionalMetadataReferences.Count; - // Extreact versions and packages; make sure there is only one version. + // Extract versions and packages; make sure there is only one version. Dictionary packages = []; // Dislply is a dll path C:\Users\%susername%\AppData\Local\Temp\TestArtifacts\%guid%\NuGetCache\first.package\1.0.0\lib\netstandard2.0\First.Package.dll // Get just {first.package, 1.0.0, lib, netstandard2.0, First.Package.dll} @@ -361,7 +400,7 @@ public async Task AddPackageReferencesFromProject_AddsMultiplePackageReferences( Assert.That(resolvedPackage.TargetFramework, Is.EqualTo("netstandard2.0")); if(packages.TryGetValue(resolvedPackage.Name, out string? version)) { - Assert.Fail($"Found more then one versions for package {resolvedPackage.Name}: {version} and {resolvedPackage.Version}"); + Assert.Fail($"Found more than one versions for package {resolvedPackage.Name}: {version} and {resolvedPackage.Version}"); } packages[resolvedPackage.Name] = resolvedPackage.Version; } @@ -379,36 +418,6 @@ public async Task AddPackageReferencesFromProject_AddsMultiplePackageReferences( } } - [Test] - [TestCase(true, true)] - [TestCase(true, false)] - [TestCase(false, true)] - public async Task TestGetLatestFramework(bool includeGoodVersions, bool includeBadVersions) - { - string[] good = { "net10.0", "net462", "net8.0", "net9.0" }; - string[] bad = { "Michelangelo", "Leonardo", "Raphael", "Donatello" }; - List frameworks = []; - for (int i = 0; i < 4; i++) - { - if (includeGoodVersions) - { - frameworks.Add(good[i]); - } - if (includeBadVersions) - { - frameworks.Add(bad[i]); - } - } - if (includeGoodVersions) - { - Assert.That(GeneratedCodeWorkspace.GetLatestTargetFramework(frameworks), Is.EqualTo("net10.0")); - } - else - { - Assert.That(GeneratedCodeWorkspace.GetLatestTargetFramework(frameworks), Is.EqualTo("Donatello")); - } - } - private static void AssertPackageVersion(IDictionary resolvedPackages, string package, string version) { package = package.ToLower(); @@ -417,15 +426,14 @@ private static void AssertPackageVersion(IDictionary resolvedPac } [Test] + [TestCase(new string[] { "net10.0", "net462", "net8.0", "net9.0" }, "net10.0")] + [TestCase(new string[] { "net10.0", "net8.0", "net9.0" }, "net10.0")] [TestCase(new string[]{ "net10.0", "net8.0", "net9.0" }, "net10.0")] [TestCase(new string[] { "net.10.0", "net.8.0", "net9.0" }, "net.10.0")] [TestCase(new string[] { "net10.0", "net.8.0", "net9.0" }, "net10.0")] - [TestCase(new string[] { "netstandard2.0", "netstandard1.0", "netstandard3.11", "netstandard3.10" }, "netstandard3.11")] - [TestCase(new string[] { "netstandard.2.0", "netstandard1.0", "netstandard3.11", "netstandard3.10" }, "netstandard3.11")] - [TestCase(new string[] { "netstandard2.0", "netstandard1.0", "netstandard.3.11", "netstandard3.10" }, "netstandard.3.11")] - [TestCase(new string[] { "netstandard2.0", "netstandard1.0", "netstandard3.11", "net462" }, "net462")] + [TestCase(new string[] { "netstandard2.0", "netstandard1.0", "netstandard3.11", "net462" }, "netstandard2.0")] [TestCase(new string[] { "net9.0", "netstandard2.0", "netstandard1.0", "netstandard3.11" }, "net9.0")] - public async Task TestGetLatestFrameworkDifferentNames(string[] frameworks, string expected) + public void TestGetLatestFrameworkDifferentNames(string[] frameworks, string expected) { Assert.That(GeneratedCodeWorkspace.GetLatestTargetFramework(frameworks), Is.EqualTo(expected)); } @@ -433,7 +441,7 @@ public async Task TestGetLatestFrameworkDifferentNames(string[] frameworks, stri [Test] [TestCase(true)] [TestCase(false)] - public async Task TestReadProjectAssetsMayBe(bool isSdkFramework) + public void TestReadProjectAssetsMayBe(bool isSdkFramework) { var ns = "TestNamespace"; var nugetCacheDir = Path.Combine(_tempDirectory!, "NuGetCache"); @@ -517,7 +525,7 @@ public async Task TestReadProjectAssetsMayBe(bool isSdkFramework) } [Test] - public async Task TestReadProjectAssetsMayBeNoFile() + public void TestReadProjectAssetsMayBeNoFile() { var ns = "TestNamespace"; var nugetCacheDir = Path.Combine(_tempDirectory!, "NuGetCache"); @@ -545,10 +553,9 @@ public async Task TestReadProjectAssetsMayBeNoFile() } [Test] - public async Task TestReadProjectAssetsMayBeUnsupportedPackageNames() + public void TestReadProjectAssetsMayBeUnsupportedPackageNames() { var ns = "TestNamespace"; - var nugetCacheDir = Path.Combine(_tempDirectory!, "NuGetCache"); var csprojContent = @" netstandard2.0,net10.0 From def35054589d502e4ca3a043e3b0c95f8d4ea025 Mon Sep 17 00:00:00 2001 From: nick863 Date: Wed, 26 Aug 2026 18:01:10 -0700 Subject: [PATCH 11/21] Fix typos --- .../test/GeneratedCodeWorkspaceTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs index d34301f6660..19424d34ea0 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs @@ -386,7 +386,7 @@ public async Task AddPackageReferencesFromProject_AddsMultiplePackageReferences( var refCountAfter = CodeModelGenerator.Instance.AdditionalMetadataReferences.Count; // Extract versions and packages; make sure there is only one version. Dictionary packages = []; - // Dislply is a dll path C:\Users\%susername%\AppData\Local\Temp\TestArtifacts\%guid%\NuGetCache\first.package\1.0.0\lib\netstandard2.0\First.Package.dll + // Dislplay is a dll path C:\Users\%username%\AppData\Local\Temp\TestArtifacts\%guid%\NuGetCache\first.package\1.0.0\lib\netstandard2.0\First.Package.dll // Get just {first.package, 1.0.0, lib, netstandard2.0, First.Package.dll} // Parse as Tuple: (Name: first.package, Version: 1.0.0, TargetFramework: netstandard2.0) IEnumerable<(string Name, string Version, string TargetFramework)> resolvedPackages = CodeModelGenerator.Instance.AdditionalMetadataReferences @@ -484,8 +484,8 @@ public void TestReadProjectAssetsMayBe(bool isSdkFramework) { Assert.That(_tempDirectory, Is.Not.Null.And.Not.Empty); Directory.CreateDirectory(Path.Combine(_tempDirectory!, "sdk")); - Directory.CreateDirectory(Path.Combine(_tempDirectory!, "sdk", "mysvc")); - projectDir = Path.Combine(_tempDirectory!, "sdk", "mysvc", "ProjectDir"); + Directory.CreateDirectory(Path.Combine(_tempDirectory!, "sdk", "my_service")); + projectDir = Path.Combine(_tempDirectory!, "sdk", "my_service", "ProjectDir"); Directory.CreateDirectory(projectDir); Directory.CreateDirectory(Path.Combine(projectDir, "src")); Directory.CreateDirectory(Path.Combine(_tempDirectory!, "artifacts")); From 4f51f963431fc67ae7c5a7f9e8387a9d2c5b7d40 Mon Sep 17 00:00:00 2001 From: nick863 <30440255+nick863@users.noreply.github.com> Date: Thu, 27 Aug 2026 10:33:01 -0700 Subject: [PATCH 12/21] Fix --- .../PostProcessing/GeneratedCodeWorkspace.cs | 34 +++++++++++++-- .../test/GeneratedCodeWorkspaceTests.cs | 42 ++++++++++++++++--- 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 519e945f1fa..323bac6d6dd 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -322,8 +322,36 @@ internal static Dictionary> ReadProjectAssets internal static string GetLatestTargetFramework(IEnumerable shortNames) { - NuGetFramework? maxFramework = shortNames.Select(x => new NuGetFramework(x)).MaxBy(x => x.Version); - return maxFramework?.Framework ?? string.Empty; + // Assume framework order as follows: + // netstandardX.X, net462, netX.X + // Q: Why not to use NuGetFramework object here? + // A: Because it does not parse/recognize version and under the hood tries to compare Versions, which are all 0.0.0. + double maxFramework = 0.0; + string maxFrameworkName = string.Empty; + foreach (string name in shortNames) + { + double current = 0.0; + Match numeral = Regex.Match(name, "\\d+[.]*\\d*$"); + if (numeral.Success) + { + current = double.Parse(numeral.Value); + } + if (name.StartsWith("net4", StringComparison.InvariantCultureIgnoreCase)) + { + current /= 100; + current += 2000.0; + } + else if (!name.StartsWith("netstandard", StringComparison.InvariantCultureIgnoreCase)) + { + current += 2000.0; + } + if (current >= maxFramework) + { + maxFramework = current; + maxFrameworkName = name; + } + } + return maxFrameworkName; } /// @@ -422,7 +450,7 @@ internal static async Task AddPackageReferencesFromProject() if (resolvedAssemblyPath == null) { CodeModelGenerator.Instance.Emitter.Debug( - $"The package {refPackageName}{(version != null ? "v. "+ version : "")} was not restored."); + $"The package {refPackageName}{(version != null ? " v. "+ version : "")} was not restored."); } else if (version is null) { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs index 19424d34ea0..a9c37ddda8e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs @@ -386,7 +386,7 @@ public async Task AddPackageReferencesFromProject_AddsMultiplePackageReferences( var refCountAfter = CodeModelGenerator.Instance.AdditionalMetadataReferences.Count; // Extract versions and packages; make sure there is only one version. Dictionary packages = []; - // Dislplay is a dll path C:\Users\%username%\AppData\Local\Temp\TestArtifacts\%guid%\NuGetCache\first.package\1.0.0\lib\netstandard2.0\First.Package.dll + // Display is a dll path C:\Users\%username%\AppData\Local\Temp\TestArtifacts\%guid%\NuGetCache\first.package\1.0.0\lib\netstandard2.0\First.Package.dll // Get just {first.package, 1.0.0, lib, netstandard2.0, First.Package.dll} // Parse as Tuple: (Name: first.package, Version: 1.0.0, TargetFramework: netstandard2.0) IEnumerable<(string Name, string Version, string TargetFramework)> resolvedPackages = CodeModelGenerator.Instance.AdditionalMetadataReferences @@ -400,7 +400,7 @@ public async Task AddPackageReferencesFromProject_AddsMultiplePackageReferences( Assert.That(resolvedPackage.TargetFramework, Is.EqualTo("netstandard2.0")); if(packages.TryGetValue(resolvedPackage.Name, out string? version)) { - Assert.Fail($"Found more than one versions for package {resolvedPackage.Name}: {version} and {resolvedPackage.Version}"); + Assert.Fail($"Found more than one version for package {resolvedPackage.Name}: {version} and {resolvedPackage.Version}"); } packages[resolvedPackage.Name] = resolvedPackage.Version; } @@ -426,13 +426,45 @@ private static void AssertPackageVersion(IDictionary resolvedPac } [Test] - [TestCase(new string[] { "net10.0", "net462", "net8.0", "net9.0" }, "net10.0")] + [TestCase(true, true)] + [TestCase(true, false)] + [TestCase(false, true)] + public async Task TestGetLatestFramework(bool includeGoodVersions, bool includeBadVersions) + { + string[] good = { "net10.0", "net462", "net8.0", "net9.0" }; + string[] bad = { "Michelangelo", "Leonardo", "Raphael", "Donatello" }; + List frameworks = []; + for (int i = 0; i < 4; i++) + { + if (includeGoodVersions) + { + frameworks.Add(good[i]); + } + if (includeBadVersions) + { + frameworks.Add(bad[i]); + } + } + if (includeGoodVersions) + { + Assert.That(GeneratedCodeWorkspace.GetLatestTargetFramework(frameworks), Is.EqualTo("net10.0")); + } + else + { + Assert.That(GeneratedCodeWorkspace.GetLatestTargetFramework(frameworks), Is.EqualTo("Donatello")); + } + } + + [Test] [TestCase(new string[] { "net10.0", "net8.0", "net9.0" }, "net10.0")] - [TestCase(new string[]{ "net10.0", "net8.0", "net9.0" }, "net10.0")] [TestCase(new string[] { "net.10.0", "net.8.0", "net9.0" }, "net.10.0")] [TestCase(new string[] { "net10.0", "net.8.0", "net9.0" }, "net10.0")] - [TestCase(new string[] { "netstandard2.0", "netstandard1.0", "netstandard3.11", "net462" }, "netstandard2.0")] + [TestCase(new string[] { "netstandard2.0", "netstandard1.0", "netstandard3.11", "netstandard3.10" }, "netstandard3.11")] + [TestCase(new string[] { "netstandard.2.0", "netstandard1.0", "netstandard3.11", "netstandard3.10" }, "netstandard3.11")] + [TestCase(new string[] { "netstandard2.0", "netstandard1.0", "netstandard.3.11", "netstandard3.10" }, "netstandard.3.11")] + [TestCase(new string[] { "netstandard2.0", "netstandard1.0", "netstandard3.11", "net462" }, "net462")] [TestCase(new string[] { "net9.0", "netstandard2.0", "netstandard1.0", "netstandard3.11" }, "net9.0")] + [TestCase(new string[] { "netstandard2.0", "net472", "net462" }, "net472")] public void TestGetLatestFrameworkDifferentNames(string[] frameworks, string expected) { Assert.That(GeneratedCodeWorkspace.GetLatestTargetFramework(frameworks), Is.EqualTo(expected)); From 590450eced800e49f15bdd45f1315e740f5de432 Mon Sep 17 00:00:00 2001 From: nick863 <30440255+nick863@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:18:25 -0700 Subject: [PATCH 13/21] Fix --- .../src/Utilities/ExternalTypeReferenceResolver.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/ExternalTypeReferenceResolver.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/ExternalTypeReferenceResolver.cs index 79b4ac32cdb..c4ca62c6b94 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/ExternalTypeReferenceResolver.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/ExternalTypeReferenceResolver.cs @@ -17,8 +17,8 @@ namespace Microsoft.TypeSpec.Generator.Utilities { /// /// Resolves entries to instances by - /// looking up the package in the NuGet global cache (or downloading it from configured feeds when - /// missing) and loading the assembly via reflection. Used by TypeFactory.CreateExternalType + /// looking up the package in the NuGet global cache and loading the assembly via reflection. + /// Used by TypeFactory.CreateExternalType /// as a fallback after CreateFrameworkType returns null. /// /// From 73fa0c039ca162158ce5b93dcd2ae5385ffad048 Mon Sep 17 00:00:00 2001 From: nick863 <30440255+nick863@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:32:18 -0700 Subject: [PATCH 14/21] Fix --- .../test/GeneratedCodeWorkspaceTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs index a9c37ddda8e..9099a58a7a9 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs @@ -159,7 +159,7 @@ public class ExternalCredential { } """ ); // - File.WriteAllText(Path.Combine(metadataPath, $"{externalPkgName}.nuspec"), $""" + File.WriteAllText(Path.Combine(metadataPath, $"{externalPkgName.ToLowerInvariant()}.nuspec"), $""" @@ -429,7 +429,7 @@ private static void AssertPackageVersion(IDictionary resolvedPac [TestCase(true, true)] [TestCase(true, false)] [TestCase(false, true)] - public async Task TestGetLatestFramework(bool includeGoodVersions, bool includeBadVersions) + public void TestGetLatestFramework(bool includeGoodVersions, bool includeBadVersions) { string[] good = { "net10.0", "net462", "net8.0", "net9.0" }; string[] bad = { "Michelangelo", "Leonardo", "Raphael", "Donatello" }; From 65297653e16c4bee26247323a0dfb81ab6b87798 Mon Sep 17 00:00:00 2001 From: nick863 <30440255+nick863@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:39:56 -0700 Subject: [PATCH 15/21] Fix --- .../test/GeneratedCodeWorkspaceTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs index 9099a58a7a9..021fbc8102a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs @@ -560,7 +560,6 @@ public void TestReadProjectAssetsMayBe(bool isSdkFramework) public void TestReadProjectAssetsMayBeNoFile() { var ns = "TestNamespace"; - var nugetCacheDir = Path.Combine(_tempDirectory!, "NuGetCache"); var csprojContent = @" netstandard2.0,net10.0 From de8d56da40102adf9d88a365e285ead1f6ca0f7d Mon Sep 17 00:00:00 2001 From: nick863 <30440255+nick863@users.noreply.github.com> Date: Thu, 27 Aug 2026 18:44:35 -0700 Subject: [PATCH 16/21] Fix --- .../src/PostProcessing/GeneratedCodeWorkspace.cs | 8 ++++---- .../test/GeneratedCodeWorkspaceTests.cs | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 323bac6d6dd..138b4fb6615 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -265,7 +265,7 @@ internal static Project AddDirectory(Project project, string directory, Func> ReadProjectAssetsMayBe() + internal static async Task>> ReadProjectAssets() { Dictionary> hshFrameworks = []; // Read in the resolved direct dependencies @@ -284,7 +284,7 @@ internal static Dictionary> ReadProjectAssets { return hshFrameworks; } - Utf8JsonReader reader = new Utf8JsonReader(File.ReadAllBytes(assetsJson)); + Utf8JsonReader reader = new Utf8JsonReader(await File.ReadAllBytesAsync(assetsJson)); using JsonDocument document = JsonDocument.ParseValue(ref reader); foreach (JsonProperty prop in document.RootElement.EnumerateObject()) { @@ -306,7 +306,7 @@ internal static Dictionary> ReadProjectAssets { string[] packageVersionRelation = (packageAndVersion.GetString() ?? "").Split(); // We only support the greater-than-or-equal relation. - // Example: "Azure.Core >= 1.62.0" + // Example: "My.Package >= 1.1.1" if (packageVersionRelation.Length == 3 && string.Equals(packageVersionRelation[1], ">=")) { hshFrameworks[currentFramework.Framework][packageVersionRelation[0].ToLower()] = packageVersionRelation[2]; @@ -411,7 +411,7 @@ internal static async Task AddPackageReferencesFromProject() var globalPackagesFolder = SettingsUtility.GetGlobalPackagesFolder(nugetSettings); // Read in the resolved direct dependencies for all frameworks - Dictionary> hshFrameworks = ReadProjectAssetsMayBe(); + Dictionary> hshFrameworks = await ReadProjectAssets(); // Get the latest framework. Dictionary hshNameVersion = []; if (hshFrameworks.Count > 0) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs index 021fbc8102a..7bcf25792af 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs @@ -473,7 +473,7 @@ public void TestGetLatestFrameworkDifferentNames(string[] frameworks, string exp [Test] [TestCase(true)] [TestCase(false)] - public void TestReadProjectAssetsMayBe(bool isSdkFramework) + public async Task TestReadProjectAssets(bool isSdkFramework) { var ns = "TestNamespace"; var nugetCacheDir = Path.Combine(_tempDirectory!, "NuGetCache"); @@ -539,7 +539,7 @@ public void TestReadProjectAssetsMayBe(bool isSdkFramework) inputNamespaceName: ns, outputPath: projectDir, configuration: $"{{\"package-name\": \"{ns}\"}}"); - Dictionary> dtFrameworks = GeneratedCodeWorkspace.ReadProjectAssetsMayBe(); + Dictionary> dtFrameworks = await GeneratedCodeWorkspace.ReadProjectAssets(); Assert.That(dtFrameworks, Has.Count.EqualTo(3)); foreach (string framework in new string[]{ "netstandard2.0", "net10.0", "net462" }) { @@ -557,7 +557,7 @@ public void TestReadProjectAssetsMayBe(bool isSdkFramework) } [Test] - public void TestReadProjectAssetsMayBeNoFile() + public async Task TestReadProjectAssetsNoFile() { var ns = "TestNamespace"; var csprojContent = @" @@ -579,12 +579,12 @@ public void TestReadProjectAssetsMayBeNoFile() inputNamespaceName: ns, outputPath: _projectDir, configuration: $"{{\"package-name\": \"{ns}\"}}"); - Dictionary> dtFrameworks = GeneratedCodeWorkspace.ReadProjectAssetsMayBe(); + Dictionary> dtFrameworks = await GeneratedCodeWorkspace.ReadProjectAssets(); Assert.That(dtFrameworks, Has.Count.EqualTo(0)); } [Test] - public void TestReadProjectAssetsMayBeUnsupportedPackageNames() + public async Task TestReadProjectAssetsUnsupportedPackageNames() { var ns = "TestNamespace"; var csprojContent = @" @@ -628,7 +628,7 @@ public void TestReadProjectAssetsMayBeUnsupportedPackageNames() inputNamespaceName: ns, outputPath: _projectDir, configuration: $"{{\"package-name\": \"{ns}\"}}"); - Dictionary> dtFrameworks = GeneratedCodeWorkspace.ReadProjectAssetsMayBe(); + Dictionary> dtFrameworks = await GeneratedCodeWorkspace.ReadProjectAssets(); Assert.That(dtFrameworks, Has.Count.EqualTo(3)); foreach (string framework in new string[] { "netstandard2.0", "net10.0", "net462" }) { From 3f5e7e2522d29e597bde94a8fef9ec9b5dd3f684 Mon Sep 17 00:00:00 2001 From: nick863 Date: Fri, 28 Aug 2026 12:04:10 -0700 Subject: [PATCH 17/21] Fix --- .../PostProcessing/GeneratedCodeWorkspace.cs | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 138b4fb6615..14da8785d9d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -268,21 +268,24 @@ internal static Project AddDirectory(Project project, string directory, Func>> ReadProjectAssets() { Dictionary> hshFrameworks = []; - // Read in the resolved direct dependencies - DirectoryInfo? directory = (new DirectoryInfo(CodeModelGenerator.Instance.Configuration.OutputDirectory)).Parent?.Parent?.Parent; - if (directory == null) - { - return hshFrameworks; - } - string assetsJson = Path.Combine(directory.FullName, "artifacts", "obj", CodeModelGenerator.Instance.Configuration.PackageName, "project.assets.json"); - if (!File.Exists(assetsJson)) - { - // Try to get file from the project directory. - assetsJson = Path.Combine(CodeModelGenerator.Instance.Configuration.ProjectDirectory, "obj", "project.assets.json"); - } + + // Read in the resolved direct dependencies. + // We first try the default location of project.assets.json, which is %project_dir%/obj/. + string assetsJson = Path.Combine(CodeModelGenerator.Instance.Configuration.ProjectDirectory, "obj", "project.assets.json"); if (!File.Exists(assetsJson)) { - return hshFrameworks; + // If it does not exists, try the artifacts/obj/%project_name%/ three directoreies above projects directory. + // If this directory does not extists or does not contain artifacts/obj/%project_name%/roject.assets.json, give up. + DirectoryInfo? directory = (new DirectoryInfo(CodeModelGenerator.Instance.Configuration.OutputDirectory)).Parent?.Parent?.Parent; + if (directory == null) + { + return hshFrameworks; + } + assetsJson = Path.Combine(directory.FullName, "artifacts", "obj", CodeModelGenerator.Instance.Configuration.PackageName, "project.assets.json"); + if (!File.Exists(assetsJson)) + { + return hshFrameworks; + } } Utf8JsonReader reader = new Utf8JsonReader(await File.ReadAllBytesAsync(assetsJson)); using JsonDocument document = JsonDocument.ParseValue(ref reader); From 72e7d5e43ea0ad26022c89b4621580d7cbce10ab Mon Sep 17 00:00:00 2001 From: nick863 Date: Mon, 31 Aug 2026 11:40:45 -0700 Subject: [PATCH 18/21] Fix --- .../PostProcessing/GeneratedCodeWorkspace.cs | 76 +++++++++++++++++-- .../test/GeneratedCodeWorkspaceTests.cs | 56 +++++++++++++- 2 files changed, 123 insertions(+), 9 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index 14da8785d9d..d2fa7b970d0 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -270,18 +270,21 @@ internal static async Task>> ReadP Dictionary> hshFrameworks = []; // Read in the resolved direct dependencies. + // We first try the default location of project.assets.json, which is %project_dir%/obj/. string assetsJson = Path.Combine(CodeModelGenerator.Instance.Configuration.ProjectDirectory, "obj", "project.assets.json"); if (!File.Exists(assetsJson)) { // If it does not exists, try the artifacts/obj/%project_name%/ three directoreies above projects directory. // If this directory does not extists or does not contain artifacts/obj/%project_name%/roject.assets.json, give up. - DirectoryInfo? directory = (new DirectoryInfo(CodeModelGenerator.Instance.Configuration.OutputDirectory)).Parent?.Parent?.Parent; - if (directory == null) - { - return hshFrameworks; - } - assetsJson = Path.Combine(directory.FullName, "artifacts", "obj", CodeModelGenerator.Instance.Configuration.PackageName, "project.assets.json"); + //DirectoryInfo? directory = (new DirectoryInfo(CodeModelGenerator.Instance.Configuration.OutputDirectory)).Parent?.Parent?.Parent; + //if (directory == null) + //{ + // return hshFrameworks; + //} + //assetsJson = Path.Combine(directory.FullName, "artifacts", "obj", CodeModelGenerator.Instance.Configuration.PackageName, "project.assets.json"); + string objPath = await TryGetObjectPath() ?? ""; + assetsJson = Path.Combine(objPath, "project.assets.json"); if (!File.Exists(assetsJson)) { return hshFrameworks; @@ -323,6 +326,65 @@ internal static async Task>> ReadP return hshFrameworks; } + internal static async Task TryGetObjectPath() + { + string projectFilePath = Path.GetFullPath( + Path.Combine(CodeModelGenerator.Instance.Configuration.ProjectDirectory, $"{CodeModelGenerator.Instance.Configuration.PackageName}.csproj")); + Process restore = new(); + ProcessStartInfo info = new() + { + UseShellExecute = false, + WindowStyle = ProcessWindowStyle.Hidden, + FileName = "dotnet", + ArgumentList = { "msbuild", projectFilePath, "-getProperty:OutputPath" }, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + restore.StartInfo = info; + string? output = default; + if (restore.Start()) + { + Task outputTask = restore.StandardOutput.ReadToEndAsync(); + Task errorTask = restore.StandardError.ReadToEndAsync(); + await restore.WaitForExitAsync(); + output = await outputTask; + string error = await errorTask; + if (restore.ExitCode != 0) + { + CodeModelGenerator.Instance.Emitter.ReportDiagnostic( + code: "unable-to-get-artifact-path", + message: $"The dotnet msbuild {projectFilePath} -getProperty:OutputPath command exited with {restore.ExitCode}.\n" + + $"Standard output: {output}\n" + + $"Error output: {error}", + severity: EmitterRpc.EmitterDiagnosticSeverity.Warning + ); + } + } + Regex regBin = Path.DirectorySeparatorChar == '\\' ? new(@"(^bin\\)|(\\bin\\)") : new($"(^bin{Path.DirectorySeparatorChar})|({Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar})"); + if (string.IsNullOrEmpty(output) || !regBin.IsMatch(output)) + { + return null; + } + output = output.Trim(['\n', '\r', '\t', ' ']); + string binProjPath = Path.Combine("bin", CodeModelGenerator.Instance.Configuration.PackageName); + if (output.Contains(binProjPath)) + { + // Special case when we have the absolute path to the atrifacts. In this case the obj folder contains subfolder named with ptroject name. + string objpath = Path.Combine("obj", CodeModelGenerator.Instance.Configuration.PackageName); + output = output.ReplaceLast(output.Substring(output.LastIndexOf(binProjPath)), objpath); + } + else + { + output = output.ReplaceLast(output.Substring(output.LastIndexOf("bin")), "obj"); + } + // Handle relative paths + if (!Path.IsPathFullyQualified(output)) + { + output = Path.Combine(Path.GetDirectoryName(projectFilePath) ?? "", output); + } + return Directory.Exists(output) ? output : null; + } + internal static string GetLatestTargetFramework(IEnumerable shortNames) { // Assume framework order as follows: @@ -397,7 +459,7 @@ internal static async Task AddPackageReferencesFromProject() message: $"The dotnet restore {projectFilePath} command exited with {restore.ExitCode}.\n" + $"Standard output: {output}\n" + $"Error output: {error}", - severity: EmitterRpc.EmitterDiagnosticSeverity.Error + severity: EmitterRpc.EmitterDiagnosticSeverity.Warning ); } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs index 7bcf25792af..151bee47328 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs @@ -475,11 +475,12 @@ public void TestGetLatestFrameworkDifferentNames(string[] frameworks, string exp [TestCase(false)] public async Task TestReadProjectAssets(bool isSdkFramework) { + string artifactPath = isSdkFramework ? $"\n{Path.Combine(_tempDirectory!, "artifacts")}\n" : ""; var ns = "TestNamespace"; var nugetCacheDir = Path.Combine(_tempDirectory!, "NuGetCache"); - var csprojContent = @" + var csprojContent = $@" - netstandard2.0,net10.0 + netstandard2.0,net10.0{artifactPath} @@ -651,6 +652,57 @@ public async Task TestReadProjectAssetsUnsupportedPackageNames() } } + [Test] + [TestCase(null, false, "/src/obj", "TestNamespace")] + [TestCase(null, true, "/src/artifacts/obj", "TestNamespace")] + [TestCase("", false, "/src/obj", "TestNamespace")] + [TestCase("", true, "/src/artifacts/obj", "TestNamespace")] + [TestCase("/temp", false, "/temp/obj/TestNamespace", "TestNamespace")] + [TestCase("/temp", true, "/temp/obj/TestNamespace", "TestNamespace")] + [TestCase("", false, "/src/obj", "Dustbin")] + [TestCase("", true, "/src/artifacts/obj", "Dustbin")] + [TestCase("/temp", false, "/temp/obj/Dustbin", "Dustbin")] + [TestCase("/temp", true, "/temp/obj/Dustbin", "Dustbin")] + public async Task TestTryGetObjectPath(string? artifactPath, bool useArtifacts, string? expectedOutput, string projName) + { + if (artifactPath != null) + { + artifactPath = artifactPath.Replace("", _projectDir!).Replace('/', Path.DirectorySeparatorChar); + } + if (expectedOutput != null) + { + expectedOutput = expectedOutput.Replace("", _projectDir!).Replace('/', Path.DirectorySeparatorChar); + } + string artifactTag = string.IsNullOrEmpty(artifactPath) ? "" : $"\n{artifactPath}\n"; + string useArtifactTag = useArtifacts ? "\ntrue\n" : ""; + var csprojContent = $@" + + netstandard2.0,net10.0{useArtifactTag}{artifactTag} + + + + 1.0.0 + + + 3.5.0 + + +"; + File.WriteAllText(Path.Combine(_projectDir!, "src", $"{projName}.csproj"), csprojContent); + MockHelpers.LoadMockGenerator( + inputNamespaceName: projName, + outputPath: _projectDir, + configuration: $"{{\"package-name\": \"{projName}\"}}"); + // If the folder was not created, we should get null. + //Assert.That(await GeneratedCodeWorkspace.TryGetObjectPath(), Is.Null); + // Create directory and make sure it is not null now. + if (expectedOutput != null) + { + Directory.CreateDirectory(expectedOutput); + Assert.That(await GeneratedCodeWorkspace.TryGetObjectPath(), Is.EqualTo(expectedOutput)); + } + } + /// /// Creates a fake NuGet package assembly in the given cache directory and returns the DLL path. /// From b3f25694e54c929f9156becab95a97ca31a14017 Mon Sep 17 00:00:00 2001 From: nick863 Date: Mon, 31 Aug 2026 13:35:35 -0700 Subject: [PATCH 19/21] Better way to get project assets --- .../PostProcessing/GeneratedCodeWorkspace.cs | 42 ++--------- .../test/GeneratedCodeWorkspaceTests.cs | 70 ++++--------------- 2 files changed, 20 insertions(+), 92 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index d2fa7b970d0..f47debd693a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -275,20 +275,12 @@ internal static async Task>> ReadP string assetsJson = Path.Combine(CodeModelGenerator.Instance.Configuration.ProjectDirectory, "obj", "project.assets.json"); if (!File.Exists(assetsJson)) { - // If it does not exists, try the artifacts/obj/%project_name%/ three directoreies above projects directory. - // If this directory does not extists or does not contain artifacts/obj/%project_name%/roject.assets.json, give up. - //DirectoryInfo? directory = (new DirectoryInfo(CodeModelGenerator.Instance.Configuration.OutputDirectory)).Parent?.Parent?.Parent; - //if (directory == null) - //{ - // return hshFrameworks; - //} - //assetsJson = Path.Combine(directory.FullName, "artifacts", "obj", CodeModelGenerator.Instance.Configuration.PackageName, "project.assets.json"); - string objPath = await TryGetObjectPath() ?? ""; - assetsJson = Path.Combine(objPath, "project.assets.json"); - if (!File.Exists(assetsJson)) + string? assetsPath = await TryGetAssetsFile(); + if (string.IsNullOrEmpty(assetsPath) || !File.Exists(assetsPath)) { return hshFrameworks; } + assetsJson = assetsPath; } Utf8JsonReader reader = new Utf8JsonReader(await File.ReadAllBytesAsync(assetsJson)); using JsonDocument document = JsonDocument.ParseValue(ref reader); @@ -326,7 +318,7 @@ internal static async Task>> ReadP return hshFrameworks; } - internal static async Task TryGetObjectPath() + internal static async Task TryGetAssetsFile() { string projectFilePath = Path.GetFullPath( Path.Combine(CodeModelGenerator.Instance.Configuration.ProjectDirectory, $"{CodeModelGenerator.Instance.Configuration.PackageName}.csproj")); @@ -336,7 +328,7 @@ internal static async Task>> ReadP UseShellExecute = false, WindowStyle = ProcessWindowStyle.Hidden, FileName = "dotnet", - ArgumentList = { "msbuild", projectFilePath, "-getProperty:OutputPath" }, + ArgumentList = { "msbuild", projectFilePath, "-getProperty:ProjectAssetsFile" }, RedirectStandardOutput = true, RedirectStandardError = true, }; @@ -360,29 +352,7 @@ internal static async Task>> ReadP ); } } - Regex regBin = Path.DirectorySeparatorChar == '\\' ? new(@"(^bin\\)|(\\bin\\)") : new($"(^bin{Path.DirectorySeparatorChar})|({Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar})"); - if (string.IsNullOrEmpty(output) || !regBin.IsMatch(output)) - { - return null; - } - output = output.Trim(['\n', '\r', '\t', ' ']); - string binProjPath = Path.Combine("bin", CodeModelGenerator.Instance.Configuration.PackageName); - if (output.Contains(binProjPath)) - { - // Special case when we have the absolute path to the atrifacts. In this case the obj folder contains subfolder named with ptroject name. - string objpath = Path.Combine("obj", CodeModelGenerator.Instance.Configuration.PackageName); - output = output.ReplaceLast(output.Substring(output.LastIndexOf(binProjPath)), objpath); - } - else - { - output = output.ReplaceLast(output.Substring(output.LastIndexOf("bin")), "obj"); - } - // Handle relative paths - if (!Path.IsPathFullyQualified(output)) - { - output = Path.Combine(Path.GetDirectoryName(projectFilePath) ?? "", output); - } - return Directory.Exists(output) ? output : null; + return output?.Trim(['\n', '\r', '\t', ' ']); } internal static string GetLatestTargetFramework(IEnumerable shortNames) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs index 151bee47328..10f577f370c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs @@ -471,16 +471,13 @@ public void TestGetLatestFrameworkDifferentNames(string[] frameworks, string exp } [Test] - [TestCase(true)] - [TestCase(false)] - public async Task TestReadProjectAssets(bool isSdkFramework) + public async Task TestReadProjectAssets() { - string artifactPath = isSdkFramework ? $"\n{Path.Combine(_tempDirectory!, "artifacts")}\n" : ""; var ns = "TestNamespace"; var nugetCacheDir = Path.Combine(_tempDirectory!, "NuGetCache"); - var csprojContent = $@" + var csprojContent = @" - netstandard2.0,net10.0{artifactPath} + netstandard2.0,net10.0 @@ -513,27 +510,11 @@ public async Task TestReadProjectAssets(bool isSdkFramework) """; string projectDir; - if (isSdkFramework) - { - Assert.That(_tempDirectory, Is.Not.Null.And.Not.Empty); - Directory.CreateDirectory(Path.Combine(_tempDirectory!, "sdk")); - Directory.CreateDirectory(Path.Combine(_tempDirectory!, "sdk", "my_service")); - projectDir = Path.Combine(_tempDirectory!, "sdk", "my_service", "ProjectDir"); - Directory.CreateDirectory(projectDir); - Directory.CreateDirectory(Path.Combine(projectDir, "src")); - Directory.CreateDirectory(Path.Combine(_tempDirectory!, "artifacts")); - Directory.CreateDirectory(Path.Combine(_tempDirectory!, "artifacts", "obj")); - Directory.CreateDirectory(Path.Combine(_tempDirectory!, "artifacts", "obj", ns)); - File.WriteAllText(Path.Combine(_tempDirectory!, "artifacts", "obj", ns, "project.assets.json"), minimalProjectAssets); - } - else - { - Assert.That(_projectDir, Is.Not.Null.And.Not.Empty); - projectDir = _projectDir ?? ""; - Directory.CreateDirectory(Path.Combine(projectDir, "src")); - Directory.CreateDirectory(Path.Combine(projectDir, "src", "obj")); - File.WriteAllText(Path.Combine(projectDir, "src", "obj", "project.assets.json"), minimalProjectAssets); - } + Assert.That(_projectDir, Is.Not.Null.And.Not.Empty); + projectDir = _projectDir ?? ""; + Directory.CreateDirectory(Path.Combine(projectDir, "src")); + Directory.CreateDirectory(Path.Combine(projectDir, "src", "obj")); + File.WriteAllText(Path.Combine(projectDir, "src", "obj", "project.assets.json"), minimalProjectAssets); File.WriteAllText(Path.Combine(projectDir, "src", $"{ns}.csproj"), csprojContent); MockHelpers.LoadMockGenerator( @@ -653,31 +634,11 @@ public async Task TestReadProjectAssetsUnsupportedPackageNames() } [Test] - [TestCase(null, false, "/src/obj", "TestNamespace")] - [TestCase(null, true, "/src/artifacts/obj", "TestNamespace")] - [TestCase("", false, "/src/obj", "TestNamespace")] - [TestCase("", true, "/src/artifacts/obj", "TestNamespace")] - [TestCase("/temp", false, "/temp/obj/TestNamespace", "TestNamespace")] - [TestCase("/temp", true, "/temp/obj/TestNamespace", "TestNamespace")] - [TestCase("", false, "/src/obj", "Dustbin")] - [TestCase("", true, "/src/artifacts/obj", "Dustbin")] - [TestCase("/temp", false, "/temp/obj/Dustbin", "Dustbin")] - [TestCase("/temp", true, "/temp/obj/Dustbin", "Dustbin")] - public async Task TestTryGetObjectPath(string? artifactPath, bool useArtifacts, string? expectedOutput, string projName) + public async Task TestTryGetObjectPath() { - if (artifactPath != null) - { - artifactPath = artifactPath.Replace("", _projectDir!).Replace('/', Path.DirectorySeparatorChar); - } - if (expectedOutput != null) - { - expectedOutput = expectedOutput.Replace("", _projectDir!).Replace('/', Path.DirectorySeparatorChar); - } - string artifactTag = string.IsNullOrEmpty(artifactPath) ? "" : $"\n{artifactPath}\n"; - string useArtifactTag = useArtifacts ? "\ntrue\n" : ""; var csprojContent = $@" - netstandard2.0,net10.0{useArtifactTag}{artifactTag} + netstandard2.0,net10.0 @@ -688,19 +649,16 @@ public async Task TestTryGetObjectPath(string? artifactPath, bool useArtifacts, "; + string projName = "TestNamespace"; File.WriteAllText(Path.Combine(_projectDir!, "src", $"{projName}.csproj"), csprojContent); MockHelpers.LoadMockGenerator( inputNamespaceName: projName, outputPath: _projectDir, configuration: $"{{\"package-name\": \"{projName}\"}}"); - // If the folder was not created, we should get null. - //Assert.That(await GeneratedCodeWorkspace.TryGetObjectPath(), Is.Null); // Create directory and make sure it is not null now. - if (expectedOutput != null) - { - Directory.CreateDirectory(expectedOutput); - Assert.That(await GeneratedCodeWorkspace.TryGetObjectPath(), Is.EqualTo(expectedOutput)); - } + + string expectedOutput = Path.Combine(_projectDir!, "src", "obj", "project.assets.json"); + Assert.That(await GeneratedCodeWorkspace.TryGetAssetsFile(), Is.EqualTo(expectedOutput)); } /// From feabc658f3e432f17914d0cdaa0ad96e78a4dec9 Mon Sep 17 00:00:00 2001 From: nick863 Date: Mon, 31 Aug 2026 13:44:02 -0700 Subject: [PATCH 20/21] Fix --- .../src/PostProcessing/GeneratedCodeWorkspace.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index f47debd693a..fc44d18311f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -272,15 +272,10 @@ internal static async Task>> ReadP // Read in the resolved direct dependencies. // We first try the default location of project.assets.json, which is %project_dir%/obj/. - string assetsJson = Path.Combine(CodeModelGenerator.Instance.Configuration.ProjectDirectory, "obj", "project.assets.json"); - if (!File.Exists(assetsJson)) + string? assetsJson = await TryGetAssetsFile(); + if (string.IsNullOrEmpty(assetsJson) || !File.Exists(assetsJson)) { - string? assetsPath = await TryGetAssetsFile(); - if (string.IsNullOrEmpty(assetsPath) || !File.Exists(assetsPath)) - { - return hshFrameworks; - } - assetsJson = assetsPath; + return hshFrameworks; } Utf8JsonReader reader = new Utf8JsonReader(await File.ReadAllBytesAsync(assetsJson)); using JsonDocument document = JsonDocument.ParseValue(ref reader); @@ -345,7 +340,7 @@ internal static async Task>> ReadP { CodeModelGenerator.Instance.Emitter.ReportDiagnostic( code: "unable-to-get-artifact-path", - message: $"The dotnet msbuild {projectFilePath} -getProperty:OutputPath command exited with {restore.ExitCode}.\n" + + message: $"The dotnet msbuild {projectFilePath} -getProperty:ProjectAssetsFile command exited with {restore.ExitCode}.\n" + $"Standard output: {output}\n" + $"Error output: {error}", severity: EmitterRpc.EmitterDiagnosticSeverity.Warning From ed25bdec04b27d27441a2e7aa91bf8896a88b1e9 Mon Sep 17 00:00:00 2001 From: nick863 Date: Wed, 2 Sep 2026 18:17:29 -0700 Subject: [PATCH 21/21] Fix external resolver --- .../PostProcessing/GeneratedCodeWorkspace.cs | 84 +++++++++--- .../ExternalTypeReferenceResolver.cs | 61 ++++++--- .../test/GeneratedCodeWorkspaceTests.cs | 125 +++++++++++++++--- .../ExternalTypeReferenceResolverTests.cs | 120 ++++++++++++++--- 4 files changed, 315 insertions(+), 75 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs index fc44d18311f..c24c35d4d40 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -272,7 +272,7 @@ internal static async Task>> ReadP // Read in the resolved direct dependencies. // We first try the default location of project.assets.json, which is %project_dir%/obj/. - string? assetsJson = await TryGetAssetsFile(); + string? assetsJson = await GetAssetFileOrNull(); if (string.IsNullOrEmpty(assetsJson) || !File.Exists(assetsJson)) { return hshFrameworks; @@ -281,14 +281,38 @@ internal static async Task>> ReadP using JsonDocument document = JsonDocument.ParseValue(ref reader); foreach (JsonProperty prop in document.RootElement.EnumerateObject()) { + if (prop.Value.ValueKind == JsonValueKind.Object && prop.NameEquals("targets")) + { + foreach (JsonProperty targetFramework in prop.Value.EnumerateObject()) + { + NuGetFramework currentFramework = NuGetFramework.ParseFolder(targetFramework.Name); + if (!hshFrameworks.ContainsKey(currentFramework.GetShortFolderName())) + { + hshFrameworks[currentFramework.GetShortFolderName()] = []; + } + if (targetFramework.Value.ValueKind == JsonValueKind.Object) + { + // Parse dependencies. They are structured as SomePackage/package.version + foreach (JsonProperty packageAndVersion in targetFramework.Value.EnumerateObject()) + { + string[] packageVersion = packageAndVersion.Name.Split('/'); + if (packageVersion.Length == 2) + { + hshFrameworks[currentFramework.GetShortFolderName()][packageVersion[0].ToLower()] = packageVersion[1]; + } + } + } + } + } + // Centrally managed packages are stored in projectFileDependencyGroups; they are not present in targets if (prop.Value.ValueKind == JsonValueKind.Object && prop.NameEquals("projectFileDependencyGroups")) { foreach (JsonProperty targetFramework in prop.Value.EnumerateObject()) { - NuGetFramework currentFramework = new(targetFramework.Name); - if (!hshFrameworks.ContainsKey(currentFramework.Framework)) + NuGetFramework currentFramework = NuGetFramework.ParseFolder(targetFramework.Name); + if (!hshFrameworks.ContainsKey(currentFramework.GetShortFolderName())) { - hshFrameworks[currentFramework.Framework] = []; + hshFrameworks[currentFramework.GetShortFolderName()] = []; } if (targetFramework.Value.ValueKind == JsonValueKind.Array) { @@ -298,11 +322,19 @@ internal static async Task>> ReadP if (packageAndVersion.ValueKind == JsonValueKind.String) { string[] packageVersionRelation = (packageAndVersion.GetString() ?? "").Split(); - // We only support the greater-than-or-equal relation. + // We only support the greater-than-or-equal relation, in other cases we only record the package. // Example: "My.Package >= 1.1.1" - if (packageVersionRelation.Length == 3 && string.Equals(packageVersionRelation[1], ">=")) + string packageName = packageVersionRelation[0].ToLower(); + if (!string.IsNullOrEmpty(packageName) && !hshFrameworks[currentFramework.GetShortFolderName()].ContainsKey(packageName)) { - hshFrameworks[currentFramework.Framework][packageVersionRelation[0].ToLower()] = packageVersionRelation[2]; + if (packageVersionRelation.Length == 3 && string.Equals(packageVersionRelation[1], ">=")) + { + hshFrameworks[currentFramework.GetShortFolderName()][packageName] = packageVersionRelation[2]; + } + else + { + hshFrameworks[currentFramework.GetShortFolderName()][packageName] = ""; + } } } } @@ -313,10 +345,14 @@ internal static async Task>> ReadP return hshFrameworks; } - internal static async Task TryGetAssetsFile() + internal static async Task GetAssetFileOrNull() { string projectFilePath = Path.GetFullPath( Path.Combine(CodeModelGenerator.Instance.Configuration.ProjectDirectory, $"{CodeModelGenerator.Instance.Configuration.PackageName}.csproj")); + if (!File.Exists(projectFilePath)) + { + return null; + } Process restore = new(); ProcessStartInfo info = new() { @@ -364,7 +400,7 @@ internal static string GetLatestTargetFramework(IEnumerable shortNames) Match numeral = Regex.Match(name, "\\d+[.]*\\d*$"); if (numeral.Success) { - current = double.Parse(numeral.Value); + current = double.Parse(numeral.Value, System.Globalization.CultureInfo.InvariantCulture); } if (name.StartsWith("net4", StringComparison.InvariantCultureIgnoreCase)) { @@ -446,7 +482,26 @@ internal static async Task AddPackageReferencesFromProject() Dictionary hshNameVersion = []; if (hshFrameworks.Count > 0) { - hshNameVersion = hshFrameworks[GetLatestTargetFramework(hshFrameworks.Keys.AsEnumerable())]; + // Mimic the behavior of NugetPackageResolver.FindPackageAssemblyInVersion here + // when selecting Framefork i.e. select the framework from the ones + // supported by the project to the one currently running. + string? frameworkName = AppContext.TargetFrameworkName; + NuGetFramework? currentFramework = null; + if (!string.IsNullOrEmpty(frameworkName)) + { + try + { + currentFramework = NuGetFramework.Parse(frameworkName); + } + catch (ArgumentException) + { + // Fall through to the runtime-version based approximation below. + } + } + currentFramework = currentFramework ?? NuGetFramework.Parse($".NETCoreApp,Version=v{Environment.Version.Major}.{Environment.Version.Minor}"); + NuGetFramework? nearest = new FrameworkReducer().GetNearest(currentFramework, hshFrameworks.Keys.Select(x => NuGetFramework.ParseFolder(x))); + string bestFramework = nearest?.GetShortFolderName() ?? GetLatestTargetFramework(hshFrameworks.Keys.AsEnumerable()); + hshNameVersion = hshFrameworks[bestFramework]; } // Build a set of assembly names already registered so we can skip them var existingRefs = new HashSet( @@ -456,10 +511,8 @@ internal static async Task AddPackageReferencesFromProject() .Where(n => !string.IsNullOrEmpty(n)), StringComparer.OrdinalIgnoreCase); - foreach (var item in projectRoot.Items.Where(i => i.ItemType == "PackageReference")) + foreach (string refPackageName in hshNameVersion.Keys) { - var refPackageName = item.Include; - if (string.IsNullOrEmpty(refPackageName)) { continue; @@ -472,9 +525,8 @@ internal static async Task AddPackageReferencesFromProject() } // Search the NuGet global packages folder for any cached version of this package. - string? version = default; - hshNameVersion.TryGetValue(refPackageName.ToLower(), out version); - string? resolvedAssemblyPath = version is null + string version = hshNameVersion[refPackageName]; + string? resolvedAssemblyPath = string.IsNullOrEmpty(version) ? NugetPackageResolver.FindPackageAssembly(globalPackagesFolder, refPackageName) : NugetPackageResolver.FindPackageAssemblyInVersion(globalPackagesFolder, refPackageName, version); if (resolvedAssemblyPath == null) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/ExternalTypeReferenceResolver.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/ExternalTypeReferenceResolver.cs index c4ca62c6b94..146d30d188b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/ExternalTypeReferenceResolver.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/ExternalTypeReferenceResolver.cs @@ -5,6 +5,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; @@ -12,6 +13,7 @@ using Microsoft.CodeAnalysis; using Microsoft.TypeSpec.Generator.Input; using NuGet.Configuration; +using NuGet.Versioning; namespace Microsoft.TypeSpec.Generator.Utilities { @@ -231,36 +233,61 @@ private static async Task ResolveResultAsync(InputExternalType var assemblyResolver = state.GetAssemblyResolver(globalPackagesFolder, generator); assemblyResolver.Activate(); - string? assemblyPath = NugetPackageResolver.FindPackageAssembly( - globalPackagesFolder, external.Package!, external.MinVersion); + // We have already resolved packages, now we will search for package and its version path in metadata. + string packageFolder = $"{Path.DirectorySeparatorChar}{external.Package!}{Path.DirectorySeparatorChar}"; + (string AssemblyPath, string PackageVersion) packageInfo = CodeModelGenerator.Instance.AdditionalMetadataReferences + .Where(x => x.Properties.Kind == MetadataImageKind.Assembly + && x.Display is not null + && x.Display.Contains(packageFolder, StringComparison.InvariantCultureIgnoreCase) + && x.Display.Substring(x.Display.LastIndexOf(packageFolder, StringComparison.InvariantCultureIgnoreCase)).Split(Path.DirectorySeparatorChar).Length > 2) + .Select(x => x.Display ?? "") + .Select(x => (AssemblyPath: x, PackageVersion: x.Substring(x.LastIndexOf(packageFolder, StringComparison.InvariantCultureIgnoreCase)).Split(Path.DirectorySeparatorChar)[2])) + .FirstOrDefault(); + // If we have a min version, we must check if our package is compliant. + bool versionAcceptable = true; + if (!string.IsNullOrEmpty(external.MinVersion) && !string.IsNullOrEmpty(packageInfo.PackageVersion)) + { + NuGetVersion existingVersion = new(packageInfo.PackageVersion); + NuGetVersion targetVersion = new(external.MinVersion); + versionAcceptable = existingVersion >= targetVersion; + } + if (!versionAcceptable) + { + var versionQualifier = string.IsNullOrEmpty(external.MinVersion) + ? string.Empty + : $"(>= {external.MinVersion})"; + return CacheResult(state, key, new ResolutionResult( + null, + $"The package '{external.Package}' minimal version declared in a typespec {versionQualifier} is higher then the one defined in project dependencies \"{packageInfo.PackageVersion}\".")); + } - if (assemblyPath == null || !File.Exists(assemblyPath)) + if (packageInfo.AssemblyPath == null || !File.Exists(packageInfo.AssemblyPath) || !versionAcceptable) { var versionQualifier = string.IsNullOrEmpty(external.MinVersion) ? string.Empty : $" (>= {external.MinVersion})"; return CacheResult(state, key, new ResolutionResult( null, - $"package '{external.Package}'{versionQualifier} was not found in the NuGet cache or any configured feed")); + $"package '{external.Package}'{versionQualifier} is not present in package dependencies.")); } // Pin every package in this package's dependency closure before loading it, so the resolving // hook binds dependencies to the versions NuGet selected rather than guessing from assembly // versions (which are routinely lower than the package versions that ship them). - assemblyResolver.RegisterPackageClosure(assemblyPath); + assemblyResolver.RegisterPackageClosure(packageInfo.AssemblyPath); byte[] assemblyBytes; try { - assemblyBytes = await File.ReadAllBytesAsync(assemblyPath).ConfigureAwait(false); + assemblyBytes = await File.ReadAllBytesAsync(packageInfo.AssemblyPath).ConfigureAwait(false); } catch (Exception ex) { generator.Emitter?.Debug( - $"Failed to read assembly '{assemblyPath}' for external type '{external.Identity}': {ex.Message}"); + $"Failed to read assembly '{packageInfo.AssemblyPath}' for external type '{external.Identity}': {ex.Message}"); return CacheResult(state, key, new ResolutionResult( null, - $"assembly '{assemblyPath}' could not be read ({ex.Message})")); + $"assembly '{packageInfo.AssemblyPath}' could not be read ({ex.Message})")); } Type? loadedType; @@ -274,10 +301,10 @@ private static async Task ResolveResultAsync(InputExternalType catch (Exception ex) { generator.Emitter?.Debug( - $"Failed to load assembly '{assemblyPath}' for external type '{external.Identity}': {ex.Message}"); + $"Failed to load assembly '{packageInfo.AssemblyPath}' for external type '{external.Identity}': {ex.Message}"); return CacheResult(state, key, new ResolutionResult( null, - $"assembly '{assemblyPath}' could not be loaded ({ex.Message})" + + $"assembly '{packageInfo.AssemblyPath}' could not be loaded ({ex.Message})" + assemblyResolver.DescribeDowngradedDependencies())); } @@ -286,23 +313,13 @@ private static async Task ResolveResultAsync(InputExternalType // Either the type genuinely isn't in the assembly, or one of its dependencies could not be // satisfied even with the NuGet probing hook installed - GetType reports both as null. generator.Emitter?.Debug( - $"Assembly '{assemblyPath}' does not declare external type '{external.Identity}', or one of its dependencies could not be resolved."); + $"Assembly '{packageInfo.AssemblyPath}' does not declare external type '{external.Identity}', or one of its dependencies could not be resolved."); return CacheResult(state, key, new ResolutionResult( null, - $"assembly '{assemblyPath}' was loaded but does not declare the type, or one of the type's dependencies could not be resolved" + + $"assembly '{packageInfo.AssemblyPath}' was loaded but does not declare the type, or one of the type's dependencies could not be resolved" + assemblyResolver.DescribeDowngradedDependencies())); } - // Register the dll as a Roslyn metadata reference exactly once per assembly path so that - // generated and custom code that uses the type compiles inside the workspace. - // Use CreateFromImage with the in-memory bytes to avoid holding the dll open. - if (state.AddedAssemblyRefs.TryAdd(assemblyPath, 0)) - { - generator.AddMetadataReference(MetadataReference.CreateFromImage(assemblyBytes)); - generator.Emitter?.Debug( - $"Added metadata reference for external type '{external.Identity}' from {assemblyPath}"); - } - return CacheResult(state, key, new ResolutionResult(loadedType, null)); } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs index 10f577f370c..49e05859ca9 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs @@ -212,7 +212,7 @@ public class ExternalCredential { } await GeneratedCodeWorkspace.AddPackageReferencesFromProject(); var refCountAfter = CodeModelGenerator.Instance.AdditionalMetadataReferences.Count; - Assert.AreEqual(refCountBefore + 1, refCountAfter, "Should have added one metadata reference"); + Assert.That(refCountAfter, Is.GreaterThanOrEqualTo(refCountBefore + 1), "Should have added at least one metadata reference and its dependencies."); } [Test] @@ -293,7 +293,7 @@ public async Task AddPackageReferencesFromProject_ResolvesPackageWithNoVersion() await GeneratedCodeWorkspace.AddPackageReferencesFromProject(); var refCountAfter = CodeModelGenerator.Instance.AdditionalMetadataReferences.Count; - Assert.AreEqual(refCountBefore + 1, refCountAfter, + Assert.That(refCountAfter, Is.GreaterThanOrEqualTo(refCountBefore + 1), "Should resolve package from cache even without a version (centrally managed)"); } @@ -329,6 +329,8 @@ public async Task AddPackageReferencesFromProject_SkipsAlreadyAddedReferences() // Pre-add the reference (simulating a plugin that already added it) CodeModelGenerator.Instance.AddMetadataReference( MetadataReference.CreateFromFile(dllPath)); + // Update dependencies + await GeneratedCodeWorkspace.AddPackageReferencesFromProject(); var refCountBefore = CodeModelGenerator.Instance.AdditionalMetadataReferences.Count; await GeneratedCodeWorkspace.AddPackageReferencesFromProject(); @@ -397,22 +399,22 @@ public async Task AddPackageReferencesFromProject_AddsMultiplePackageReferences( .Select(x => (Name: x[0], Version: x[1], TargetFramework: x[3])); foreach (var resolvedPackage in resolvedPackages) { - Assert.That(resolvedPackage.TargetFramework, Is.EqualTo("netstandard2.0")); if(packages.TryGetValue(resolvedPackage.Name, out string? version)) { + Assert.That(resolvedPackage.TargetFramework, Is.EqualTo("netstandard2.0")); Assert.Fail($"Found more than one version for package {resolvedPackage.Name}: {version} and {resolvedPackage.Version}"); } packages[resolvedPackage.Name] = resolvedPackage.Version; } if (badPackage) { - Assert.AreEqual(refCountBefore + 1, refCountAfter, "Should have added one metadata reference as the second one was intentionally broken"); + Assert.That(refCountAfter, Is.GreaterThanOrEqualTo(refCountBefore + 1), "Should have added one metadata reference as the second one was intentionally broken"); AssertPackageVersion(packages, "Second.Package", "3.5.0"); Assert.That(packages, Does.Not.ContainKey("first.package")); } else { - Assert.AreEqual(refCountBefore + 2, refCountAfter, "Should have added two metadata references"); + Assert.That(refCountAfter, Is.GreaterThanOrEqualTo(refCountBefore + 2), "Should have added two metadata references"); AssertPackageVersion(packages, "First.Package", "1.0.0"); AssertPackageVersion(packages, "Second.Package", "3.5.0"); } @@ -491,7 +493,38 @@ public async Task TestReadProjectAssets() string minimalProjectAssets = """ { "version": 4, - "targets": {}, + "targets": { + "netstandard2.0": { + "First.Package/1.0.0": { + "type": "package", + "dependencies": {} + }, + "Second.Package/3.5.0": { + "type": "package", + "dependencies": {} + } + }, + "net462": { + "First.Package/1.0.0": { + "type": "package", + "dependencies": {} + }, + "Second.Package/3.5.0": { + "type": "package", + "dependencies": {} + } + }, + "net10.0": { + "First.Package/1.0.0": { + "type": "package", + "dependencies": {} + }, + "Second.Package/3.5.0": { + "type": "package", + "dependencies": {} + } + } + }, "projectFileDependencyGroups": { "netstandard2.0": [ "First.Package >= 1.0.0", @@ -538,6 +571,20 @@ public async Task TestReadProjectAssets() } } + [Test] + public async Task TestReadProjectAssetsFileDoesNotExist() + { + var ns = "TestNamespace"; + Assert.That(_projectDir, Is.Not.Null.And.Not.Empty); + File.Delete(Path.Combine(_projectDir!, "src", $"{ns}.csproj")); + + MockHelpers.LoadMockGenerator( + inputNamespaceName: ns, + outputPath: _projectDir, + configuration: $"{{\"package-name\": \"{ns}\"}}"); + Assert.That(await GeneratedCodeWorkspace.GetAssetFileOrNull(), Is.Null); + } + [Test] public async Task TestReadProjectAssetsNoFile() { @@ -586,19 +633,63 @@ public async Task TestReadProjectAssetsUnsupportedPackageNames() string minimalProjectAssets = """ { "version": 4, - "targets": {}, + "targets": { + "netstandard2.0": { + "First.Package/1.0.0": { + "type": "package", + "dependencies": {} + }, + "Second.Package": { + "type": "package", + "dependencies": {} + } + }, + "net462": { + "First.Package/1.1.0": { + "type": "package", + "dependencies": {} + }, + "Second.Package/3.5.0/3.7.0": { + "type": "package", + "dependencies": {} + } + }, + "net478": {}, + "net479": {}, + "net480": {}, + "net481": {}, + "net10.0": { + "First.Package/1.2.0": { + "type": "package", + "dependencies": {} + }, + "Second.Package 3.5.0": { + "type": "package", + "dependencies": {} + } + } + }, "projectFileDependencyGroups": { "netstandard2.0": [ - "First.Package >= 1.0.0", - "Second.Package" + "First.Package >= 2.0.0" ], "net462": [ - "First.Package >= 1.1.0", - "Second.Package == 3.5.0" + "First.Package >= 2.1.0" + ], + "net478": [ + "First.Package >= 2.1.0" + ], + "net479": [ + "First.Package" + ], + "net480": [ + "First.Package < 2.1.0" + ], + "net481": [ + "First.Package <= 1.2.3 > 1.0.0" ], "net10.0": [ - "First.Package >= 1.2.0", - "Second.Package < 3.5.0" + "First.Package >= 2.2.0" ] } } @@ -611,13 +702,17 @@ public async Task TestReadProjectAssetsUnsupportedPackageNames() outputPath: _projectDir, configuration: $"{{\"package-name\": \"{ns}\"}}"); Dictionary> dtFrameworks = await GeneratedCodeWorkspace.ReadProjectAssets(); - Assert.That(dtFrameworks, Has.Count.EqualTo(3)); + Assert.That(dtFrameworks, Has.Count.EqualTo(7)); foreach (string framework in new string[] { "netstandard2.0", "net10.0", "net462" }) { string version = framework switch { "netstandard2.0" => "1.0.0", "net462" => "1.1.0", + "net478" => "2.1.0", + "net479" => "", + "net480" => "", + "net481" => "", "net10.0" => "1.2.0", _ => throw new InvalidOperationException($"Invalid value {framework}") }; @@ -658,7 +753,7 @@ public async Task TestTryGetObjectPath() // Create directory and make sure it is not null now. string expectedOutput = Path.Combine(_projectDir!, "src", "obj", "project.assets.json"); - Assert.That(await GeneratedCodeWorkspace.TryGetAssetsFile(), Is.EqualTo(expectedOutput)); + Assert.That(await GeneratedCodeWorkspace.GetAssetFileOrNull(), Is.EqualTo(expectedOutput)); } /// diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/ExternalTypeReferenceResolverTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/ExternalTypeReferenceResolverTests.cs index b94a50c5836..d67552cb47d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/ExternalTypeReferenceResolverTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/ExternalTypeReferenceResolverTests.cs @@ -5,7 +5,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using System.Threading.Tasks; +using Microsoft.CodeAnalysis; using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Tests.Common; using Microsoft.TypeSpec.Generator.Utilities; @@ -68,11 +70,14 @@ public void TryResolve_ReturnsNullWhenPackageMissing() } [Test] - public void TryResolve_LoadsTypeFromNuGetCache() + public async Task TryResolve_LoadsTypeFromNuGetCache() { const string pkgName = "Test.External.Loadable"; + const string pkgVersion = "1.2.3"; const string typeName = "Test.External.Loadable.LoadableType"; - CreateFakeNuGetPackage(_nugetCacheDir!, pkgName, "1.2.3"); + CreateFakeNuGetPackage(_nugetCacheDir!, pkgName, pkgVersion); + + await CreateProjectAndLoadDependencies([pkgName], [pkgVersion]); var external = new InputExternalTypeMetadata(typeName, pkgName, null); @@ -83,16 +88,19 @@ public void TryResolve_LoadsTypeFromNuGetCache() } [Test] - public void TryResolve_PrefersHighestCachedVersionAtOrAboveMinVersion() + public async Task TryResolve_PrefersHighestCachedVersionAtOrAboveMinVersion() { const string pkgName = "Test.MultiVersion.Package"; const string typeName = "Test.MultiVersion.Package.SomeType"; + const string highestVersion = "3.0.0"; // Create three cached versions; MinVersion=2.0.0 must skip 1.0.0 and pick 3.0.0 (highest >= MinVersion). + CreateFakeNuGetPackage(_nugetCacheDir!, pkgName, "1.0.0"); CreateFakeNuGetPackage(_nugetCacheDir!, pkgName, "2.5.0"); - CreateFakeNuGetPackage(_nugetCacheDir!, pkgName, "3.0.0"); - + CreateFakeNuGetPackage(_nugetCacheDir!, pkgName, highestVersion); + + await CreateProjectAndLoadDependencies([pkgName], [highestVersion]); var external = new InputExternalTypeMetadata(typeName, pkgName, "2.0.0"); var resolved = ExternalTypeReferenceResolver.TryResolve(external); @@ -106,11 +114,13 @@ public void TryResolve_PrefersHighestCachedVersionAtOrAboveMinVersion() } [Test] - public void TryResolve_AddsMetadataReferenceOnce() + public async Task TryResolve_AddsMetadataReferenceOnce() { const string pkgName = "Test.MetadataRef.Package"; const string typeName = "Test.MetadataRef.Package.RefType"; - CreateFakeNuGetPackage(_nugetCacheDir!, pkgName, "1.0.0"); + const string pkgVersion = "1.0.0"; + CreateFakeNuGetPackage(_nugetCacheDir!, pkgName, pkgVersion); + await CreateProjectAndLoadDependencies([pkgName], [pkgVersion]); var external = new InputExternalTypeMetadata(typeName, pkgName, null); @@ -123,33 +133,55 @@ public void TryResolve_AddsMetadataReferenceOnce() Assert.IsNotNull(resolved2); Assert.AreSame(resolved1, resolved2, "Cache should return the same Type for repeated lookups."); Assert.AreEqual( - refsBefore + 1, + refsBefore, refsAfter, - "Resolver should add the assembly as a metadata reference exactly once."); + "Resolver should add the assembly during project load."); } [Test] - public void TryResolve_ReturnsNullForUnknownPackage() + public async Task TryResolve_ReturnsNullForUnknownPackage() { var external = new InputExternalTypeMetadata( "Some.Unknown.Type", "Definitely.Not.A.Real.Package.Anywhere.Test", "999.0.0"); + await CreateProjectAndLoadDependencies([], []); + var resolved = ExternalTypeReferenceResolver.TryResolve(external); + Assert.IsNull(resolved); + StringAssert.Contains( + "is not present in package dependencies.", + ExternalTypeReferenceResolver.GetFailureReason(external), + "A missing package should be reported as a missing package."); + } + + [Test] + public async Task TryResolve_ReturnsNullForHigherMinVersion() + { + const string pkgName = "My.Package"; + const string typeName = "My.Package.NewType"; + CreateFakeNuGetPackage(_nugetCacheDir!, pkgName, "41.0.0"); + CreateFakeNuGetPackage(_nugetCacheDir!, pkgName, "42.0.0"); + var external = new InputExternalTypeMetadata( + typeName, + pkgName, + "42.0.0"); + await CreateProjectAndLoadDependencies([pkgName], ["41.0.0"]); var resolved = ExternalTypeReferenceResolver.TryResolve(external); Assert.IsNull(resolved); StringAssert.Contains( - "was not found in the NuGet cache", + "minimal version declared in a typespec (>= 42.0.0) is higher then the one defined in project dependencies \"41.0.0\"", ExternalTypeReferenceResolver.GetFailureReason(external), "A missing package should be reported as a missing package."); } [Test] - public void TryResolve_ResolvesTypeWhoseBaseTypeLivesInAnotherPackage() + public async Task TryResolve_ResolvesTypeWhoseBaseTypeLivesInAnotherPackage() { var nugetCacheDir = Path.Combine(_tempDirectory!, "NuGetCache"); const string basePkg = "Test.Dependency.Base"; + const string pkgVersion = "2.0.0"; const string leafPkg = "Test.Dependent.Leaf"; const string leafTypeName = "Test.Dependent.Leaf.DerivedFromDependencyType"; @@ -159,7 +191,7 @@ public void TryResolve_ResolvesTypeWhoseBaseTypeLivesInAnotherPackage() var baseDll = CreateFakeNuGetPackage( nugetCacheDir, basePkg, - "2.0.0", + pkgVersion, template: "DependencyPackageSource", assemblyVersion: "1.0.0.0"); @@ -177,6 +209,8 @@ public void TryResolve_ResolvesTypeWhoseBaseTypeLivesInAnotherPackage() basePackage: basePkg, referencedAssemblyPaths: [baseDll], dependencies: [(basePkg, "[2.0.0, )")]); + // All the dependencies, even indirect onees will be present in project.assets.json. + await CreateProjectAndLoadDependencies([basePkg, leafPkg], [pkgVersion, "1.0.0"]); var external = new InputExternalTypeMetadata(leafTypeName, leafPkg, "1.0.0"); @@ -200,11 +234,13 @@ public void TryResolve_ResolvesTypeWhoseBaseTypeLivesInAnotherPackage() } [Test] - public void TryResolve_ReportsFailureReasonWhenTypeMissingFromAssembly() + public async Task TryResolve_ReportsFailureReasonWhenTypeMissingFromAssembly() { var nugetCacheDir = Path.Combine(_tempDirectory!, "NuGetCache"); const string pkgName = "Test.MissingType.Package"; - CreateFakeNuGetPackage(nugetCacheDir, pkgName, "1.0.0"); + const string pkgVersion = "1.0.0"; + CreateFakeNuGetPackage(nugetCacheDir, pkgName, pkgVersion); + await CreateProjectAndLoadDependencies([pkgName], [pkgVersion]); var external = new InputExternalTypeMetadata($"{pkgName}.NotDeclaredAnywhere", pkgName, null); @@ -219,8 +255,9 @@ public void TryResolve_ReportsFailureReasonWhenTypeMissingFromAssembly() public async Task ResolveAllAsync_ResolvesExternalTypesFromInputLibrary() { const string pkgName = "Test.PreWalk.Package"; + const string pkgVersion = "1.0.0"; const string typeName = "Test.PreWalk.Package.PreWalkType"; - CreateFakeNuGetPackage(_nugetCacheDir!, pkgName, "1.0.0"); + CreateFakeNuGetPackage(_nugetCacheDir!, pkgName, pkgVersion); var external = new InputExternalTypeMetadata(typeName, pkgName, null); var unionWithExternal = InputFactory.Union( @@ -234,10 +271,7 @@ public async Task ResolveAllAsync_ResolvesExternalTypesFromInputLibrary() InputFactory.Property("ext", unionWithExternal), ]); - MockHelpers.LoadMockGenerator( - outputPath: _projectDir, - configuration: "{}", - inputModelTypes: [model]); + await CreateProjectAndLoadDependencies([pkgName], [pkgVersion]); var refsBefore = CodeModelGenerator.Instance.AdditionalMetadataReferences.Count; await ExternalTypeReferenceResolver.ResolveAllAsync(); @@ -247,9 +281,9 @@ public async Task ResolveAllAsync_ResolvesExternalTypesFromInputLibrary() Assert.IsNotNull(resolved); Assert.AreEqual(typeName, resolved!.FullName); Assert.AreEqual( - refsBefore + 1, + refsBefore, CodeModelGenerator.Instance.AdditionalMetadataReferences.Count, - "Pre-walk should add the metadata reference exactly once."); + "The Metadata must be loaded when the package is being loaded."); } private static string CreateFakeNuGetPackage( @@ -294,6 +328,48 @@ private static void WriteLocalNuGetConfig(string projectDir, string nugetCacheDi File.WriteAllText(nugetConfigPath, config); } + private async Task CreateProjectAndLoadDependencies(string[] packages, string?[] versions) + { + Assert.That(packages.Length, Is.EqualTo(versions.Length), "Each package must have a version (it can be null)"); + StringBuilder sbPackagesProject = new(); + StringBuilder sbPackagesAssets = new(); + string tab = " "; + for (int i = 0; i< packages.Length; i++) + { + sbPackagesProject.Append($"\n \n {versions[i]}\n \n"); + sbPackagesAssets.Append($"\n{tab}{tab}\"{packages[i]}\": {{\n{tab}{tab}{tab}\"type\": \"package\",\n{tab}{tab}{tab}\"dependencies\": {{}}\n{tab}{tab}}}\n"); + } + var csprojContent = $@" + + netstandard2.0,net10.0 + + {sbPackagesProject} +"; + string minimalProjectAssets = $$""" + { + "version": 4, + "targets": { + "netstandard2.0": {{{sbPackagesAssets}} + }, + "net10.0": {{{sbPackagesAssets}} + } + } + } + """; + Assert.That(_projectDir, Is.Not.Null.And.Not.Empty); + Directory.CreateDirectory(Path.Combine(_projectDir!, "src")); + Directory.CreateDirectory(Path.Combine(_projectDir!, "src", "obj")); + string ns = "TestProject"; + File.WriteAllText(Path.Combine(_projectDir!, "src", "obj", "project.assets.json"), minimalProjectAssets); + File.WriteAllText(Path.Combine(_projectDir!, "src", $"{ns}.csproj"), csprojContent); + + MockHelpers.LoadMockGenerator( + inputNamespaceName: ns, + outputPath: _projectDir!, + configuration: $"{{\"package-name\": \"{ns}\"}}"); + await GeneratedCodeWorkspace.AddPackageReferencesFromProject(); + } + private static void ResetResolverIfInitialized() { try