Skip to content

Commit 1f8c6e8

Browse files
fix(http-client-csharp): resolve PackageReference assemblies for cust… (#10229)
…om code compilation When custom code references types from external NuGet packages (e.g., Azure.Storage.Common.StorageSharedKeyCredential), the Roslyn compilation would fail because those assemblies weren't added as metadata references. Parse the project's .csproj file for PackageReference items and resolve their assemblies from the NuGet global packages cache. This allows custom constructors and other user code that references external library types to compile correctly. Fixes #10224 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0680ffb commit 1f8c6e8

3 files changed

Lines changed: 387 additions & 0 deletions

File tree

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/CSharpGen.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ public async Task ExecuteAsync()
3232
var outputPath = CodeModelGenerator.Instance.Configuration.OutputDirectory;
3333
var generatedSourceOutputPath = CodeModelGenerator.Instance.Configuration.ProjectGeneratedDirectory;
3434

35+
// Resolve PackageReference items from the .csproj so custom code referencing
36+
// external NuGet types (e.g., Azure.Storage.Common) compiles correctly.
37+
await GeneratedCodeWorkspace.AddPackageReferencesFromProject();
38+
3539
GeneratedCodeWorkspace customCodeWorkspace = await GeneratedCodeWorkspace.Create(isCustomCodeProject: true);
3640
// The generated attributes need to be added into the workspace before loading the custom code. Otherwise,
3741
// Roslyn doesn't load the attributes completely and we are unable to get the attribute arguments.

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

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@
1010
using System.Threading.Tasks;
1111
using Microsoft.Build.Construction;
1212
using Microsoft.CodeAnalysis;
13+
using MSBuildProjectCollection = Microsoft.Build.Evaluation.ProjectCollection;
1314
using Microsoft.CodeAnalysis.CSharp;
1415
using Microsoft.CodeAnalysis.Formatting;
1516
using Microsoft.CodeAnalysis.Simplification;
1617
using Microsoft.TypeSpec.Generator.Primitives;
1718
using Microsoft.TypeSpec.Generator.Providers;
1819
using Microsoft.TypeSpec.Generator.Utilities;
1920
using NuGet.Configuration;
21+
using NuGet.Protocol;
22+
using NuGet.Protocol.Core.Types;
2023

2124
namespace Microsoft.TypeSpec.Generator
2225
{
@@ -280,6 +283,147 @@ public async Task PostProcessAsync()
280283
}
281284
}
282285

286+
/// <summary>
287+
/// Resolves PackageReference items from the project's .csproj file and adds their assemblies
288+
/// as metadata references so that custom code referencing external NuGet types compiles correctly.
289+
/// </summary>
290+
internal static async Task AddPackageReferencesFromProject()
291+
{
292+
var packageName = CodeModelGenerator.Instance.Configuration.PackageName;
293+
string projectFilePath = Path.GetFullPath(
294+
Path.Combine(CodeModelGenerator.Instance.Configuration.ProjectDirectory, $"{packageName}.csproj"));
295+
296+
if (!File.Exists(projectFilePath))
297+
{
298+
return;
299+
}
300+
301+
var projectRoot = ProjectRootElement.Open(projectFilePath, new MSBuildProjectCollection());
302+
303+
var nugetSettings = Settings.LoadDefaultSettings(projectFilePath);
304+
var globalPackagesFolder = SettingsUtility.GetGlobalPackagesFolder(nugetSettings);
305+
306+
// Build a set of assembly names already registered so we can skip them
307+
var existingRefs = new HashSet<string>(
308+
CodeModelGenerator.Instance.AdditionalMetadataReferences
309+
.Where(r => r.Display is not null)
310+
.Select(r => Path.GetFileNameWithoutExtension(r.Display!))
311+
.Where(n => !string.IsNullOrEmpty(n)),
312+
StringComparer.OrdinalIgnoreCase);
313+
314+
foreach (var item in projectRoot.Items.Where(i => i.ItemType == "PackageReference"))
315+
{
316+
var refPackageName = item.Include;
317+
318+
if (string.IsNullOrEmpty(refPackageName))
319+
{
320+
continue;
321+
}
322+
323+
// Skip packages already added as metadata references (e.g., by a plugin)
324+
if (existingRefs.Contains(refPackageName))
325+
{
326+
continue;
327+
}
328+
329+
// Search the NuGet global packages folder for any cached version of this package.
330+
string? resolvedAssemblyPath = FindPackageAssembly(globalPackagesFolder, refPackageName);
331+
332+
// If not found in cache, download the latest version from NuGet feeds
333+
if (resolvedAssemblyPath == null)
334+
{
335+
try
336+
{
337+
var latestVersion = await ResolveLatestPackageVersion(refPackageName, nugetSettings);
338+
if (latestVersion != null)
339+
{
340+
var downloader = new NugetPackageDownloader(refPackageName, latestVersion, null, nugetSettings);
341+
var downloadedPath = await downloader.DownloadAndInstallPackage();
342+
var downloadedAssembly = Path.Combine(downloadedPath, $"{refPackageName}.dll");
343+
if (File.Exists(downloadedAssembly))
344+
{
345+
resolvedAssemblyPath = downloadedAssembly;
346+
}
347+
}
348+
}
349+
catch (Exception ex)
350+
{
351+
CodeModelGenerator.Instance.Emitter.Debug(
352+
$"Could not download package {refPackageName}: {ex.Message}");
353+
}
354+
}
355+
356+
if (resolvedAssemblyPath != null)
357+
{
358+
CodeModelGenerator.Instance.AddMetadataReference(
359+
MetadataReference.CreateFromFile(resolvedAssemblyPath));
360+
CodeModelGenerator.Instance.Emitter.Debug(
361+
$"Added metadata reference: {refPackageName} from {resolvedAssemblyPath}");
362+
}
363+
}
364+
}
365+
366+
/// <summary>
367+
/// Searches the NuGet global packages folder for a package assembly across all cached versions.
368+
/// Returns the first matching assembly found, preferring newer versions.
369+
/// </summary>
370+
private static string? FindPackageAssembly(string globalPackagesFolder, string packageName)
371+
{
372+
var packageDir = Path.Combine(globalPackagesFolder, packageName.ToLowerInvariant());
373+
374+
if (!Directory.Exists(packageDir))
375+
{
376+
return null;
377+
}
378+
379+
foreach (var versionDir in Directory.GetDirectories(packageDir).OrderDescending())
380+
{
381+
foreach (var tfm in NugetPackageDownloader.PreferredDotNetFrameworkVersions)
382+
{
383+
var assemblyPath = Path.Combine(versionDir, "lib", tfm, $"{packageName}.dll");
384+
if (File.Exists(assemblyPath))
385+
{
386+
return assemblyPath;
387+
}
388+
}
389+
}
390+
391+
return null;
392+
}
393+
394+
/// <summary>
395+
/// Queries configured NuGet feeds to resolve the latest stable version of a package.
396+
/// </summary>
397+
private static async Task<string?> ResolveLatestPackageVersion(string packageName, ISettings nugetSettings)
398+
{
399+
var sources = SettingsUtility.GetEnabledSources(nugetSettings);
400+
using var cacheContext = new SourceCacheContext();
401+
foreach (var source in sources)
402+
{
403+
try
404+
{
405+
var repository = Repository.Factory.GetCoreV3(source.Source);
406+
var resource = await repository.GetResourceAsync<FindPackageByIdResource>();
407+
var versions = await resource.GetAllVersionsAsync(
408+
packageName, cacheContext, NuGet.Common.NullLogger.Instance, CancellationToken.None);
409+
var latest = versions?
410+
.Where(v => !v.IsPrerelease)
411+
.OrderByDescending(v => v)
412+
.FirstOrDefault();
413+
if (latest != null)
414+
{
415+
return latest.ToString();
416+
}
417+
}
418+
catch
419+
{
420+
// Skip sources that fail (auth, network, etc.)
421+
}
422+
}
423+
424+
return null;
425+
}
426+
283427
internal static async Task<Compilation?> LoadBaselineContract()
284428
{
285429
var packageName = CodeModelGenerator.Instance.TypeFactory.PrimaryNamespace;

0 commit comments

Comments
 (0)