diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs
index 777c96bf558..9f31bebadcd 100644
--- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs
@@ -32,6 +32,10 @@ public async Task ExecuteAsync()
var outputPath = CodeModelGenerator.Instance.Configuration.OutputDirectory;
var generatedSourceOutputPath = CodeModelGenerator.Instance.Configuration.ProjectGeneratedDirectory;
+ // Resolve PackageReference items from the .csproj so custom code referencing
+ // external NuGet types (e.g., Azure.Storage.Common) compiles correctly.
+ await GeneratedCodeWorkspace.AddPackageReferencesFromProject();
+
GeneratedCodeWorkspace customCodeWorkspace = await GeneratedCodeWorkspace.Create(isCustomCodeProject: true);
// The generated attributes need to be added into the workspace before loading the custom code. Otherwise,
// Roslyn doesn't load the attributes completely and we are unable to get the attribute arguments.
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 6a389540e83..431b5443059 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
@@ -10,6 +10,7 @@
using System.Threading.Tasks;
using Microsoft.Build.Construction;
using Microsoft.CodeAnalysis;
+using MSBuildProjectCollection = Microsoft.Build.Evaluation.ProjectCollection;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Simplification;
@@ -17,6 +18,8 @@
using Microsoft.TypeSpec.Generator.Providers;
using Microsoft.TypeSpec.Generator.Utilities;
using NuGet.Configuration;
+using NuGet.Protocol;
+using NuGet.Protocol.Core.Types;
namespace Microsoft.TypeSpec.Generator
{
@@ -280,6 +283,147 @@ public async Task PostProcessAsync()
}
}
+ ///
+ /// 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.
+ ///
+ internal static async Task AddPackageReferencesFromProject()
+ {
+ var packageName = CodeModelGenerator.Instance.Configuration.PackageName;
+ string projectFilePath = Path.GetFullPath(
+ Path.Combine(CodeModelGenerator.Instance.Configuration.ProjectDirectory, $"{packageName}.csproj"));
+
+ if (!File.Exists(projectFilePath))
+ {
+ return;
+ }
+
+ var projectRoot = ProjectRootElement.Open(projectFilePath, new MSBuildProjectCollection());
+
+ var nugetSettings = Settings.LoadDefaultSettings(projectFilePath);
+ var globalPackagesFolder = SettingsUtility.GetGlobalPackagesFolder(nugetSettings);
+
+ // 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);
+
+ foreach (var item in projectRoot.Items.Where(i => i.ItemType == "PackageReference"))
+ {
+ var refPackageName = item.Include;
+
+ if (string.IsNullOrEmpty(refPackageName))
+ {
+ continue;
+ }
+
+ // Skip packages already added as metadata references (e.g., by a plugin)
+ if (existingRefs.Contains(refPackageName))
+ {
+ continue;
+ }
+
+ // Search the NuGet global packages folder for any cached version of this package.
+ string? resolvedAssemblyPath = FindPackageAssembly(globalPackagesFolder, refPackageName);
+
+ // If not found in cache, download the latest version from NuGet feeds
+ if (resolvedAssemblyPath == null)
+ {
+ try
+ {
+ var latestVersion = await 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}");
+ }
+ }
+
+ if (resolvedAssemblyPath != null)
+ {
+ CodeModelGenerator.Instance.AddMetadataReference(
+ MetadataReference.CreateFromFile(resolvedAssemblyPath));
+ CodeModelGenerator.Instance.Emitter.Debug(
+ $"Added metadata reference: {refPackageName} from {resolvedAssemblyPath}");
+ }
+ }
+ }
+
+ ///
+ /// Searches the NuGet global packages folder for a package assembly across all cached versions.
+ /// Returns the first matching assembly found, preferring newer versions.
+ ///
+ private static string? FindPackageAssembly(string globalPackagesFolder, string packageName)
+ {
+ var packageDir = Path.Combine(globalPackagesFolder, packageName.ToLowerInvariant());
+
+ if (!Directory.Exists(packageDir))
+ {
+ return null;
+ }
+
+ foreach (var versionDir in Directory.GetDirectories(packageDir).OrderDescending())
+ {
+ foreach (var tfm in NugetPackageDownloader.PreferredDotNetFrameworkVersions)
+ {
+ var assemblyPath = Path.Combine(versionDir, "lib", tfm, $"{packageName}.dll");
+ if (File.Exists(assemblyPath))
+ {
+ return assemblyPath;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// Queries configured NuGet feeds to resolve the latest stable version of a package.
+ ///
+ private static async Task ResolveLatestPackageVersion(string packageName, ISettings nugetSettings)
+ {
+ 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();
+ var versions = await resource.GetAllVersionsAsync(
+ packageName, cacheContext, NuGet.Common.NullLogger.Instance, CancellationToken.None);
+ var latest = versions?
+ .Where(v => !v.IsPrerelease)
+ .OrderByDescending(v => v)
+ .FirstOrDefault();
+ if (latest != null)
+ {
+ return latest.ToString();
+ }
+ }
+ catch
+ {
+ // Skip sources that fail (auth, network, etc.)
+ }
+ }
+
+ return null;
+ }
+
internal static async Task LoadBaselineContract()
{
var packageName = CodeModelGenerator.Instance.TypeFactory.PrimaryNamespace;
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 62c2fa12635..0a75d8c9360 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
@@ -97,6 +97,245 @@ await MockHelpers.LoadMockGeneratorAsync(
Assert.NotNull(fooMethod, "Foo method should be found in the SimpleType");
}
+ [Test]
+ public async Task AddPackageReferencesFromProject_AddsReferencesFromCsproj()
+ {
+ var ns = "TestNamespace";
+ var nugetCacheDir = Path.Combine(_tempDirectory!, "NuGetCache");
+
+ // Create a fake external package assembly in the NuGet cache
+ var externalPkgName = "My.External.Library";
+ var externalPkgVersion = "2.0.0";
+ var externalPkgDir = Path.Combine(
+ nugetCacheDir, externalPkgName.ToLowerInvariant(), externalPkgVersion, "lib", "netstandard2.0");
+ Directory.CreateDirectory(externalPkgDir);
+
+ var externalSyntaxTree = CSharpSyntaxTree.ParseText(@"
+namespace My.External.Library
+{
+ public class ExternalCredential { }
+}");
+ var externalCompilation = CSharpCompilation.Create(
+ externalPkgName,
+ [externalSyntaxTree],
+ [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)],
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+ var externalDllPath = Path.Combine(externalPkgDir, $"{externalPkgName}.dll");
+ var emitResult = externalCompilation.Emit(externalDllPath);
+ Assert.IsTrue(emitResult.Success, "Failed to emit external test assembly");
+
+ // Create a .csproj with a PackageReference to the external package
+ var csprojContent = $@"
+
+ netstandard2.0
+
+
+
+ {externalPkgVersion}
+
+
+";
+ var csProjPath = Path.Combine(_projectDir!, "src", $"{ns}.csproj");
+ File.WriteAllText(csProjPath, csprojContent);
+
+ MockHelpers.LoadMockGenerator(
+ inputNamespaceName: ns,
+ outputPath: _projectDir,
+ configuration: $"{{\"package-name\": \"{ns}\"}}");
+
+ var refCountBefore = CodeModelGenerator.Instance.AdditionalMetadataReferences.Count;
+ await GeneratedCodeWorkspace.AddPackageReferencesFromProject();
+ var refCountAfter = CodeModelGenerator.Instance.AdditionalMetadataReferences.Count;
+
+ Assert.AreEqual(refCountBefore + 1, refCountAfter, "Should have added one metadata reference");
+ }
+
+ [Test]
+ public async Task AddPackageReferencesFromProject_SkipsWhenNoCsproj()
+ {
+ // Use a namespace that doesn't match any .csproj in the project dir
+ MockHelpers.LoadMockGenerator(
+ inputNamespaceName: "NonExistentNamespace",
+ outputPath: _projectDir,
+ configuration: "{\"package-name\": \"NonExistentNamespace\"}");
+
+ var refCountBefore = CodeModelGenerator.Instance.AdditionalMetadataReferences.Count;
+ await GeneratedCodeWorkspace.AddPackageReferencesFromProject();
+ var refCountAfter = CodeModelGenerator.Instance.AdditionalMetadataReferences.Count;
+
+ Assert.AreEqual(refCountBefore, refCountAfter, "Should not add references when no .csproj exists");
+ }
+
+ [Test]
+ public async Task AddPackageReferencesFromProject_SkipsPackageNotInCache()
+ {
+ var ns = "TestNamespace";
+
+ // Create a .csproj referencing a package that doesn't exist in
+ // the cache or on any NuGet feed — should gracefully skip it.
+ var csprojContent = @"
+
+ netstandard2.0
+
+
+
+ 1.0.0
+
+
+";
+ var csProjPath = Path.Combine(_projectDir!, "src", $"{ns}.csproj");
+ File.WriteAllText(csProjPath, csprojContent);
+
+ MockHelpers.LoadMockGenerator(
+ inputNamespaceName: ns,
+ outputPath: _projectDir,
+ configuration: $"{{\"package-name\": \"{ns}\"}}");
+
+ var refCountBefore = CodeModelGenerator.Instance.AdditionalMetadataReferences.Count;
+ await GeneratedCodeWorkspace.AddPackageReferencesFromProject();
+ var refCountAfter = CodeModelGenerator.Instance.AdditionalMetadataReferences.Count;
+
+ Assert.AreEqual(refCountBefore, refCountAfter, "Should not add references for packages not in cache");
+ }
+
+ [Test]
+ public async Task AddPackageReferencesFromProject_ResolvesPackageWithNoVersion()
+ {
+ var ns = "TestNamespace";
+ var nugetCacheDir = Path.Combine(_tempDirectory!, "NuGetCache");
+
+ // Create a fake package in the cache (simulating a centrally managed package)
+ var externalPkgName = "Centrally.Managed.Package";
+ CreateFakeNuGetPackage(nugetCacheDir, externalPkgName, "4.2.0");
+
+ // Create a .csproj with no Version on the PackageReference
+ var csprojContent = $@"
+
+ netstandard2.0
+
+
+
+
+";
+ File.WriteAllText(Path.Combine(_projectDir!, "src", $"{ns}.csproj"), csprojContent);
+
+ MockHelpers.LoadMockGenerator(
+ inputNamespaceName: ns,
+ outputPath: _projectDir,
+ configuration: $"{{\"package-name\": \"{ns}\"}}");
+
+ var refCountBefore = CodeModelGenerator.Instance.AdditionalMetadataReferences.Count;
+ await GeneratedCodeWorkspace.AddPackageReferencesFromProject();
+ var refCountAfter = CodeModelGenerator.Instance.AdditionalMetadataReferences.Count;
+
+ Assert.AreEqual(refCountBefore + 1, refCountAfter,
+ "Should resolve package from cache even without a version (centrally managed)");
+ }
+
+ [Test]
+ public async Task AddPackageReferencesFromProject_SkipsAlreadyAddedReferences()
+ {
+ var ns = "TestNamespace";
+ var nugetCacheDir = Path.Combine(_tempDirectory!, "NuGetCache");
+
+ // Create a fake external package assembly in the NuGet cache
+ var externalPkgName = "Already.Added.Package";
+ var externalPkgVersion = "1.0.0";
+ var dllPath = CreateFakeNuGetPackage(nugetCacheDir, externalPkgName, externalPkgVersion);
+
+ // Create a .csproj referencing the package
+ var csprojContent = $@"
+
+ netstandard2.0
+
+
+
+ {externalPkgVersion}
+
+
+";
+ File.WriteAllText(Path.Combine(_projectDir!, "src", $"{ns}.csproj"), csprojContent);
+
+ MockHelpers.LoadMockGenerator(
+ inputNamespaceName: ns,
+ outputPath: _projectDir,
+ configuration: $"{{\"package-name\": \"{ns}\"}}");
+
+ // Pre-add the reference (simulating a plugin that already added it)
+ CodeModelGenerator.Instance.AddMetadataReference(
+ MetadataReference.CreateFromFile(dllPath));
+
+ var refCountBefore = CodeModelGenerator.Instance.AdditionalMetadataReferences.Count;
+ await GeneratedCodeWorkspace.AddPackageReferencesFromProject();
+ var refCountAfter = CodeModelGenerator.Instance.AdditionalMetadataReferences.Count;
+
+ Assert.AreEqual(refCountBefore, refCountAfter,
+ "Should not add duplicate reference for a package already in AdditionalMetadataReferences");
+ }
+
+ [Test]
+ public async Task AddPackageReferencesFromProject_AddsMultiplePackageReferences()
+ {
+ var ns = "TestNamespace";
+ var nugetCacheDir = Path.Combine(_tempDirectory!, "NuGetCache");
+
+ // Create two fake packages in the cache
+ CreateFakeNuGetPackage(nugetCacheDir, "First.Package", "1.0.0");
+ CreateFakeNuGetPackage(nugetCacheDir, "Second.Package", "3.5.0");
+
+ var csprojContent = @"
+
+ netstandard2.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}\"}}");
+
+ 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");
+ }
+
+ ///
+ /// Creates a fake NuGet package assembly in the given cache directory and returns the DLL path.
+ ///
+ private static string CreateFakeNuGetPackage(string nugetCacheDir, string packageName, string version)
+ {
+ var pkgDir = Path.Combine(
+ nugetCacheDir, packageName.ToLowerInvariant(), version, "lib", "netstandard2.0");
+ Directory.CreateDirectory(pkgDir);
+
+ var syntaxTree = CSharpSyntaxTree.ParseText($@"
+namespace {packageName}
+{{
+ public class Placeholder {{ }}
+}}");
+ var compilation = CSharpCompilation.Create(
+ packageName,
+ [syntaxTree],
+ [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)],
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+
+ var dllPath = Path.Combine(pkgDir, $"{packageName}.dll");
+ var result = compilation.Emit(dllPath);
+ Assert.IsTrue(result.Success, $"Failed to emit fake assembly for {packageName}");
+ return dllPath;
+ }
+
private void CreateTestAssemblyAndProjectFile(string nugetCacheDir, string csProjectFileName)
{
var ns = csProjectFileName.StartsWith("TestNamespaceUnevaluatedFrameworkValue")