Disposable Types Must Unsubscribe Events

Rule ND1315 Design

Detects disposable types subscribing to events in constructors without unsubscribing in Dispose() (or a method it calls). Event publisher references can keep instances alive: memory leaks, unexpected callbacks.

How to fix

1) Match each constructor subscription with an unsubscription in Dispose() (or a method always called by it). 2) Centralize subscriptions/unsubscriptions in dedicated methods for explicit symmetry. 3) With inheritance, follow the full dispose pattern so derived types can release additional subscriptions. 4) Prefer weak-event patterns or explicit lifetime ownership when the publisher outlives the subscriber.

CQLinq source

This rule is written in CQLinq, C# LINQ over the NDepend code model. It ships with every new NDepend project: you can read it, adapt it, or use it as a starting point for your own custom rules.

// <Name>Disposable Types Must Unsubscribe Events</Name>
// <Id>ND1315:DisposableTypesMustUnsubscribeEvents</Id>
warnif count > 0
      
// Prepare a convenient lookup table first for performance reasons
let lookup = Events
.SelectMany(e => e.MethodsSubscribingToMe
        .Where(m => m.IsConstructor)
        .Select(m => new { m.ParentType, e}))
.ToLookup(pair => pair.ParentType, pair => pair.e)

from pair in lookup
where pair.Key.Implement("System.IDisposable".AllowNoMatch()) 
let t = pair.Key        

// Events this Disposable type subscribes to
let eventsSubscribedInCtor = pair.ToArray()
      
// Check if Dispose() deregisters these events
let disposeMethod = t.Methods.FirstOrDefault(m => 
   m.SimpleName == "Dispose" && m.NbParameters == 0)
where disposeMethod  != null        

let notDeregisteredInDispose = eventsSubscribedInCtor.Where(e => 
    // No Dispose() or methods called by Dispose() unregister event
    !e.MethodsUnsubscribingToMe.Contains(disposeMethod) &&
    !e.MethodsUnsubscribingToMe.Any(x => disposeMethod.MethodsCalled.Contains(x))
).ToArray()

where notDeregisteredInDispose.Any()
select new {
    t,
    eventsSubscribedInCtor, 
    notDeregisteredInDispose,
    disposeMethod,
    Debt = (2*notDeregisteredInDispose.Count()).ToMinutes().ToDebt(),
    Severity = Severity.High
}

// <Explanation>
// The {0}'s Dispose method() should unregister the {2}.
// </Explanation>
// <Desc>
// Detects disposable types subscribing to events in constructors without
// unsubscribing in Dispose() (or a method it calls). Event publisher
// references can keep instances alive: memory leaks, unexpected callbacks.
// </Desc>
// <HowToFix>
// 1) Match each constructor subscription with an unsubscription in Dispose()
//    (or a method always called by it).
// 2) Centralize subscriptions/unsubscriptions in dedicated methods for
//    explicit symmetry.
// 3) With inheritance, follow the full dispose pattern so derived types can
//    release additional subscriptions.
// 4) Prefer weak-event patterns or explicit lifetime ownership when the
//    publisher outlives the subscriber.
// </HowToFix>

Design: all 16 items