In .NET 6, how do I reference two two versions of the same nuget package

I would like to reference the same namespace from dll1 and alias dll2 to have like a prefix.

So in this example, I would have to different projects:

namespace1
 DoOperationFirstWay(){LoadedLib.Type1..}
namespace2
 DoOperationSecondWay(){Alias.LoadedLib.Type1..}

I do not have access to the source to change the namespaces.

Both DoOperationFirstWay() and DoOperationSecondWay() need to be able to be called in the same runspace

There are two parts to this, the build time stuff, and the runtime stuff.

Build time stuff

Ok, so one of NuGet’s goals is to look at a restore graph, and then resolve any package that appears multiple times into a single version that is selected. If you really, truly want to use two different versions, you’re going to have to work against NuGet.

NuGet has a feature/MSBuild item called <PackageDownload which is intended for a specific purpose, but you can abuse it in this case. The version attribute/metadata can be a semicolon list of versions. For example <PackageDownload Include="MyPackage" Version="[1.0.0];[2.0.0]" />. However, note that PackageDownload does not download dependencies (again it was designed for something specific, you’re using it in a way that’s outside that design requirement). If these packages have dependencies, you’re going to have to figure out how you want to get them.

Next, the design of PackageDownload was explicitly to download and unzip the nupkg only, don’t do the normal asset selection (after all, multiple versions can be downloaded). So, now you’re responsible for your own asset selection, and both telling the build process how to pass the relevant dlls to the compiler, and also how to copy the dlls to the bin/publish directory. To figure all this out, you should use MSBuild’s binary logs, or binlogs. Run dotnet build -bl on the command line, and it’ll create a file named msbuild.binlog in the current directory. You can open it with https://msbuildlog.com. There’s a windows app you can download, or just view it right in your web browser. Then, become somewhat of an MSBuild & .NET SDK expert to figure out how other dlls get passes to csc’s command line, and figure out what you need to do to get these two package dll’s passed in with different aliases. Similarly, since the dll names are probably the same, you need to get then copied to the bin directory in different directories.

As for how to find the packages that were downloaded with PackageDownload, NuGet defines a property named NuGetPackageRoot, which is where all the packages get downloaded & extracted to. Therefore, you might be able to use something like <Reference Include="$(NuGetPackageRoot)MyPackageName\1.2.3\lib\net6.0\some.dll" Alias="MyPackageV1" />, but I’m just guessing that metadata named Alias is how to tell the compiler to use that alias. You need to figure that out with the binlog.

Runtime stuff

Once you’ve got source code that can use alias1.namespace and alias2.namespace,and you’ve got bin\v1\some.dll and bin\v2\some.dll, you need to be able to load and execute the correct dll at the correct time.

With the .NET Framework, if the assemblies were strong name signed, it is possible to just use <codebase> entries in the app.config file, as the .NET Framework assembly loader always expects an exact assembly version match, unless a binding redirect is present. However, very, very few people wanted this, it caused much more problems than benefit, so with .NET Core the assembly loader was changed to only ensure that the dll on disk being loaded is equal or higher version than the assembly reference being requested.

Therefore, I don’t believe you’ll have a choice, except you must learn how to use AssemblyLoadContext to load at least one of the two dlls. You might be able to load the lower version into the default context, and that way if you accidentally attempt to use the higher version dll without the custom context it will fail. But if I were you,I’d load both dlls into separate custom contexts, to minimize risk of errors. Therefore, your DoOperationFirstWay() and DoOperationSecondWay() methods won’t be quite as simple as what your question asks for, you need the relevant code to load LoadedLib.Type1 from the correct context, but it’s feasible.

Alternative 1

As you can see, it’s non-trivial to implement. If the packages have dependencies that you need, it might be easier to have two more projects in your solution, one for each of the package versions you want to reference.

In your main project where you want to use the two packages, you’ll want to have ProjectReferences that do not copy the project’s output to the bindirectory. Then, you’ll want a target that runs after build (<Target Name="CopyMultiVersionPackagesToSubdirectories" AfterTargets="Build">), which will get each of the two project’s output directories (if you want to do things “right”, otherwise just hardcode the relative path), and then copies their bin\* files to $(OutputDir)\SubDirectory1\. Then your runtime code can use AssemblyLoadContext to load the project dll, the package dll, and any other dependencies from SubDirectory1 and SubDirectory2.

Alternative 2

If your app is a web app, then use a microservice inspired architecture to have a different webapi app for each package version, and then your “main” webapp will call one of the two, depending on which functionality it needs.

If your app is not a web app, then consider having one or two external processes, each of which references one of the package versions. That way proc1.exe and proc2.exe can be a boring, normal .NET project that <PackageReference‘s one of the package versions, no need to worry about AssemblyLoadContext. Instead, you have to worry about Remote Procedure Calls (RPC), or Inter Process Communication (IPC).

There are many ways this can be done. Either make proc1 and proc2 console apps, and the host process talks to them via stdin and stdout. Or, you can use anonymous pipes. Some people say that memory mapped files can be used for IPC, but I don’t have experience. As far as I’m aware, that’s only to share data, you’ll still need a signal to synchronize between processes, like a named mutex or something. There’s also a library named StreamJsonRpc which Visual Studio uses to talk to its out of process components. I have no idea how to use it in a custom app, but I know the library exists. gRPC is another well known protocol for talking between processes, typically used over HTTP, but I don’t think that http is a good choice for a host app to talk to its own child processes.

Conclusion

If it sounds like I’m trying to persuade you to not go down this path, that’s because that’s my opinion. The only reason I can think of for why you’d want to do this is because the package you’re referencing has a breaking change. Your question is asking to treat the symptom, not the root cause, and all the solution ideas I provided above are similarly treating the symptom. Maybe you’re better off trying to fix the root cause.

If your team owns the package, you’re better off adding new classes & methods so a higher version of the package contains both the old and new behaviour. It might be technical debt to have “duplicate” code, but so is all the workarounds to enable what your question asked for.

If the package is owned by another team in the same company, then this become office politics, not a technical question, but if your team really needs to have both the old and new behaviour, then the company is better off if that other team takes on the technical debt of multiple methods/classes to give your team an easy package and APIs to use.

If the package is owned by a different company, then it depends if you already have a relationship with that company, or how responsive they are to requests like “please don’t break APIs, and bring the previously removed API back”, but in this case I’d expect you would either need to reevaluate your own business requirements in needing both package versions in one app, or you’ll need to implement one of the ideas above.

Leave a Comment