Skip to content

Connecting to a Running Process

Will Fuqua edited this page Jun 29, 2026 · 1 revision

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.

How to connect

The target's source does not need to change, but the application must opt in by launching with two environment variables.

  1. Print the variables for your shell:

    csharprepl connect init

    The shell is auto-detected. Pass --shell to choose one explicitly: pwsh, powershell, cmd, bash, or fish.

  2. 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.

  3. 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.

What you can do

  • 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 shorthand Get<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.

Modifying a running process

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.

Replace a method

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 half

Wrap a method

To 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

List and undo patches

  • #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.

Method-replacement limits

  • The signature of your method must match the target's (with the instance prepended for instance methods, and the orig delegate prepended for #wrap).
  • ref/out/in parameters 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).

Requirements and limits

  • net10.0 only. 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.

Clone this wiki locally