Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@
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;
using Microsoft.TypeSpec.Generator.Primitives;
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
{
Expand Down Expand Up @@ -280,6 +283,147 @@ public async Task PostProcessAsync()
}
}

/// <summary>
/// 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.
/// </summary>
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<string>(
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))
Comment thread
jorgerangel-msft marked this conversation as resolved.
{
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}");
}
}
}

/// <summary>
/// Searches the NuGet global packages folder for a package assembly across all cached versions.
/// Returns the first matching assembly found, preferring newer versions.
/// </summary>
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;
}

/// <summary>
/// Queries configured NuGet feeds to resolve the latest stable version of a package.
/// </summary>
private static async Task<string?> 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<FindPackageByIdResource>();
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<Compilation?> LoadBaselineContract()
{
var packageName = CodeModelGenerator.Instance.TypeFactory.PrimaryNamespace;
Expand Down
Loading
Loading