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..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 @@ -6,6 +6,8 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Text.Json; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Build.Construction; @@ -18,6 +20,8 @@ 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; namespace Microsoft.TypeSpec.Generator @@ -261,6 +265,161 @@ internal static Project AddDirectory(Project project, string directory, Func>> ReadProjectAssets() + { + 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 = await GetAssetFileOrNull(); + if (string.IsNullOrEmpty(assetsJson) || !File.Exists(assetsJson)) + { + return hshFrameworks; + } + Utf8JsonReader reader = new Utf8JsonReader(await File.ReadAllBytesAsync(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()) + { + 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 = NuGetFramework.ParseFolder(targetFramework.Name); + if (!hshFrameworks.ContainsKey(currentFramework.GetShortFolderName())) + { + hshFrameworks[currentFramework.GetShortFolderName()] = []; + } + 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 the greater-than-or-equal relation, in other cases we only record the package. + // Example: "My.Package >= 1.1.1" + string packageName = packageVersionRelation[0].ToLower(); + if (!string.IsNullOrEmpty(packageName) && !hshFrameworks[currentFramework.GetShortFolderName()].ContainsKey(packageName)) + { + if (packageVersionRelation.Length == 3 && string.Equals(packageVersionRelation[1], ">=")) + { + hshFrameworks[currentFramework.GetShortFolderName()][packageName] = packageVersionRelation[2]; + } + else + { + hshFrameworks[currentFramework.GetShortFolderName()][packageName] = ""; + } + } + } + } + } + } + } + } + return hshFrameworks; + } + + 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() + { + UseShellExecute = false, + WindowStyle = ProcessWindowStyle.Hidden, + FileName = "dotnet", + ArgumentList = { "msbuild", projectFilePath, "-getProperty:ProjectAssetsFile" }, + 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:ProjectAssetsFile command exited with {restore.ExitCode}.\n" + + $"Standard output: {output}\n" + + $"Error output: {error}", + severity: EmitterRpc.EmitterDiagnosticSeverity.Warning + ); + } + } + return output?.Trim(['\n', '\r', '\t', ' ']); + } + + internal static string GetLatestTargetFramework(IEnumerable shortNames) + { + // 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, System.Globalization.CultureInfo.InvariantCulture); + } + 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; + } + /// /// 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. @@ -275,24 +434,85 @@ 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", + ArgumentList = {"restore", projectFilePath}, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + 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) + { + 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}", + severity: EmitterRpc.EmitterDiagnosticSeverity.Warning + ); + } + } + else + { + 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 for all frameworks + Dictionary> hshFrameworks = await ReadProjectAssets(); + // Get the latest framework. + Dictionary hshNameVersion = []; + if (hshFrameworks.Count > 0) + { + // 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( - 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")) + foreach (string refPackageName in hshNameVersion.Keys) { - var refPackageName = item.Include; - if (string.IsNullOrEmpty(refPackageName)) { continue; @@ -305,29 +525,26 @@ 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 = hshNameVersion[refPackageName]; + string? resolvedAssemblyPath = string.IsNullOrEmpty(version) + ? NugetPackageResolver.FindPackageAssembly(globalPackagesFolder, refPackageName) + : NugetPackageResolver.FindPackageAssemblyInVersion(globalPackagesFolder, refPackageName, version); 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( + $"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( - $"Could not download package {refPackageName}: {ex.Message}"); + $"Using cached {refPackageName} v. {maxVersion.Version}."); } } 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..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,13 +13,14 @@ using Microsoft.CodeAnalysis; using Microsoft.TypeSpec.Generator.Input; using NuGet.Configuration; +using NuGet.Versioning; 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. /// /// @@ -231,62 +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); - - if (assemblyPath == null) + // 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)) { - try - { - var resolvedVersion = !string.IsNullOrEmpty(external.MinVersion) - ? external.MinVersion! - : 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}"); - } + 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; @@ -300,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())); } @@ -312,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/src/Utilities/NugetPackageResolver.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/NugetPackageResolver.cs index 2c2883a3534..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; 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..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 @@ -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; @@ -148,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.ToLowerInvariant()}.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 = $@" @@ -172,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] @@ -253,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)"); } @@ -289,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(); @@ -299,14 +341,27 @@ public async Task AddPackageReferencesFromProject_SkipsAlreadyAddedReferences() } [Test] - public async Task AddPackageReferencesFromProject_AddsMultiplePackageReferences() + [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"); // 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"); + 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 = @" @@ -331,8 +386,374 @@ public async Task AddPackageReferencesFromProject_AddsMultiplePackageReferences( var refCountBefore = CodeModelGenerator.Instance.AdditionalMetadataReferences.Count; await GeneratedCodeWorkspace.AddPackageReferencesFromProject(); var refCountAfter = CodeModelGenerator.Instance.AdditionalMetadataReferences.Count; + // Extract versions and packages; make sure there is only one version. + Dictionary packages = []; + // 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 + .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) + { + 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.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.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"); + } + } + + 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(true, true)] + [TestCase(true, false)] + [TestCase(false, true)] + public void 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[] { "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")] + [TestCase(new string[] { "netstandard2.0", "net472", "net462" }, "net472")] + public void TestGetLatestFrameworkDifferentNames(string[] frameworks, string expected) + { + Assert.That(GeneratedCodeWorkspace.GetLatestTargetFramework(frameworks), Is.EqualTo(expected)); + } + + [Test] + public async Task TestReadProjectAssets() + { + 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": { + "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", + "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; + 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 = await GeneratedCodeWorkspace.ReadProjectAssets(); + 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 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() + { + var ns = "TestNamespace"; + 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 = await GeneratedCodeWorkspace.ReadProjectAssets(); + Assert.That(dtFrameworks, Has.Count.EqualTo(0)); + } + + [Test] + public async Task TestReadProjectAssetsUnsupportedPackageNames() + { + var ns = "TestNamespace"; + 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": { + "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 >= 2.0.0" + ], + "net462": [ + "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 >= 2.2.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 = await GeneratedCodeWorkspace.ReadProjectAssets(); + 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}") + }; + 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."); + } + } + } + + [Test] + public async Task TestTryGetObjectPath() + { + var csprojContent = $@" + + netstandard2.0,net10.0 + + + + 1.0.0 + + + 3.5.0 + + +"; + string projName = "TestNamespace"; + File.WriteAllText(Path.Combine(_projectDir!, "src", $"{projName}.csproj"), csprojContent); + MockHelpers.LoadMockGenerator( + inputNamespaceName: projName, + outputPath: _projectDir, + configuration: $"{{\"package-name\": \"{projName}\"}}"); + // Create directory and make sure it is not null now. - Assert.AreEqual(refCountBefore + 2, refCountAfter, "Should have added two metadata references"); + string expectedOutput = Path.Combine(_projectDir!, "src", "obj", "project.assets.json"); + Assert.That(await GeneratedCodeWorkspace.GetAssetFileOrNull(), Is.EqualTo(expectedOutput)); } /// @@ -357,6 +778,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; } 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