Skip to content

Fix resolution of the external dependencies. - #11742

Open
Nikolay Rovinskiy (nick863) wants to merge 23 commits into
mainfrom
nirovins/fix_external_package_resolution
Open

Fix resolution of the external dependencies.#11742
Nikolay Rovinskiy (nick863) wants to merge 23 commits into
mainfrom
nirovins/fix_external_package_resolution

Conversation

@nick863

@nick863 Nikolay Rovinskiy (nick863) commented Aug 21, 2026

Copy link
Copy Markdown
Member

Problem: Assume, we have the external assembly defined in a typespec as follows:

@@alternateType(
  Azure.AI.Projects.BingCustomSearchPreviewTool,
  {
    identity: "Azure.AI.Extensions.OpenAI.BingCustomSearchPreviewTool",
    package: "Azure.AI.Extensions.OpenAI",
    minVersion: "3.0.0-alpha.20260820.5",
  },
  "csharp"
);

If the version 3.0.0-alpha.20260820.5 is not present in the repository, the ExternalTypeReferenceResolver will not download the needed assembly and the one already present will be used. This will result in some classes not being found as by default the latest stable version is being downloaded.

Solution: Currently, the external package is resolved as follows:

  1. Try to get the assembly of minVersion from available repository
  2. If it fails, use the version, which has been already downloaded.

In this PR we are adding more logic:

  1. If minVersion is provided, try to download if
  2. If it is not available, get the latest version; If minVersion is prerelease, use the latest version, including the prerelease one.
  3. If minVersion is not provided, use the latest stable version.
  4. Use anything already available.

@pkg-pr-new

pkg-pr-new Bot commented Aug 21, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@typespec/http-client-csharp@11742

commit: ed25bde

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the C# generator’s external NuGet dependency resolution so it can (when needed) fall back to the latest available version (optionally including prereleases) instead of only using a requested minimum version or whatever is already cached.

Changes:

  • Added a helper to enumerate available package versions across enabled NuGet sources.
  • Updated external type resolution to select a version based on MinVersion presence and prerelease status before downloading.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/NugetPackageResolver.cs Adds GetAllVersions helper for collecting versions from enabled NuGet sources.
packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/ExternalTypeReferenceResolver.cs Uses version enumeration to choose a download version when the requested MinVersion isn’t available and to include prereleases when appropriate.
Suppressed comments (1)

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/ExternalTypeReferenceResolver.cs:260

  • new NuGetVersion(external.MinVersion) will throw for an invalid/unsupported version string, which changes behavior compared to the previous string-based flow and ends up being swallowed by the broad catch (reported as "package not found"). Also, versions.Max() throws on an empty sequence, so missing packages/feeds can trigger an exception and skip the intended fallback selection.
                        NuGetVersion minVersion = new(external.MinVersion);
                        IList<NuGetVersion> versions = await NugetPackageResolver.GetAllVersions(external.Package!, nugetSettings, allowPrerelease: minVersion.IsPrerelease);
                        if (versions.Any(x => x == minVersion))
                        {
                            resolvedVersion = external.MinVersion;
                        }
                        else
                        {
                            resolvedVersion = versions.Max()?.ToString();
                        }

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@github-actions

Copy link
Copy Markdown
Contributor

No changes needing a change description found.

@JoshLove-msft

Copy link
Copy Markdown
Contributor

The resolved version should be based on the csproj/central package management. The MinVersion from the decorator is only meant to be used as a safety check - it doesn't influence the version that is used, but it can trigger an error if the min version doesn't align with the resolved version. Can you clarify what issue this is solving?

@nick863

Copy link
Copy Markdown
Member Author

The resolved version should be based on the csproj/central package management. The MinVersion from the decorator is only meant to be used as a safety check - it doesn't influence the version that is used, but it can trigger an error if the min version doesn't align with the resolved version. Can you clarify what issue this is solving?

I have updated the description.

@JoshLove-msft

Copy link
Copy Markdown
Contributor

The resolved version should be based on the csproj/central package management. The MinVersion from the decorator is only meant to be used as a safety check - it doesn't influence the version that is used, but it can trigger an error if the min version doesn't align with the resolved version. Can you clarify what issue this is solving?

I have updated the description.

The minVersion should not be used to influence the version that is downloaded by the generator. It is only meant to be used as a compatibility floor. It is optional - it doesn't have to be specified at all. I'm not sure what problem this is solving.

Copilot AI review requested due to automatic review settings August 22, 2026 00:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/ExternalTypeReferenceResolver.cs:260

  • If the configured feeds return no versions (e.g., package doesn't exist, or only prerelease versions exist but allowPrerelease is false), versions.Max() will throw on an empty sequence and the resolver will fall into the catch path. Handle the empty list explicitly so resolution can fail cleanly without relying on exceptions.
                        NuGetVersion minVersion = new(external.MinVersion);
                        IList<NuGetVersion> versions = await NugetPackageResolver.GetAllVersions(external.Package!, nugetSettings, allowPrerelease: minVersion.IsPrerelease);
                        if (versions.Any(x => x == minVersion))
                        {
                            resolvedVersion = external.MinVersion;
                        }
                        else
                        {
                            resolvedVersion = versions.Max()?.ToString();
                        }

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/ExternalTypeReferenceResolver.cs:266

  • New behavior adds multiple version-selection branches (minVersion present + exact version missing -> pick latest; prerelease minVersion -> include prerelease; minVersion absent -> latest stable). There are existing unit tests for ExternalTypeReferenceResolver, but none cover these new branches. Add tests that exercise: (1) minVersion not in feed selects latest available version; (2) prerelease minVersion allows selecting a prerelease latest; (3) stable minVersion does not select prerelease when only prerelease versions exist.
                    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<NuGetVersion> 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);
                    }

@nick863

Copy link
Copy Markdown
Member Author

The resolved version should be based on the csproj/central package management. The MinVersion from the decorator is only meant to be used as a safety check - it doesn't influence the version that is used, but it can trigger an error if the min version doesn't align with the resolved version. Can you clarify what issue this is solving?

I have updated the description.

The minVersion should not be used to influence the version that is downloaded by the generator. It is only meant to be used as a compatibility floor. It is optional - it doesn't have to be specified at all. I'm not sure what problem this is solving.

The problem is that we did not released the new stable version yet, while the downloaded version is 2.0.0. The logic in ExternalTypeReferenceResolver will try to download the compatible assembly. In this PR I am changing the download logic to help situation when the exact version is not present in the repository.
Without this fix if minVersion is not present, the code generation will fail with cryptic error, because it will try to use the incompatible latest stable version.

@JoshLove-msft

Copy link
Copy Markdown
Contributor

Thanks, that clarifies the reproduction. I think the root fix should be in project-reference resolution rather than selecting a package version from minVersion:

  1. Resolve the target project's ProjectAssetsFile (normally obj/project.assets.json) from the evaluated project and read the NuGet restore graph. This gives us the exact package version selected by the .csproj/central package management, including prereleases, ranges, and transitive dependencies.
  2. For each external package, locate that exact package/version and use the compile asset selected for the applicable target framework instead of probing the highest cached version or querying feeds for a latest version. Register dependency assemblies from the same assets target so the generator and eventual SDK build use one consistent graph.
  3. Parse minVersion only as a compatibility floor. If the resolved project version is lower, emit an actionable diagnostic containing the package name, resolved version, and required minimum. If it is equal or higher, use the project-resolved version even when the exact minimum version was never published.
  4. If the assets file is missing/stale, or the external package is absent from the restored graph, report that the project must be restored or add the required PackageReference; do not silently choose a different feed version. Improving this diagnostic also addresses the current cryptic failure.
  5. Add tests for a centrally managed prerelease, a resolved version newer than a nonexistent minimum, omitted minVersion, a resolved version below the floor, multiple cached versions (the assets-selected version must win), and missing assets/package entries.

With that flow, the reported case resolves the project's 3.0.0-alpha... package regardless of whether the decorator's floor exists as an exact package version, while avoiding loading an assembly different from the one used to compile the SDK. The new GetAllVersions/latest-version selection would not be needed.

--generated by Copilot

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Copilot AI review requested due to automatic review settings August 24, 2026 20:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Suppressed comments (1)

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs:295

  • Waiting for the process to exit before draining either redirected stream can deadlock once dotnet restore fills an OS pipe buffer. This codebase already handles the same failure mode by reading stdout and stderr concurrently in GeneratorHandler.ReadProcessOutput (lines 340-346); start both reads before awaiting process exit here as well.
            if (restore.Start())
            {
                await restore.WaitForExitAsync();
                if (restore.ExitCode != 0)
                {
                    string output = await restore.StandardOutput.ReadToEndAsync();

Copilot AI review requested due to automatic review settings August 31, 2026 18:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

Suppressed comments (5)

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs:318

  • projectFileDependencyGroups contains the requested constraint, not the version selected by restore. For example, when a project requests 1.0.0 but only 2.0.0 exists, NuGet writes Package >= 1.0.0 here while targets contains Package/2.0.0. This code therefore passes 1.0.0 to the exact-version lookup and rejects the package that restore just downloaded—the missing-minVersion scenario this PR is intended to fix. Read the selected version (and ideally its compile asset) from targets instead.
                                    // We only support the greater-than-or-equal relation.
                                    // Example: "My.Package >= 1.1.1"
                                    if (packageVersionRelation.Length == 3 && string.Equals(packageVersionRelation[1], ">="))
                                    {
                                        hshFrameworks[currentFramework.Framework][packageVersionRelation[0].ToLower()] = packageVersionRelation[2];

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs:177

  • This fake assembly does not reference Azure.Core, but its nuspec declares Azure.Core 1.61.0. Because the test redirects NUGET_PACKAGES to an isolated cache and now runs dotnet restore, this forces a real feed lookup/download and makes the unit test non-hermetic. Remove the unused dependency, consistent with test/common/FakeNuGetPackage.cs:35-53, which only emits dependencies explicitly required by the test assembly.
                <dependencies>

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs:754

  • Every package created by this helper unnecessarily depends on Azure.Core 1.61.0. The new production path runs dotnet restore against an isolated fake cache, so these otherwise self-contained tests must contact an external feed. Emit no dependencies here, as the shared hermetic helper does in test/common/FakeNuGetPackage.cs:35-53.
                <dependencies>

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs:739

  • NuGet's global-packages layout expects the manifest filename to use the normalized lowercase package ID. This mixed-case filename is not found by the exact-version local repository lookup on case-sensitive systems, so the newly added version-selection tests fail there. Match the shared helper at test/common/FakeNuGetPackage.cs:111.
            File.WriteAllText(Path.Combine(metadataPath, $"{packageName}.nuspec"), $"""

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/ExternalTypeReferenceResolver.cs:21

  • After this change the resolver only consults the cache, but its failure reason at line 244 and the fallback diagnostics in TypeFactory.cs:373-374 still claim that configured feeds were searched. Package failures will therefore tell users that a feed lookup occurred when it did not. Update those messages to describe project restore/cache lookup.
    /// looking up the package in the NuGet global cache and loading the assembly via reflection.
    /// Used by <c>TypeFactory.CreateExternalType</c>

Copilot AI review requested due to automatic review settings August 31, 2026 18:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Suppressed comments (3)

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs:339

  • Deriving the assets directory from OutputPath only works when output and intermediate paths follow the bin/obj convention. Valid projects can configure OutputPath and BaseIntermediateOutputPath independently, causing this method to return null even though restore produced an assets file. Query MSBuild's ProjectAssetsFile (or MSBuildProjectExtensionsPath) directly instead of rewriting OutputPath.
                ArgumentList = { "msbuild", projectFilePath, "-getProperty:OutputPath" },

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs:480

  • This local is never used and produces CS0219. Since the generator tree enables warnings as errors in generator/Directory.Build.props:29, the test project will not build.
            var nugetCacheDir = Path.Combine(_tempDirectory!, "NuGetCache");

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs:739

  • NuGet's global-packages layout requires the nuspec filename to use the lowercased package ID. With names such as First.Package, this uppercase filename prevents NuGet from recognizing the fake package on case-sensitive systems; the shared helper uses the required form at test/common/FakeNuGetPackage.cs:111.
            File.WriteAllText(Path.Combine(metadataPath, $"{packageName}.nuspec"), $"""

Copilot AI review requested due to automatic review settings August 31, 2026 20:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Suppressed comments (4)

Previously missed (1) — in code that hasn't changed since the last review.

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs:697

  • NuGet's global-packages layout uses the normalized lowercase package ID for the manifest filename (test/common/FakeNuGetPackage.cs:111). With names such as First.Package, this writes First.Package.nuspec; on case-sensitive systems NuGetv3LocalRepository.FindPackage cannot find the fake package, so the exact-version tests fail. Normalize this filename too.
            File.WriteAllText(Path.Combine(metadataPath, $"{packageName}.nuspec"), $"""

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs:477

  • This local is never used, so it emits CS0219. Because generator/Directory.Build.props:29 treats warnings as errors, the test project will not compile; remove it.
            var nugetCacheDir = Path.Combine(_tempDirectory!, "NuGetCache");

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs:180

  • This fake package declares a real Azure.Core dependency even though the test only needs the package DLL. Since NUGET_PACKAGES points to an empty temporary cache and the production path now runs dotnet restore, this forces the unit test to contact a NuGet feed and fail in offline/restricted environments. Keep the fake package dependency-free, as test/common/FakeNuGetPackage.cs:90-111 does when no dependencies are requested.
                <dependencies>
                  <group targetFramework="net10.0">
                    <dependency id="Azure.Core" version="1.61.0" exclude="Build,Analyzers" />
                  </group>

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs:715

  • Every package created by this helper gains a real Azure.Core dependency. The tests now execute dotnet restore against an otherwise empty temporary cache, making them network-dependent even though none uses Azure.Core. Keep these fake packages dependency-free, matching test/common/FakeNuGetPackage.cs:90-111 when no dependencies are requested.
                <dependencies>
                  <group targetFramework="net10.0">
                    <dependency id="Azure.Core" version="1.61.0" exclude="Build,Analyzers" />
                  </group>

Copilot AI review requested due to automatic review settings August 31, 2026 20:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Suppressed comments (4)

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/ExternalTypeReferenceResolver.cs:237

  • Removing the download fallback makes package-backed external types fail on a clean machine whenever the package is not already represented by an existing .csproj. CSharpGen.ExecuteAsync invokes this resolver before new-project scaffolding writes the project (CSharpGen.cs:34-42,165-168), so AddPackageReferencesFromProject returns early and nothing populates the cache. Keep a feed-resolution/download path driven by InputExternalTypeMetadata (including the stable/prerelease fallback rules), rather than relying exclusively on project restore.
            string? assemblyPath = NugetPackageResolver.FindPackageAssembly(
                globalPackagesFolder, external.Package!, external.MinVersion);

            if (assemblyPath == null || !File.Exists(assemblyPath))

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs:372

  • Parsing framework versions as double does not preserve TFM ordering. For example, supported TFMs net48 and net472 become 0.48 and 4.72, so net472 is incorrectly selected as newer; multi-digit minor versions similarly misorder. Parse TFMs with NuGetFramework.ParseFolder/Version semantics and apply the intended framework-family precedence explicitly.
                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;

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs:284

  • projectFileDependencyGroups contains the declared dependency constraint, not the version NuGet actually selected. For the reported case it remains Package >= 3.0.0-alpha... even when restore falls forward to another version, so this method later looks for the unavailable minimum and discards the successfully restored package. Read the resolved package version from the selected targets graph (or use NuGet's lock-file model) while using this section only to identify direct dependencies.
                if (prop.Value.ValueKind == JsonValueKind.Object && prop.NameEquals("projectFileDependencyGroups"))

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs:479

  • FindPackageAssemblyInVersion deliberately chooses the asset nearest to the generator runtime (see NugetPackageResolver.cs:117-126), not the project target. Even for a netstandard2.0-only project this can register a package's net8.0 assembly, causing custom code to type-check against APIs unavailable in the generated project's target. Preserve the selected assets target and resolve the exact package version's assembly for that TFM (or add a separate exact compile-time lookup).
                string? resolvedAssemblyPath = version is null
                     ? NugetPackageResolver.FindPackageAssembly(globalPackagesFolder, refPackageName)
                     : NugetPackageResolver.FindPackageAssemblyInVersion(globalPackagesFolder, refPackageName, version);

@@ -234,32 +234,6 @@ private static async Task<ResolutionResult> ResolveResultAsync(InputExternalType
string? assemblyPath = NugetPackageResolver.FindPackageAssembly(
globalPackagesFolder, external.Package!, external.MinVersion);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please resolve this assembly from the version selected in the project assets graph rather than applying MinVersion to the entire cache again. AddPackageReferencesFromProject can add 2.5.0 from project.assets.json, but this call independently selects the highest cached version at or above the floor. With the project pinned to 2.5.0, minVersion 2.0.0, and 3.0.0 also cached, the resolver returns the type from 3.0.0. That makes reflection and generation use a different API surface from the SDK build and can mask a project version below the compatibility floor.

--generated by Copilot

string[] packageVersionRelation = (packageAndVersion.GetString() ?? "").Split();
// We only support the greater-than-or-equal relation.
// Example: "My.Package >= 1.1.1"
if (packageVersionRelation.Length == 3 && string.Equals(packageVersionRelation[1], ">="))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read the resolved package version and compile asset from the selected targets entry instead of parsing projectFileDependencyGroups. That section contains requested constraints. A restore of Azure.Core [1.50.0,1.62.0] produces Azure.Core >= 1.50.0 <= 1.62.0 here and Azure.Core/1.50.0 in targets; this three-token-only parser drops the dependency, after which the caller can select cached 1.62.0. Ranged and floating dependencies therefore still bind a different package than restore.

--generated by Copilot

string? version = default;
hshNameVersion.TryGetValue(refPackageName.ToLower(), out version);
string? resolvedAssemblyPath = version is null
? NugetPackageResolver.FindPackageAssembly(globalPackagesFolder, refPackageName)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This remains reproducible at the current head. AddPackageReferencesFromProject_ResolvesPackageWithNoVersion runs a project whose restore exits with NU1015, but execution continues and version being null makes the method load cached 4.2.0; the test passes. After any restore, MSBuild, or assets failure, return or skip instead of probing an unverified cache, otherwise generation can succeed against a package the project cannot build with.

--generated by Copilot

Match numeral = Regex.Match(name, "\\d+[.]*\\d*$");
if (numeral.Success)
{
current = double.Parse(numeral.Value);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This regression is present again at the current head. With CurrentCulture set to fr-FR, double.Parse throws FormatException for net8.0 at this line, so package-reference resolution fails on affected locales. Use InvariantCulture or compare parsed framework versions without double.

--generated by Copilot

@jorgerangel-msft Jorge Rangel (jorgerangel-msft) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a few comments

return hshFrameworks;
}

internal static async Task<string?> TryGetAssetsFile()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we use the .net pattern for TryGet here or rename the method to something else if we don't want to use that pattern ?


internal static async Task<string?> TryGetAssetsFile()
{
string projectFilePath = Path.GetFullPath(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we validate this file exists before we pass it to the process ?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Suppressed comments (4)

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/ExternalTypeReferenceResolver.cs:21

  • Removing the feed fallback makes this resolver cache-only, so it cannot implement the PR's stated missing-version behavior. AddPackageReferencesFromProject restores only packages already present as <PackageReference> items; a package supplied solely by InputExternalTypeMetadata is not added there, and CSharpGen invokes this resolver separately. Consequently the example package remains unresolved when absent from the cache, and the described stable/prerelease fallback never runs. The download/version-selection logic needs to remain in this resolver (including prerelease-aware latest-version selection).
    /// looking up the package in the NuGet global cache and loading the assembly via reflection.
    /// Used by <c>TypeFactory.CreateExternalType</c>

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/PostProcessing/GeneratedCodeWorkspace.cs:305

  • projectFileDependencyGroups contains the declared dependency constraint, not the version NuGet actually selected. If 3.5.0 is unavailable and restore resolves 3.6.0, this value remains Package >= 3.5.0; the later exact FindPackageAssemblyInVersion(..., "3.5.0") lookup therefore misses the package that was just restored. Read the selected package identity/version from the chosen target (or NuGet's lock-file model) while using this group only to identify direct dependencies.
                                    string[] packageVersionRelation = (packageAndVersion.GetString() ?? "").Split();
                                    // We only support the greater-than-or-equal relation.
                                    // Example: "My.Package >= 1.1.1"
                                    if (packageVersionRelation.Length == 3 && string.Equals(packageVersionRelation[1], ">="))
                                    {
                                        hshFrameworks[currentFramework.Framework][packageVersionRelation[0].ToLower()] = packageVersionRelation[2];

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs:180

  • This fake assembly does not reference Azure.Core, but its new nuspec declares that real dependency. Because the test isolates NUGET_PACKAGES and the production path now runs dotnet restore, the test must contact a feed for Azure.Core and may fail—or bypass the assets-file path—offline. Keep the fake package hermetic by declaring no dependencies, as the shared helper does in test/common/FakeNuGetPackage.cs:84-126.
                <dependencies>
                  <group targetFramework="net10.0">
                    <dependency id="Azure.Core" version="1.61.0" exclude="Build,Analyzers" />
                  </group>

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs:715

  • Every package produced by this helper now declares an unused real Azure.Core dependency. Since these tests use an isolated package cache and call dotnet restore, that makes their success depend on remote feed availability instead of remaining hermetic. Use an empty dependency set (or the existing FakeNuGetPackage.Create helper in test/common/FakeNuGetPackage.cs:41-76).
                <dependencies>
                  <group targetFramework="net10.0">
                    <dependency id="Azure.Core" version="1.61.0" exclude="Build,Analyzers" />
                  </group>

Copilot AI review requested due to automatic review settings September 3, 2026 01:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The fallback behavior is incomplete, first-time generation can no longer resolve external packages, and several new tests are invalid or non-hermetic.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (5)

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/ExternalTypeReferenceResolver.cs:261

  • This branch returns immediately when the project's resolved version is below MinVersion, which contradicts the stated fallback sequence: it should first try MinVersion, then the latest eligible version, and finally an already available version. The new TryResolve_ReturnsNullForHigherMinVersion test currently codifies the opposite behavior. Keep the NuGet download/version-selection fallback here instead of caching a terminal failure.
            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}\"."));

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/ExternalTypeReferenceResolver.cs:271

  • Resolution now fails whenever the package was not already added from the project. This breaks first-time --new-project generation: AddPackageReferencesFromProject returns because the csproj does not exist yet, and CSharpGen only writes that csproj at the end of generation (lines 165-168). Preserve a direct NuGet resolution fallback based on external.Package/MinVersion before reporting the package missing.
            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} is not present in package dependencies."));

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs:186

  • This fake package now depends on a real Azure.Core version even though its emitted source does not use Azure.Core. Since the test switches NUGET_PACKAGES to a fresh directory and then runs dotnet restore, it must contact external feeds, making the unit test network/version dependent. Use the hermetic FakeNuGetPackage.Create pattern (test/common/FakeNuGetPackage.cs:41-76) with a locally created fake dependency, or omit this dependency.
                <dependencies>
                  <group targetFramework="net10.0">
                    <dependency id="Azure.Core" version="1.61.0" exclude="Build,Analyzers" />
                  </group>
                  <group targetFramework="net8.0">
                    <dependency id="Azure.Core" version="1.61.0" exclude="Build,Analyzers" />
                  </group>
                  <group targetFramework=".NETStandard2.0">
                    <dependency id="Azure.Core" version="1.61.0" exclude="Build,Analyzers" />
                  </group>

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs:816

  • This duplicated fake nuspec also introduces a real Azure.Core dependency into tests that run dotnet restore against a fresh package cache. That makes these unit cases depend on external feed availability. Follow test/common/FakeNuGetPackage.cs:41-76 and create all needed packages locally, or remove the unused dependency declaration.
                <dependencies>
                  <group targetFramework="net10.0">
                    <dependency id="Azure.Core" version="1.61.0" exclude="Build,Analyzers" />
                  </group>
                  <group targetFramework="net8.0">
                    <dependency id="Azure.Core" version="1.61.0" exclude="Build,Analyzers" />
                  </group>
                  <group targetFramework=".NETStandard2.0">
                    <dependency id="Azure.Core" version="1.61.0" exclude="Build,Analyzers" />
                  </group>

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/GeneratedCodeWorkspaceTests.cs:792

  • NuGet's global-packages layout uses the normalized lowercase package ID for the nuspec filename. Writing First.Package.nuspec on a case-sensitive filesystem means NuGetv3LocalRepository.FindPackage may not recognize this fake package, causing the new restore-based tests to fail on Linux. Normalize the filename as the shared FakeNuGetPackage helper does.
            File.WriteAllText(Path.Combine(metadataPath, $"{packageName}.nuspec"), $"""
  • Files reviewed: 5/5 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment on lines +298 to +302
string[] packageVersion = packageAndVersion.Name.Split('/');
if (packageVersion.Length == 2)
{
hshFrameworks[currentFramework.GetShortFolderName()][packageVersion[0].ToLower()] = packageVersion[1];
}
}
var csprojContent = $@"<Project Sdk=""Microsoft.NET.Sdk"">
<PropertyGroup>
<TargetFramework>netstandard2.0,net10.0</TargetFramework>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

emitter:client:csharp Issue for the C# client emitter: @typespec/http-client-csharp

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants