A afirmação acabou por ser uma mentira, uma mentira e uma provocação

Mas não importava mais, porque o desafio foi aceito.

aviso Legal
. . . .
Treinamento
Criamos uma cadeia de herança. Para simplificar, usaremos construtores sem parâmetros. No construtor, exibiremos informações sobre o tipo e o identificador do objeto no qual ele é chamado.
public class A
{
public A()
{
Console.WriteLine($"Type '{nameof(A)}' .ctor called on object #{GetHashCode()}");
}
}
public class B : A
{
public B()
{
Console.WriteLine($"Type '{nameof(B)}' .ctor called on object #{GetHashCode()}");
}
}
public class C : B
{
public C()
{
Console.WriteLine($"Type '{nameof(C)}' .ctor called on object #{GetHashCode()}");
}
}
Execute o programa:
class Program
{
static void Main()
{
new C();
}
}
E obtemos o resultado:
Type 'A' .ctor called on object #58225482
Type 'B' .ctor called on object #58225482
Type 'C' .ctor called on object #58225482
Digressão lírica
, . , . , . :
:
public A() : this() { } // CS0516 Constructor 'A.A()' cannot call itself
:
public A() : this(new object()) { }
public A(object _) : this(0) { }
public A(int _) : this() { } // CS0768 Constructor 'A.A(int)' cannot call itself through another constructor
Removendo código duplicado
Adicione uma classe auxiliar:
internal static class Extensions
{
public static void Trace(this object obj) =>
Console.WriteLine($"Type '{obj.GetType().Name}' .ctor called on object #{obj.GetHashCode()}");
}
E nós substituímos em todos os construtores
Console.WriteLine($"Type '{nameof(...)}' .ctor called on object #{GetHashCode()}");
em
this.Trace();
No entanto, o programa agora mostra: Em nosso caso, o seguinte truque pode ser usado. Quem sabe sobre os tipos de tempo de compilação? Compilador. Ele também seleciona sobrecargas de método com base nesses tipos. E para tipos e métodos genéricos, ele também gera entidades construídas. Portanto, retornamos a inferência de tipo correta reescrevendo o método Trace da seguinte maneira:
Type 'C' .ctor called on object #58225482
Type 'C' .ctor called on object #58225482
Type 'C' .ctor called on object #58225482
public static void Trace<T>(this T obj) =>
Console.WriteLine($"Type '{typeof(T).Name}' .ctor called on object #{obj.GetHashCode()}");
Acessando um construtor de tipo base
É aqui que a reflexão vem em socorro. Adicione um método às extensões :
public static Action GetBaseConstructor<T>(this T obj) =>
() => typeof(T)
.BaseType
.GetConstructor(Type.EmptyTypes)
.Invoke(obj, Array.Empty<object>());
Adicione a propriedade aos tipos B e C :
private Action @base => this.GetBaseConstructor();
Chamar um construtor de tipo base em qualquer lugar
Altere o conteúdo dos construtores B e C para:
this.Trace();
@base();
Agora, a saída é semelhante a esta:
Type 'A' .ctor called on object #58225482
Type 'B' .ctor called on object #58225482
Type 'A' .ctor called on object #58225482
Type 'C' .ctor called on object #58225482
Type 'A' .ctor called on object #58225482
Type 'B' .ctor called on object #58225482
Type 'A' .ctor called on object #58225482
Mudando a ordem de chamada dos construtores do tipo base
Dentro do tipo A, crie um tipo auxiliar:
protected class CtorHelper
{
private CtorHelper() { }
}
Como apenas a semântica é importante aqui, faz sentido tornar o construtor de tipo privado. A instanciação não faz sentido. O tipo destina-se exclusivamente a distinguir entre as sobrecargas dos construtores do tipo A e aquelas derivadas dele. Pelo mesmo motivo, o tipo deve ser colocado dentro de A e protegido.
Adicione os construtores apropriados para A , B e C :
protected A(CtorHelper _) { }
protected B(CtorHelper _) { }
protected C(CtorHelper _) { }
Para os tipos B e C, adicione uma chamada para todos os construtores:
: base(null)
Como resultado, as classes devem ser semelhantes a este
internal static class Extensions
{
public static Action GetBaseConstructor<T>(this T obj) =>
() => typeof(T)
.BaseType
.GetConstructor(Type.EmptyTypes)
.Invoke(obj, Array.Empty<object>());
public static void Trace<T>(this T obj) =>
Console.WriteLine($"Type '{typeof(T).Name}' .ctor called on object #{obj.GetHashCode()}");
}
public class A
{
protected A(CtorHelper _) { }
public A()
{
this.Trace();
}
protected class CtorHelper
{
private CtorHelper() { }
}
}
public class B : A
{
private Action @base => this.GetBaseConstructor();
protected B(CtorHelper _) : base(null) { }
public B() : base(null)
{
this.Trace();
@base();
}
}
public class C : B
{
private Action @base => this.GetBaseConstructor();
protected C(CtorHelper _) : base(null) { }
public C() : base(null)
{
this.Trace();
@base();
}
}
E a saída se torna:
Type 'C' .ctor called on object #58225482
Type 'B' .ctor called on object #58225482
Type 'A' .ctor called on object #58225482
O simplório ingênuo pensa que o compilador trapaceou

Compreendendo o resultado
Adicionando um método às extensões :
public static void TraceSurrogate<T>(this T obj) =>
Console.WriteLine($"Type '{typeof(T).Name}' surrogate .ctor called on object #{obj.GetHashCode()}");
e chamando-o em todos os construtores que aceitam CtorHelper , obtemos a seguinte saída: A ordem dos construtores de acordo com o princípio base / derivado, é claro, não mudou. Mesmo assim, a ordem dos construtores disponíveis para o código do cliente que carregam a carga semântica foi alterada graças ao redirecionamento por meio de chamadas para inacessível para os construtores auxiliares do cliente que não fazem nada.
Type 'A' surrogate .ctor called on object #58225482
Type 'B' surrogate .ctor called on object #58225482
Type 'C' .ctor called on object #58225482
Type 'A' surrogate .ctor called on object #58225482
Type 'B' .ctor called on object #58225482
Type 'A' .ctor called on object #58225482