-
Notifications
You must be signed in to change notification settings - Fork 123
Connecting to a Running Process
C# REPL can connect to a separate, already-running .NET application and evaluate C# inside it, reading and writing the application's live state (for example statics and services resolved from dependency injection).
Warning
Connecting to a connector-enabled process is equivalent to running arbitrary code inside it, with its privileges. This is a development and diagnostics tool. Never enable the connector on a production process.
It works by injecting a real Roslyn scripting engine into the target, so you can run unconstrained C# there. It is not a debugger: breakpoints, stepping, and non-cooperative attach are not supported.
The target's source does not need to change, but the application must opt in by launching with two environment variables.
-
Print the variables for your shell:
csharprepl connect initThe shell is auto-detected. Pass
--shellto choose one explicitly:pwsh,powershell,cmd,bash, orfish. -
Set those variables in the shell you will launch the app from, then start the app. Do not set them as permanent, machine-wide variables. Only set them in the shell where you launch the target.
-
Connect by process id:
csharprepl connect list # lists available processes to connect to, with their process ID. csharprepl connect 1234 # connects to a process with process ID 1234.
When you connect, you'll see something like this:
Connecting to the connector in process 3427...
Connected to AspNetWebApi (pid 3427)
Runtime: .NET 10.0.7
Connector: v1.0.0.0 (protocol v2)
DI provider captured: yes, services and Get<T>() are available.
Development/diagnostics tool: code you evaluate runs inside the target process with its full privileges. Never connect a production process.
Type C# to evaluate it in the target. Type exit (or press Ctrl+D) to detach; the target keeps running and you can
reconnect later.- Statics: reference them by fully-qualified name, for example
MyApp.Program.SomeStatic(read and write). - DI services (ASP.NET Core or Generic Host apps):
services.GetRequiredService<T>(), or the shorthandGet<T>(). The connector captures the application's root service provider.
Type exit (or press Ctrl+D) to detach. The application keeps running, and you can reconnect later.
While connected to a process, you can replace or wrap live methods in the target. This detours the method's machine code to a delegate you define in the REPL, so the change takes effect immediately for every later call. Patching is done with the MonoMod library.
Define a method that matches the target's signature, then point #replace at the fully-qualified target method. Instance methods take the instance as their first parameter:
> decimal half(MyApp.OrderService svc, int qty, decimal unit) => qty * unit * 0.5m;
> #replace MyApp.OrderService.CalculatePrice with halfTo call the original from inside your replacement, use #wrap. The replacement's first parameter is an orig delegate (a Func<...>/Action<...> matching the original's signature) that invokes the unmodified method:
> decimal logged(Func<MyApp.OrderService, int, decimal, decimal> orig, MyApp.OrderService svc, int qty, decimal unit)
{
var price = orig(svc, qty, unit);
Console.WriteLine($"CalculatePrice({qty}, {unit}) = {price}");
return price;
}
> #wrap MyApp.OrderService.CalculatePrice with logged-
#patches: list the method replacements currently applied to the target, with their ids. -
#revert <id>: undo one patch, restoring the original method. -
#revert all: undo every patch.
Patches take effect immediately and persist in the target until reverted or the process exits.
- The signature of your method must match the target's (with the instance prepended for instance methods, and the
origdelegate prepended for#wrap). -
ref/out/inparameters work with#replace, but not with#wrap. - Not supported: generic methods, pointer parameters, and methods the JIT has already inlined at a call site (those call sites keep the old behavior).
-
net10.0only. The connector and the target must both be on .NET 10. - Single-file apps are limited:
- A framework-dependent single-file app bundles its assemblies without metadata, so strongly-typed access to the app's own types is unavailable. Use reflection to reach them.
- A self-contained single-file app is unsupported. The runtime itself is bundled, so nothing can be compiled, and the connector refuses to start.
For how this works under the hood, see the Injected Hook documentation.