basic-reference-assemblies icon indicating copy to clipboard operation
basic-reference-assemblies copied to clipboard

Usage with `AssemblyBuilder`/`MetadataLoadContext`

Open Windows10CE opened this issue 1 year ago • 1 comments

As mentioned in the new documentation for persistable dynamic assemblies in .NET 9 here, to create assemblies for specific TFMs (or assemblies that can actually be used as references in modern C# projects), the new API requires you to create a MetadataLoadContext using the reference assemblies for that TFM. As is already noted by this package's README, this can be challenging!

Adding a bit of documentation that myself and others can point to (or even a helper method to create a MetadataLoadContext like exists for Compilations) would help a lot (I am completely willing to contribute this myself).

This also currently always pulls in a dependency for Microsoft.CodeAnalysis.Common, which if you just want the embedded resources for the assembly files (for example to use for this, or other applications like loading them with AsmResolver.DotNet or MetadataReader) can cause a fairly large transitive dependency that isn't wanted. I completely understand if this is out of scope for what you have here, just let me know if that's the case, I'll probably end up creating a fork of what you have as a separate package.

Windows10CE avatar Mar 15 '24 05:03 Windows10CE

Here's some code for those who just want to get spinning with MetadataLoadContext:

using System.Reflection;

using var context = CreateMetadataLoadContextWithAllAssembliesLoaded(
    from reference in Basic.Reference.Assemblies.Net90.ReferenceInfos.All
    select (reference.FileName, reference.ImageBytes));

foreach (var assembly in context.GetAssemblies().OrderBy(a => a.GetName().Name))
{
    Console.WriteLine($"{assembly.GetName().Name}: {assembly.GetExportedTypes().Length} public types");
}

static MetadataLoadContext CreateMetadataLoadContextWithAllAssembliesLoaded(
    IEnumerable<(string FileName, byte[] ImageBytes)> references)
{
    var resolver = new BasicReferenceAssembliesResolver(references);
    var context = new MetadataLoadContext(resolver);

    foreach (var assemblyName in resolver.AvailableAssemblyNames)
        context.LoadFromAssemblyName(assemblyName);

    return context;
}

internal sealed class BasicReferenceAssembliesResolver : MetadataAssemblyResolver
{
    private readonly Dictionary<string, byte[]> imageBytesByAssemblyName = new(StringComparer.OrdinalIgnoreCase);

    public IReadOnlyCollection<string> AvailableAssemblyNames => imageBytesByAssemblyName.Keys;

    public BasicReferenceAssembliesResolver(
        IEnumerable<(string FileName, byte[] ImageBytes)> references)
    {
        foreach (var reference in references)
        {
            if (!reference.FileName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
                throw new ArgumentException("Reference assembly file names are all expected to end with .dll.", nameof(references));

            if (!imageBytesByAssemblyName.TryAdd(reference.FileName[..^".dll".Length], reference.ImageBytes))
                throw new ArgumentException("Reference assemblies are expected to have unique names.", nameof(references));
        }
    }

    public override Assembly? Resolve(MetadataLoadContext context, AssemblyName assemblyName)
    {
        return assemblyName.Name is { } name && imageBytesByAssemblyName.TryGetValue(name, out var imageBytes)
            ? context.LoadFromByteArray(imageBytes)
            : null;
    }
}

jnm2 avatar Feb 24 '25 01:02 jnm2