I’ve just started a new project that is part of a bigger initiative to port a largish application from the .NET Framework to .NET Core (or .NET 5 if you will).
One major thing there is the fact that it is heavily using AppDomains. It does that in a way that somewhat resembles what IIS is doing, if that helps you understand. That is, each AppDomain hosts its own logical application (or service). Certain libraries used inside these assume that they have their own “space” (like statics/globals, settings, etc.). So not just to “simply” ensure one can load different versions of the same assembly (e.g. JSON.NET).
So just using the new AssemblyLoadContext
-mechansim is not enough for use. No, we need to do what Microsoft suggests (as one) possible
way to deal with that: using separate processes.
Mind you, that the project currently is alpha-state, at best, but nevertheless basically works - if you’re interested.
With no further ado, here is the content of the readme. For code and more goto cklutz/ProcessIsolation on Github.
The ProcessIsolation project tries to provide a means of running (managed) code isolated from one another using separate processes. This is one of the recommended scenarios that Microsoft suggests, now that AppDomains are no more.
The basic architecture is conceptually rather simple. You have your own application (called the host), that wants to run code inside isolated containers, so it doesn’t interfere with stability of your host process. This project uses operating system processes as those containers.
The host references the ProcessIsolation.dll
assembly, which contains all the code required to
manage the isolation processes. The isolation processes themselves are instances of the pihost.exe
executable, that uses the .NET AssemblyLoadContext
-class to load the code that should actually
run in isolation.
By using AssemblyLoadContext
, the isolated code may even reference different versions of dependencies
than the pihost.exe
and thus provides maximum flexibility in this regard.
Additionally, the host can specify a number of properties for the isolation process. Including resource limits (like CPU or memory usage). Also, the ProcessIsolation framework ensures that no orphaned processes remain should the host crash. It also allows the host to transparently restart an isolation process, should that be required.
The ProcessIsolation framework comes as three nuget packages:
ProcessIsolation
: contains the host side part of the frameworkProcessIsolation.Host
: contains the pihost.exe
executableProcessIsolation.Shared
: contains shared assemblies used by both of the aboveAn example for a host process is available as example/SampleHost.
Typical code that could be loaded into an isolation process is available as example/SampleLib.
Note that the this code has no reference to ProcessIsolation
.
Don’t get bogged down by the apparent complexity of the SampleHost. It also serves as a testbed to exercise most functions that the ProcessIsolation framework provides and thus has a multitude of command line options in this regard.
The basic code to run code (from another assembly) in isolation is this:
As you can see the current interface is somewhat limited. Basically it mimics the AppDomain.ExecuteAssembly()
-method.
To make it work, the assembly being specified with the InvokeMethodAsync()
-method must have a method with the following
signature:
That is, it must
int
(returning 0
means OK, anything else an error)string[]
as argumentsThus, the interface between the host process and the isolation process (and thus the code running within) is rather arcane. This is so, because in this first attempt of the project we didn’t want to settle on a n IPC mechanism yet.
As actual value of plain System.Diagnostics.Process
this project - as said above - provides a number of options,
settings and APIs to manage and handle the isolated processes. Explore the IsolationOptions
and ProcessIsolator
classes for more information.
The later is of course required in some form to allow complex communication between the host and the code in the isolation process. Something like remoting (which is no longer available).
Currently, gRPC is not an option as it requires a full HTTP/2 capable stack and thus would make Kestrel a dependency. Also gRPC at the moment doesn’t allow for local-host-only IPC or something based on named pipes. All in all that seems a bit heavyweight at the moment.
Other options exist of course, as this project is using jacqueskang/IpcServiceFramework
internally at the moment.
Finally, there is nothing preventing a host and isolated user code to establish their on IPC (even gRPC) on top.
To build the project use the build.ps
file: