We will bring this to life by showing how to create from scratch a tutorial showing a few scenarios and outputing some statistics as it goes along
I am using VSCode on a macbook, but this should work nicely anywhere as long as you have dotnet installed.
In this guide, we will create a Memory Leaker—a service that intentionally consumes memory until it crashes. We will verify that ProcessSandbox.Runner detects the leak and recycles the worker without crashing your main app.
Open your terminal in VS Code and run these commands to set up a clean solution structure:
# 1. Create the folder structure
mkdir ProcessSandboxDemo
cd ProcessSandboxDemo
# 2. Create the solution
dotnet new sln
# 3. Create the "Host" app (Your main application)
dotnet new console -n SandboxHost
# 4. Create the "Library" (The code we want to isolate)
dotnet new classlib -n LegacyLibrary
# 5. Create the contracts (The interface and types we need to share). Note netstandard2.0 is unneccessary here, but you might want it in the future for maximum compatability.
dotnet new classlib -n Contracts -f netstandard2.0
# 6. Link projects to solution
dotnet sln add SandboxHost/SandboxHost.csproj
dotnet sln add LegacyLibrary/LegacyLibrary.csproj
dotnet sln add Contracts/Contracts.csproj
# 7. Add reference to contracts
dotnet add SandboxHost/SandboxHost.csproj reference Contracts/Contracts.csproj
dotnet add LegacyLibrary/LegacyLibrary.csproj reference Contracts/Contracts.csproj
# 8. Install ProcessSandbox.Runner
dotnet add SandboxHost/SandboxHost.csproj package ProcessSandbox.Runner --prerelease
# 9. Add Logging package
dotnet add SandboxHost/SandboxHost.csproj package Microsoft.Extensions.Logging.Console
# 10. Install ProcessSandbox.Abstractions into Contracts (So we can use the right version of messagepack)
dotnet add Contracts/Contracts.csproj package ProcessSandbox.Abstractions --prerelease
Open Contracts and replace with this:
using System;
using MessagePack;
namespace LegacyLibrary.Contracts;
/// <summary>
/// Contains key information about the worker process (for demo/debugging)
/// </summary>
[MessagePackObject]
public class ProcessInfo
{
/// <summary>
/// The ID of the worker process
/// </summary>
[Key(0)]
public int ProcessId { get; set; }
/// <summary>
/// The memory usage of the worker process in megabytes
/// </summary>
[Key(1)]
public double MemoryMB { get; set; }
};
/// <summary>
/// A service that simulates instability by leaking memory
/// </summary>
public interface IUnstableService : IDisposable
{
/// <summary>
/// Returns the current process Info for debugging in the demo
/// </summary>
/// <returns></returns>
ProcessInfo GetProcessInfo();
/// <summary>
/// Simulates a memory leak by allocating unmanaged memory
/// </summary>
/// <param name="megabytes"></param>
void LeakMemory(int megabytes);
}We need code that behaves badly. Open LegacyLibrary/Class1.cs and replace it with this:
namespace LegacyLibrary;
using System.Diagnostics;
using LegacyLibrary.Contracts;
/// <summary>
/// A service that simulates instability by leaking memory
/// </summary>
public class UnstableService : IUnstableService
{
// A static list that never gets cleared = Classic Memory Leak
private static readonly List<byte[]> _memoryHog = new();
/// <summary>
/// Returns the current process ID and memory usage
/// </summary>
public ProcessInfo GetProcessInfo()
{
var process = Process.GetCurrentProcess();
return new ProcessInfo()
{
ProcessId = process.Id,
MemoryMB = process.WorkingSet64 / (1024.0 * 1024.0)
};
}
/// <summary>
/// Simulates a memory leak by allocating unmanaged memory
/// </summary>
/// <param name="megabytes"></param>
public void LeakMemory(int megabytes)
{
// Allocate unmanaged memory to simulate a heavy leak
var data = new byte[megabytes * 1024 * 1024];
// Fill it so it actually commits to RAM
new Random().NextBytes(data);
// Add to static list so GC cannot collect it
_memoryHog.Add(data);
}
/// <summary>
/// Disposes the service.
/// </summary>
public void Dispose()
{
// Nothing to dispose
}
}Now, let's configure the host to watch this service. We will set a strict memory limit of 500MB.
Open SandboxHost/Program.cs:
using Microsoft.Extensions.Logging;
using ProcessSandbox.Pool;
using LegacyLibrary.Contracts;
using ProcessSandbox.Proxy;
// 1. Setup minimal logging to see Sandbox internals
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddConsole();
builder.SetMinimumLevel(LogLevel.Warning); // Only show warnings/errors from the pool
});
// 2. Configure the Pool
var config = new ProcessPoolConfiguration
{
MinPoolSize = 1,
MaxPoolSize = 1, // Keep it simple: 1 worker
// The Critical Part: Point to our separate DLL
// Note the replace of SandboxHost with LegacyLibrary in this instance just to get the right path
// In your real application, you'll probably want to package legecy code separately and use configuration appropriately
ImplementationAssemblyPath = Path.Combine(AppContext.BaseDirectory, "LegacyLibrary.dll").Replace("SandboxHost", "LegacyLibrary"),
ImplementationTypeName = "LegacyLibrary.UnstableService", // The actual type
// SAFETY LIMITS
MaxMemoryMB = 500, // Recycle if it uses > 500MB
ProcessRecycleThreshold = 0 // Disable call-count recycling (rely on memory)
};
Console.WriteLine("--- 🛡️ Starting ProcessSandbox Monitor ---");
Console.WriteLine($"Policy: Max Memory = {config.MaxMemoryMB}MB");
// 3. Create the Proxy Factory
var factory = await ProcessProxyFactory<IUnstableService>.CreateAsync(config, loggerFactory);
// 4. Run the Simulation Loop
var iteration = 1;
while (true)
{
// A. Invoke the bad code
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write($"\n[Call #{iteration}] Sending request... ");
// This shows the closure pattern:
var info = await factory.UseProxyAsync<ProcessInfo>(async proxy =>
{
// Leak Memory and Get Process Info will be sent to the same proxy in the prcess worker
proxy.LeakMemory(10); // Leak 10MB per call
return proxy.GetProcessInfo();
});
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"Success from the proxy (Worker PID: {info.ProcessId}, Used: {info.MemoryMB}MB)");
// Or if you prefer the lease pattern - it is your responsibility to Dispose the lease:
using var lease = await factory.AcquireLeaseAsync();
lease.LeakMemory(10);
info = lease.GetProcessInfo();
Console.WriteLine($"Success from the lease (Worker PID: {info.ProcessId}, Used: {info.MemoryMB}MB)");
iteration++;
}- Open your terminal to the
ProcessSandboxDemofolder. - Run the host (make sure we have build the LegacyLibrary first) with dotnet build
dotnet build dotnet run --project SandboxHost
- Initial Calls: You will see the
(Worker PID: 1234)stay the same. Memory usage will climb: 10MB... 20MB... 30MB... 40MB. - The Trigger: Once it hits ~500MB, the Sandbox policies kick in.
- The Recycle: You won't see a crash. You won't see an exception.
- The Result: Suddenly, on the next call, the Worker PID will change (e.g., from 1234 to 5678). The memory usage will drop back to near zero.
You have just successfully swapped out a corrupted process for a fresh one without stopping your application!