Types with disposable instance fields must be disposable

Rule ND1301 Design

Warns when a class has an instance field whose type implements System.IDisposable or System.IAsyncDisposable but does not implement IDisposable itself: such a class indirectly owns an unmanaged resource. If it owns no unmanaged resource directly, it should not have a finalizer.

False positives can occur when the referenced disposable objects outlive the referencing object.

How to fix

Implement IDisposable and call the fields' Dispose() from IDisposable.Dispose(). Then ensure every method calling a constructor of the type disposes the objects created.

Estimated Debt: 5 minutes per type plus 2 minutes per disposable field plus 4 minutes per method calling a constructor.

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>Types with disposable instance fields must be disposable</Name>
// <Id>ND1301:TypesWithDisposableInstanceFieldsMustBeDisposable</Id>
warnif count > 0

// Several IDisposable types can be found if several .NET profiles are referenced.
let iDisposables = ThirdParty.Types.WithFullNameIn("System.IDisposable", "System.IAsyncDisposable")
where iDisposables.Any() // in case the code base doesn't use at all System.IDisposable or IAsyncDisposable

from t in JustMyCode.Types.Except(
      JustMyCode.Types.ThatImplementAny(iDisposables)
      // Don't match ASP.NET types like Page, MasterPage or Control
      .Union(JustMyCode.Types.Where(t => t.BaseClasses.Any(bc => bc.ParentNamespace.Name.StartsWith("System.Web.UI")))))
where !t.IsGeneratedByCompiler 

let instanceFieldsDisposable = 
    t.InstanceFields.Where(f => f.FieldType != null &&
                                f.FieldType.InterfacesImplemented.Intersect(iDisposables).Any() &&

                                // Impose that disposable types be instantiated by 't' to avoid false positives
                                f.FieldType.Constructors.Any() && 
                                t.Constructors.ThatCreateA(f.FieldType).Any())

where instanceFieldsDisposable.Any()

// Don't warn for types that implement the dispose pattern with a Dispose(bool) method.
where t.Methods.FirstOrDefault(m => m.Name == "Dispose(Boolean)") == null

let methodsThatCallsCtor = t.Constructors.SelectMany(ctor => ctor.MethodsCallingMe).Distinct()

select new { 
   t, 
   instanceFieldsDisposable,
   methodsThatCallsCtor,
   Debt = (5 + 2*instanceFieldsDisposable.Count() 
             + 4*methodsThatCallsCtor.Count()).ToMinutes().ToDebt(),
   Severity = Severity.Medium
}

//<Expl>
// The *{0}* should implement *IDisposable* to dispose the disposable 
// field{1?:s} it instantiates at construction time:
// the *{1}*
//</Expl>

//<Desc>
// Warns when a class has an instance field whose type implements
// *System.IDisposable* or *System.IAsyncDisposable* but does not implement
// *IDisposable* itself: such a class indirectly owns an unmanaged resource.
// If it owns no unmanaged resource directly, it should not have a finalizer.
//
// False positives can occur when the referenced disposable objects outlive
// the referencing object.
//</Desc>

//<HowToFix>
// Implement *IDisposable* and call the fields' *Dispose()* from
// *IDisposable.Dispose()*. Then ensure every method calling a constructor of
// the type disposes the objects created.
//
// Estimated Debt: 5 minutes per type plus 2 minutes per disposable field
// plus 4 minutes per method calling a constructor.
//</HowToFix>

Design: all 16 items