Skip to content

AminZar69/EventHandlerLeak

Repository files navigation

EventHandlerLeakDemo

A small .NET console demo that shows how .NET events can silently leak memory in MVVM-style code, and two ways to fix it.

The leak

When you subscribe to an event with publisher.SomeEvent += subscriber.Handler, the publisher holds a strong reference to the subscriber.

So if the publisher outlives the subscriber, which is exactly what happens when the publisher is a singleton service, a message bus, a parent ViewModel, or any other long-lived object, the subscriber cannot be garbage collected, even after every other reference to it has been dropped.

In this demo:

  • Service is the long-lived publisher with a DataChanged event.
  • LeakyViewModel subscribes to DataChanged in its constructor and never unsubscribes. This is the bug.
  • The demo creates the VM inside a helper method and returns only a WeakReference to it, so by the time control returns to the caller, no local variable references the VM anymore. After a forced GC, the VM is still alive, because Service is still holding it through the event subscription.

The demo confirms this by wrapping the VM in a WeakReference. WeakReference doesn't count as a GC root, so we can check IsAlive after a forced collection to see whether anything else is keeping the VM rooted. If it's True, something is leaking it.

The leaky scenario also raises an event after the GC, and you'll see the (supposedly gone) VM still respond.

Solution 1 — IDisposable

The most direct fix: implement IDisposable on the VM and -= the handler in Dispose.

public class DisposableViewModel : IDisposable
{
    private readonly Service _service;
    private bool _disposed;

    public DisposableViewModel(Service service)
    {
        _service = service;
        _service.DataChanged += OnDataChanged;
    }

    public void Dispose()
    {
        if (_disposed) return;
        _service.DataChanged -= OnDataChanged;
        _disposed = true;
        GC.SuppressFinalize(this);
    }
}

The demo exercises this with RunDisposable(callDispose: true). With Dispose called, the VM is collected after GC and the event raise produces no output.

Solution 2 — Generic weak-event wrapper

A reusable helper that subscribes to the event on the VM's behalf, but holds the VM via a WeakReference. The publisher ends up holding the wrapper strongly, but the wrapper doesn't keep the VM alive.

_serviceSubscription = new WeakEventSubscription<Service, string>(
    source:  service,
    attach:  (s, h) => s.DataChanged += h,
    detach:  (s, h) => s.DataChanged -= h,
    target:  this,
    invoke:  static (target, sender, args) =>
        ((WrapperViewModel)target).OnDataChanged(sender, args));

The demo exercises this with RunWrapper. The VM is collected after GC even though Dispose is never called on the wrapper VM in the test.

What the program does

Program.cs runs three scenarios. For each one, Helper:

  1. Creates a Service.
  2. Creates the VM inside a helper method so its local reference falls out of scope cleanly when the method returns. (This matters — in Debug builds the JIT may keep Main-scope locals rooted for the entire method, which would falsely report a leak.)
  3. Returns a WeakReference to the VM. The WeakReference is a probe — it observes whether the VM survives a GC without itself being a GC root.
  4. Forces a full GC.
  5. Prints whether the VM is still alive.
  6. Raises an event to demonstrate whether a zombie subscriber is still reacting.

Building and running with VS Code

Prerequisites

  • .NET 8 SDK (or whichever version your .csproj targets) — run dotnet --version in a terminal to confirm.
  • VS Code.
  • The C# Dev Kit extension (which pulls in the base C# extension automatically).

Steps

  1. Clone and open the repo:

    git clone https://github.com/AminZar69/EventHandlerLeakDemo.git
    cd EventHandlerLeakDemo
    code .
  2. Restore packages. The C# Dev Kit will usually do this automatically when it detects the .csproj. If it doesn't, open the integrated terminal (Ctrl+`) and run:

    dotnet restore
  3. Build from the terminal:

    dotnet build

    Or press Ctrl+Shift+B and pick the build task.

  4. Run from the terminal:

    dotnet run

About

A demo C Sharp code for a potential event handlers memory leak!

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages