Collection properties should be read only

Rule ND2300 .NET Framework Usage / System.Collections

A writable collection property lets users replace the whole collection; a read-only one still allows setting individual members. If replacement is needed, prefer clear and re-populate methods (see Clear() and AddRange() on System.Collections.Generic.List<T>).

Binary and XML serialization support read-only collection properties (XmlSerializer has specific requirements for ICollection / IEnumerable types). Matches property setters assigning a collection.

How to fix

Make the property read-only, adding clear and re-populate methods if the design requires it.

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>Collection properties should be read only</Name>
// <Id>ND2300:CollectionPropertiesShouldBeReadOnly</Id>

warnif count > 0

// First find collectionTypes 
let collectionInterfaces = ThirdParty.Types.WithFullNameIn(
      "System.Collections.ICollection", 
      "System.Collections.Generic.ICollection<T>")

let collectionTypes = Types.ThatImplementAny(collectionInterfaces)
                           .Union(collectionInterfaces)
                           .Union(Types.WithFullName("System.Array"))
                           .ToHashSetEx()
where collectionTypes.Count() > 0

// Find all Read/Write property that is typed with a collection type
from property in JustMyCode.Properties.Where(
   p => p.IsReadWrite &&
        p.PropertyType != null &&
        collectionTypes.Contains(p.PropertyType))

let propSetter = property.SetMethod

where  propSetter != null &&
      !propSetter.IsPrivate && // Ignore private setters since this is private implementation detail
      !propSetter.ParentType.IsPrivate && // Ignore setters of private types

       // Ignore properties of serializable types
      !propSetter.ParentType.TypesUsed.Any(t1 => t1.IsAttributeClass && 
        (t1.ParentNamespace.Name == "Newtonsoft.Json" || t1.ParentNamespace.Name == "System.Text.Json")
       ) &&
      !propSetter.ParentType.HasAttribute("System.Runtime.Serialization.DataContractAttribute".AllowNoMatch()) &&
      !propSetter.ParentType.HasAttribute("System.Xml.Serialization.XmlRootAttribute".AllowNoMatch()) &&
      !propSetter.ParentType.HasAttribute("System.SerializableAttribute".AllowNoMatch())

select new { 
   property, 
   CollectionType = property.PropertyType,
   Debt = 10.ToMinutes().ToDebt(),
   Severity = Severity.Medium
}

//<Expl>
// The *{0}* should be read-only because it is typed with the collection *{1}*.
// Doing so favors *encapsulation*, *predictable behavior* and *consistency*.
//</Expl>

//<Desc>
// A writable collection property lets users replace the whole collection; a
// read-only one still allows setting individual members. If replacement is
// needed, prefer clear and re-populate methods (see *Clear()* and
// *AddRange()* on *System.Collections.Generic.List<T>*).
//
// Binary and XML serialization support read-only collection properties
// (*XmlSerializer* has specific requirements for *ICollection* /
// *IEnumerable* types). Matches property setters assigning a collection.
//</Desc>

//<HowToFix>
// Make the property read-only, adding clear and re-populate methods if the
// design requires it.
//</HowToFix>

.NET Framework Usage / System.Collections: all 4 items