Only a member of the BCL team can tell us for sure, but it was probably just an oversight that List<T>.ForEach lets you modify the list.
First, David B's answer doesn't make sense to me. It's List<T>, not C#, that checks if you modify the list within a foreach loop and throws an InvalidOperationException if you do. It has nothing to do with the language you're using.
Second, there's this warning in the documentation:
Modifying the underlying collection in the body of the Action<T> delegate is not supported and causes undefined behavior.
I find it unlikely that the BCL team wanted such a simple method like ForEach to have undefined behavior.
Third, as of .NET 4.5, List<T>.ForEach will throw an InvalidOperationException if the delegate modifies the list. If a program depends on the old behavior, it will stop working when it's recompiled to target .NET 4.5. The fact that Microsoft is willing to accept this breaking change strongly suggests that the original behavior was unintended and should not be relied upon.
For reference, here's how List<T>.ForEach is implemented in .NET 4.0, straight from the reference source:
public void ForEach(Action<T> action) {
if( action == null) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
}
Contract.EndContractBlock();
for(int i = 0 ; i < _size; i++) {
action(_items[i]);
}
}
And here's how it's been changed in .NET 4.5:
public void ForEach(Action<T> action) {
if( action == null) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
}
Contract.EndContractBlock();
int version = _version;
for(int i = 0 ; i < _size; i++) {
if (version != _version && BinaryCompatibility.TargetsAtLeast_Desktop_V4_5) {
break;
}
action(_items[i]);
}
if (version != _version && BinaryCompatibility.TargetsAtLeast_Desktop_V4_5)
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
}