Disposable Types Must Unsubscribe Events
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>
Open in the interactive Rules Explorer · All default rules
Design: all 16 items
- Avoid custom delegates ND1300
- Types with disposable instance fields must be disposable ND1301
- Disposable types with unmanaged resources should declare finalizer ND1302
- Methods that create disposable object(s) and that don't call Dispose() ND1303
- Disposable Types Must Unsubscribe Events ND1315
- Classes that are candidate to be turned into structures ND1304
- Avoid namespaces with few types ND1305
- Nested types should not be visible ND1306
- Declare types in namespaces ND1307
- Empty static constructor can be discarded ND1308
- Instances size shouldn't be too big ND1309
- Attribute classes should be sealed ND1310
- Don't use obsolete types, methods or fields ND1311
- Do implement methods that throw NotImplementedException ND1312
- Override equals and operator equals on value types ND1313
- Boxing/unboxing should be avoided ND1314