Solucionar error because it is missing a public instance constructor

solucionar error because missing public instance constructor
Inicio » Informática » .Net » Solucionar error because it is missing a public instance constructor

Tabla de contenido

Hace poco analizamos la forma correcta en .NET de convertir el appsettings.json a un objeto, pues bien, en este artículo vamos a ver la forma de solucionar un error derivado de utilizar Bind o Get aunque, como veremos a continuación, el error podemos encontrarlo en otros escenarios.

Código base de ejemplo

El código de ejemplo para solucionar el error because it is missing a public instance constructor puedes encontrarlo en el respositorio de Github de este artículo aunque a continuación enumeraremos los elementos principales.

En primer lugar tenemos una aplicación de consola escrita en C# donde hemos instalado los siguientes paquetes para poder convertir el appsettings.json a un objeto C#:

<ItemGroup>
  <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.7" />
  <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.7" />
</ItemGroup>

Después tenemos el siguiente código dentro de la clase Program.cs que lo único que hace es leer el json y convertirlo a un objeto a través de las alternativas Get y Bind:

using Microsoft.Extensions.Configuration;
using MissingAPublicInstanceConstructor;

internal class Program
{
    private static void Main(string[] args)
    {
        var build = new ConfigurationBuilder()
        .AddJsonFile("appsettings.json")
        .Build();

        // Alternative 1
        var config1 = build.Get<Config>();

        // Alternative 2
        var config2 = new Config();
        build.Bind(config2);

        // Output
        Console.WriteLine($"config1?.PropString: '{config1?.PropString}'");
        Console.WriteLine($"config1?.PropClass1.PropString: '{config1?.PropClass1.PropString}'");

        Console.WriteLine($"config2.PropString: '{config2.PropString}'");
        Console.WriteLine($"config2.PropClass1.PropString: '{config2.PropClass1.PropString}'");
    }
}

A continuación tenemos las clases que representan nuestra configuración, son clases completamente normales con propiedades simples y complejas:

namespace MissingAPublicInstanceConstructor
{
    public class Config
    {
        public Config() { }
        public string PropString { get; set; }
        public int PropInt { get; set; }
        public Class1 PropClass1 { get; set; }
        public Class2 PropClass2 { get; set; }
    }
    public class Class1
    {
        public Class1() { }
        public string PropString { get; set; }
        public int PropInt { get; set; }
    }
    public class Class2
    {
        public Class2() { }
        public string PropString { get; set; }
        public int PropInt { get; set; }
    }
}

Y finalmente tenemos el fichero appsettings.json que convertiremos a un objeto de la clase Config.cs:

{
  "PropString": "Text example",
  "PropInt": 123,
  "PropClass1": {
    "PropString": "Text Class1",
    "PropInt": 456
  },
  "PropClass2": {
    "PropString": "Text Class2",
    "PropInt": 789
  }
}

Descripción completa del error

El error completo al ejecutar la aplicación es el siguiente:

Unhandled exception. System.InvalidOperationException: Cannot create instance of type 'Project.Class' because it is missing a public instance constructor.
   at Microsoft.Extensions.Configuration.ConfigurationBinder.CreateInstance(Type , IConfiguration, BinderOptions, ParameterInfo[]&)
   at Microsoft.Extensions.Configuration.ConfigurationBinder.BindInstance(Type , BindingPoint, IConfiguration, BinderOptions, Boolean)
   at Microsoft.Extensions.Configuration.ConfigurationBinder.BindProperty(PropertyInfo, Object, IConfiguration, BinderOptions)
   at Microsoft.Extensions.Configuration.ConfigurationBinder.BindProperties(Object, IConfiguration, BinderOptions, ParameterInfo[])
   at Microsoft.Extensions.Configuration.ConfigurationBinder.BindInstance(Type , BindingPoint, IConfiguration, BinderOptions, Boolean)
   at Microsoft.Extensions.Configuration.ConfigurationBinder.Get(IConfiguration, Type , Action`1 )
   at Microsoft.Extensions.Configuration.ConfigurationBinder.Get[T](IConfiguration , Action`1 )
   at Microsoft.Extensions.Configuration.ConfigurationBinder.Get[T](IConfiguration )
   at Program.Main(String[] args) in C:\Users\Daniel\source\repos\CodeArco\Project\Solution\Program.cs:line 8

Y la parte que realmente nos interesa son las dos siguientes líneas:

  • Cannot create instance of type ‘Project.Class’ because it is missing a public instance constructor, esta línea indica que no se puede crear una instancia de una clase del proyecto porque no tiene un constructor público y sin parámetros, como veremos más adelante esto no es del todo cierto.
  • at Microsoft.Extensions.Configuration.ConfigurationBinder.Get, esta línea indica que el error se produce en el método Get que utilizamos para convertir el appsettings.json a un objeto.

Cuándo y por qué ocurre el error

La parte más interesante de este error es que la descripción es errónea o confusa, es decir, el error no ocurre cuando falta un constructor público, realmente el error ocurre cuando se dan los dos siguientes puntos de manera simultánea:

  • Se está utilizando recorte de código al publicar, si no habilitas esta opción el error no va a aparecer.
  • Se está utilizando reflexión de forma directa o indirecta en alguna parte del código, en este caso el método que utiliza reflexión de manera interna es Get para convertir el appsettings.json al objeto representado por la clase Config.cs.

Dado lo anterior podemos concluir que el error ocurre porque:

  • A través de reflexión se está creando una instancia de un objeto (en nuestro caso Config.cs) utilizando el constructor sin parámetros y público new().
  • Dado que dicho constructor no se está referenciando en ninguna parte del proyecto el recorte de código lo elimina y, como consecuencia, cuando es invocado por la reflexión, lanza error de ejecución porque no lo encuentra.

También es interesante conocer que por defecto C# crea constructores sin parámetros y públicos siempre que tu clase no tenga otros constructores, de esto se deriva que no es necesario crearlos siempre, esto lo veremos en mayor profundidad más adelante.

Además es necesario tener en cuenta que este error puede darse en otros escenarios donde se utilice reflexión, no solo en el binding hecho a través del método Get.

Soluciones no recomendadas a evitar

Antes de proceder a analizar la solución al error «because it is missing a public instance constructor» vamos a ver cuales son las soluciones no recomendadas, éstas no resolverán tu error o lo harán de forma muy cuestionable:

  • Crear constructores vacíos sin hacer nada más en todas las clases afectadas, esto no solucionará el error. En nuestro caso la modificación consistiría en crear un new() en todas las clases de la configuración.
    El problema no ocurre porque falte un new(), el problema ocurre porque el recorte de código está eliminando el new() ya que no está siendo referenciado en ninguna parte del código.
    Tal y como hemos comentado antes C# crea por defecto constructores sin parámetros y públicos en aquellas clases que no tengan ningún otro constructor y, por lo tanto, crear constructores vacíos y públicos solo es necesario si tienes otros constructures en la clase.
  • Referenciar los constructores vacios en el código funciona, resolverá el error, pero es una solución muy sucia que no sigue prácticas de código limpio.
    En nuestro caso si utilizamos el siguiente código en el Program.cs el error desaparecerá pero, como puedes observar, queda bastante feo:
  •   var c = new Config();
      var c1 = new Class1();
      var c2 = new Class2();
  • Desactivar el recorte de código también resolverá el error, pero es una solución bastante drástica que debes evitar; a continuación veremos que la solución pasa por desactivar el recorte de código parcialmente, de esta manera conseguiremos solucionar el error manteniendo las ventajas del recorte de código.
  • Utilizar Bind en vez de Get no solucionará el problema, solo lo camuflará.
    Tal y como comentamos en el artículo para convertir appsettings.json a un objeto C# el método Bind es menos restrictivo o tolerante a errores que Get; si haces la prueba con recorte de código verás que no aparecerá el error «because it is missing a public instance constructor» pero te aparecerá uno nuevo cuando intente acceder a la configuración ya que ésta estará vacía o inicializada a los valores por defecto:
      Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
        at Program.Main(String[]) in C:\Users\Daniel\source\repos\CodeArco\MissingAPublicInstanceConstructor\MissingAPublicInstanceConstructor\Program.cs:line 24

Solución recomendada con TrimmerRoot.xml

El primer paso para solucionar el error es crear en todas las clases constructores sin parámetros y públicos, pero… ¿por qué debemos crearlos si antes hemos comentado que no es necesario? Bueno, porque solo es necesario crearlo en aquellas clases que tengan otros constructores ya que C# solo los crea automáticamente si no hay ningún constructor. Es decir, para evitar que en alguna clase se nos olvide vamos a crearlos en todas.

El segundo paso consiste en crear el fichero TrimmerRoot.xml en la raiz de tu proyecto con todas las clases a las que no quieres que se aplique recorte de código:

<linker>
    <assembly fullname="MissingAPublicInstanceConstructor">
        <type fullname="MissingAPublicInstanceConstructor.Config" preserve="all" />
        <type fullname="MissingAPublicInstanceConstructor.Class1" preserve="all" />
        <type fullname="MissingAPublicInstanceConstructor.Class2" preserve="all" />
    </assembly>
</linker>

Observa que MissingAPublicInstanceConstructor es el nombre del proyecto y que tenemos una línea por cada clase utilizada en la configuración.

El tercer paso consiste en modificar el .csproj del proyecto y hacer referencia al anterior fichero a través del nodo TrimmerRootDescriptor:

<ItemGroup>
    <TrimmerRootDescriptor Include="TrimmerRoot.xml" />
</ItemGroup>

Finalmente limpia, recompila y publica con recorte de código la aplicación y comprueba que el error «because it is missing a public instance constructor» se ha solucionado: