NuGet / Home

Repo for NuGet Client issues
Other
1.5k stars 252 forks source link

Loading NuGet Package assembly #10914

Closed RestoreMonarchy closed 3 years ago

RestoreMonarchy commented 3 years ago

Hello,

I'm making a project that will dynamically load assemblies from NuGet package in runtime. I'm using NuGet.Client packages to download & install dependencies then to read the NuGet package.

I'm stuck in my LoadLibAsync method. I use it to load the assembly from the passed NuGetPackage as a PackageArchiveReader reader parameter. It works well for most NuGet packages like Dapper or EPPlus, but it doesn't work for System.Data.SqlClient 4.8.2 or System.Drawing.

private async Task<IEnumerable<Assembly>> LoadLibAsync(IAssemblyContext context, PackageArchiveReader reader)
{
    IEnumerable<FrameworkSpecificGroup> libs = await reader.GetLibItemsAsync(CancellationToken.None);

    List<Assembly> assemblies = new List<Assembly>();
    NuGetFramework framework = frameworkReducer.GetNearest(targetFramework, libs.Select(l => l.TargetFramework));
    foreach (FrameworkSpecificGroup lib in libs)
    {
        if (lib.TargetFramework != framework)
        {
            continue;
        }

        foreach (string item in lib.Items)
        {
            if (!item.EndsWith(".dll"))
                continue;

            ZipArchiveEntry entry = reader.GetEntry(item);
            using Stream libStream = entry.Open();                    
            using MemoryStream ms = new MemoryStream();
            await libStream.CopyToAsync(ms);
            ms.Position = 0;
            Assembly assembly = context.LoadAssembly(ms);
            assemblies.Add(assembly);
            logger.LogInformation($"{assembly.GetName().Name} {assembly.GetName().Version} has been loaded!");
        }
    }
    return assemblies;
}

When I try to load System.Data.SqlClient 4.8.2 using the LoadLibAsync it loads the assembly from the lib folder which contains only I think a reference binary for it. It does get loaded but every method will throw the exception:

 Message: 
    Test method Shearlegs.Framework.Test.UnitTestSamplePluginExcel.TestSamplePluginExcel threw exception: 
    System.PlatformNotSupportedException: System.Data.SqlClient is not supported on this platform.

I know I should load the assembly from runtimes if it exists in the NuGet package like System.Data.SqlClient 4.8.2, because it's a complete assembly. image I couldn't find any method in NuGet.Client libraries that does it tho. I also don't know how I can make a handling for it myself, because System.Data.SqlClient contains unix and win directories inside runtimes, but I read that other packages may contain specific windows versions.

What should I use to load assemblies like System.Data.SqlClient or System.Drawing? Thanks for help!

zivkan commented 3 years ago

We have a class, ManagedCodeConventions that can be used to select assets from a package. I guess our unit tests have the easiest examples of how to use this class.

The issue is for the runtimes/ assets is that you need a runtime graph. NuGet doesn't contain this itself, the .NET SDK passes us the file path via msbuild property. Therefore, you'll need to find a solution to how you want to get the runtimes.json file, but it appears that the .NET team is still publishing it as a NuGet package, despite shipping it in the .NET SDK: https://www.nuget.org/packages/Microsoft.NETCore.Platforms/

How you determine what your current runtime is, whether it's win-???, macos-??? or linux-???, and what to replace that ??? with is another problem you'll need to solve. Again, MSBuild passes that information to NuGet during restore, so it's not a problem that NuGet itself has.

RestoreMonarchy commented 3 years ago

@zivkan I got it working thanks to your hints!

I added runtime.json file, from the latest version of Microsoft.NETCore.Platforms package, to my project, so it appears in the root of the output directory.

This is my LoadLibAsync method now:

private RuntimeGraph GetRuntimeGraph(string expandedPath)
{
    string runtimeGraphFile = Path.Combine(expandedPath, RuntimeGraph.RuntimeGraphFileName);
    if (File.Exists(runtimeGraphFile))
    {
        using (FileStream stream = File.OpenRead(runtimeGraphFile))
        {
            return JsonRuntimeFormat.ReadRuntimeGraph(stream);
        }
    }

    return null;
}

private async Task<IEnumerable<Assembly>> LoadLibAsync(IAssemblyContext context, PackageArchiveReader reader, string path)
{
    RuntimeGraph graph = GetRuntimeGraph(Directory.GetCurrentDirectory());
    ManagedCodeConventions conv = new ManagedCodeConventions(graph);

    ContentItemCollection collection = new ContentItemCollection();
    collection.Load(await reader.GetFilesAsync(CancellationToken.None));

    List<Assembly> assemblies = new List<Assembly>();

    NuGetFramework framework = frameworkReducer.GetNearest(targetFramework, await reader.GetSupportedFrameworksAsync(CancellationToken.None));

    ContentItemGroup natives = collection.FindBestItemGroup(
        conv.Criteria.ForRuntime(RuntimeInformation.RuntimeIdentifier), 
        conv.Patterns.NativeLibraries);

    ContentItemGroup group = collection.FindBestItemGroup(
        conv.Criteria.ForFrameworkAndRuntime(framework, RuntimeInformation.RuntimeIdentifier),
        conv.Patterns.RuntimeAssemblies);

    if (group != null)
    {
        foreach (ContentItem item in group.Items)
        {
            if (!item.Path.EndsWith(".dll"))
                continue;

            ZipArchiveEntry entry = reader.GetEntry(item.Path);
            using Stream libStream = entry.Open();
            using MemoryStream ms = new MemoryStream();
            await libStream.CopyToAsync(ms);
            ms.Position = 0;
            Assembly assembly = context.LoadAssembly(ms);
            assemblies.Add(assembly);
            logger.LogInformation($"{assembly.GetName().Name} {assembly.GetName().Version} has been loaded!");
        }
    }

    if (natives != null)
    {
        foreach (ContentItem item in natives.Items)
        {
            if (!item.Path.EndsWith(".dll"))
                continue;
            string itemPath = Path.Combine(path, item.Path);
            NativeLibrary.Load(itemPath);                    
        }
    }

    return assemblies;
}

System.Data.SqlClient and all other dependencies are loaded without issues now and my code runs without errors

..., but I'm not sure if I load the native libraries like sni.dll for System.Data.SqlClient correctly. Right now I'm loading all dependencies into the AssemblyContext which after execution of a method from a plugin I'm unloading. Native library I'm just simply loading with NativeLibrary.Load and it works. Should I load native library into the AssemblyContext, so I could unload it later along with other libraries? Can I even do this? Or should I keep it like this and it won't have any leaks?

Thanks for help again

zivkan commented 3 years ago

Should I load native library into the AssemblyContext, so I could unload it later along with other libraries? Can I even do this? Or should I keep it like this and it won't have any leaks?

I don't know, I'm not an expert on AssemblyLoadContext or AppDomain. Closing since this is no longer about NuGet. It seems you know enough to test for yourself, but you could also try stack overflow, or asking at dotnet/runtime.