Возможно, я не совсем прав, но компилятор делает что-то вроде этого:
Когда дело доходит до
b.DoSomething("test")
он пытается найти метод с той же сигнатурой в этом или базовом классе, и когда он находит метод в базовом классе, сопоставляет вызов с ним, просто игнорируя метод расширения.
Но если вы, например, удалите базовый класс A из объявления B в той же строке, компилятор проверит, что в этом или базовом классе нет метода с такой сигнатурой, и заменит его вызовом статического метода BExtensions.DoSomething.
Вы можете проверить это с помощью .NET Reflector.
Когда B выводится из A:
.locals init (
[0] class Test.A a,
[1] class Test.B b)
...
ldloc.1 // loading local variable b
ldstr "test"
callvirt instance string Test.A::DoSomething(string)
call void [mscorlib]System.Console::WriteLine(string)
Когда B наследуется от System.Object:
.locals init (
[0] class Test.A a,
[1] class Test.B b)
...
ldloc.1 // loading local variable b
ldstr "test"
call string Test.BExtensions::DoSomething(class Test.B, string)
call void [mscorlib]System.Console::WriteLine(string)