Un recurso incrustado es un archivo que el compilador escribe dentro del propio ensamblado, de modo que la DLL o el EXE lo lleva consigo en lugar de distribuir un archivo suelto a su lado. Un archivo de texto, una imagen o un PDF entra sin cambios y vuelve a salir a través de System.Reflection, no a través de una ruta de archivo.

Así, el ensamblado y sus recursos forman un solo archivo en lugar de varios, y un recurso que falta se convierte en un error de compilación en lugar de en una incidencia de soporte. Los recursos también tienen otras formas: la parte de localización la tratamos en Localización en ASP.NET Core y la de las cadenas en Cómo leer una cadena de un archivo .resx (de recursos) en C#.

Para descargar el código fuente de este artículo, puedes visitar nuestro repositorio de GitHub.

Empecemos.

¿Qué son los recursos incrustados?

Un recurso incrustado es un archivo que el compilador copia en el ensamblado durante la compilación y registra en su manifiesto. La DLL o el EXE contiene los bytes, así que no hay ningún archivo aparte que desplegar, ninguna ruta relativa que pueda estar mal y ninguna forma de que el archivo desaparezca entre la compilación y la ejecución.

El archivo en sí no cambia. El texto sigue siendo texto y un PDF sigue siendo un PDF. Lo que cambia es cómo accedemos a él: a través de System.Reflection en lugar de System.IO.

Hay dos formas de colocar un archivo ahí, y ambas escriben el mismo elemento en el archivo de proyecto. En Visual Studio, establecemos la Build Action del archivo en Embedded resource. En el .csproj, añadimos nosotros mismos una entrada <EmbeddedResource Include="..." />.

Dos llamadas sobre Assembly lo vuelven a sacar. GetManifestResourceNames() devuelve todos los nombres del manifiesto, y GetManifestResourceStream(name) abre uno de ellos como Stream.

Ese nombre no es la ruta del archivo, y de ese único hecho trata la mayor parte de este artículo.

¿Cómo añadir un recurso incrustado a un proyecto?

Hay dos mecanismos para añadir un archivo al ensamblado, y ambos acaban escribiendo la misma línea en el archivo de proyecto.

En Visual Studio, seleccionamos el archivo en Solution Explorer, abrimos sus Properties y establecemos Build Action en Embedded resource. Visual Studio escribe la entrada por nosotros.

En el archivo de proyecto, la escribimos a mano como <EmbeddedResource Include="Resources\text-file.txt" />. Editarlo directamente nos da dos cosas que la lista desplegable no puede expresar. Include acepta patrones glob de MSBuild, así que Resources\**\*.txt incrusta un subárbol completo y sigue incrustando los archivos que se le añadan después. Y una ruta puede salir de la carpeta del proyecto, algo que la ventana Properties no tiene forma de indicar.

Hay una categoría que no necesita ninguna entrada. El SDK de .NET ya incrusta todos los archivos .resx del proyecto, y por eso un archivo de diseñador de recursos funciona sin que nadie añada nada. Si se establece EnableDefaultEmbeddedResourceItems en false, se desactiva ese comportamiento.

Empecemos preparando un programa de prueba rápido, una aplicación de línea de comandos sencilla. Podemos crear una nueva aplicación de línea de comandos directamente desde Visual Studio o usar el comando dotnet:

dotnet new console -n Embedded_Resources_in_NET

Una vez configurado el proyecto de línea de comandos, podemos pasar enseguida a incorporar recursos incrustados a la aplicación.

Vamos a generar archivos de texto (.txt) y PDF (.pdf) de ejemplo para las pruebas. Podemos tomar archivos del disco duro o extraerlos de la aplicación de ejemplo de Code Maze.

Creación de las carpetas y los archivos de ejemplo

Preparemos las carpetas y los archivos de los recursos. Primero, crearemos dos subcarpetas en el directorio del proyecto: Resources y Files. Aunque los nombres concretos no son cruciales, Resources es un nombre habitual.

Así es como podemos hacerlo:

md Resources
cd Resources
md Pdf
cd ..
md Files

Así, hemos creado una estructura de carpetas en la que las carpetas Resources y Files están al mismo nivel, y la carpeta Pdf está dentro de Resources.

A continuación, copia un archivo de texto del disco en cada una de las carpetas Resources y Files y ponle el nombre text-file.txt. Del mismo modo, dupliquemos un archivo PDF de ejemplo, llamémoslo pdf-file.pdf y coloquémoslo en la subcarpeta Pdf dentro de Resources.

Después de completar estos pasos, la estructura de carpetas quedará así:

|
|-- Resources
    |-- text-file.txt
    |-- Pdf
        |-- pdf-file.pdf
|-- Files
    |-- text-file.txt

El contenido real de los archivos no importa para lo que queremos hacer.

Añadir recursos incrustados con Visual Studio

Ahora que hemos incluido todas las carpetas y los archivos en el proyecto de .NET/C#, deberían verse en Solution Explorer.

Para marcar cada uno de estos archivos para que se incruste, sigue estos pasos:

  1. Haz clic con el botón derecho en cada archivo.
  2. En el menú contextual, elige ‘Properties’.
  3. En la ventana Properties, selecciona ‘Build Action’.
  4. Elige ‘Embedded resource’ en el menú desplegable.

ventana de propiedades de un recurso incrustado

¿Cómo listar los recursos incrustados de un ensamblado?

Ahora que hemos incrustado tres recursos en la aplicación, escribamos algo de código C# para obtener una lista de estos archivos.

Primero, necesitaremos una referencia al ensamblado que contiene los recursos incrustados. Todos los miembros que escribamos a partir de aquí van dentro de una clase SampleResourceReader, en un archivo propio que empieza con using System.Diagnostics; y using System.Reflection;. Como hemos añadido todos los recursos al ensamblado principal, podemos acceder a él fácilmente:

private static Assembly ThisAssembly
    => typeof(SampleResourceReader).Assembly;

Una vez que tenemos la referencia al ensamblado, podemos listar todos sus recursos con el método GetManifestResourceNames():

private static void ListResourcesInAssembly(Assembly? assembly)
{
    if (assembly is null)
        return;

    var resources = assembly.GetManifestResourceNames();
    if (resources.Length == 0)
        return;

    Console.WriteLine($"Resources in {assembly.FullName}");
    foreach (var resource in resources)
    {
        Console.WriteLine(resource);
    }

    Console.WriteLine();
}

Es una función de uso general, así que primero comprobamos si tenemos una referencia válida al ensamblado. Después, llamamos al método GetManifestResourceNames() para obtener la lista de todo lo que tiene incrustado. Si no hay archivos incrustados, salimos de la función. En caso contrario, mostramos los nombres de los recursos en la consola.

The Web API Production Checklist, ebook gratuito en inglés

Ebook gratis

¿Tu Web API está lista para producción?

33 puntos que comprobar antes de desplegarla, con la solución de cada uno. Un PDF gratuito de 76 páginas para .NET 10.

El ebook está en inglés.

Descarga la checklist gratis

PDF gratuito. Un solo correo para enviártelo. Puedes darte de baja cuando quieras.

Los nombres de los archivos incrustados

Al llamar a ListResourcesInAssembly(ThisAssembly) mediante un método público desde Program.cs, obtenemos la lista de archivos:

Resources in Embedded_Resources_in_NET, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Embedded_Resources_in_NET.Files.text-file.txt
Embedded_Resources_in_NET.Resources.text-file.txt
Embedded_Resources_in_NET.Resources.Pdf.pdf-file.pdf

Las partes de cada nombre de recurso están separadas por puntos. Si leemos ese nombre hacia atrás, vemos de dónde viene:

El nombre de recurso del manifiesto Embedded_Resources_in_NET.Resources.Pdf.pdf-file.pdf dividido en el espacio de nombres raíz, dos nombres de carpeta y el nombre del archivo.

‘Embedded_Resources_in_NET‘ es nuestro espacio de nombres raíz, que de forma predeterminada es el nombre del proyecto; Resources y Pdf son las subcarpetas correspondientes, y pdf-file.pdf es el nombre del archivo del recurso incrustado. El punto (‘.’) separa el espacio de nombres raíz, toda la estructura de carpetas y el nombre del propio recurso incrustado.

Esta estructura nos permite tener en el ensamblado dos archivos idénticos llamados text-file.txt sin conflictos. Uno está en la subcarpeta Files, por lo que su nombre es Embedded_Resources_in_NET.Files.text-file.txt, mientras que el otro está en la subcarpeta Resources, así que su nombre es Embedded_Resources_in_NET.Resources.text-file.txt. Como los nombres son distintos, no hay ningún problema.

Recursos incrustados en el archivo .csproj

Visual Studio registra en el archivo .csproj qué archivos se han incrustado. Esta es la parte del XML que nos interesa:

<ItemGroup>
    <EmbeddedResource Include="Files\text-file.txt" />
    <EmbeddedResource Include="Resources\Pdf\pdf-file.pdf" />
    <EmbeddedResource Include="Resources\text-file.txt" />
</ItemGroup>

Visual Studio escribe una ruta literal por archivo, pero el elemento admite bastante más que eso. La primera línea de abajo incrusta un archivo note.txt que ahora añadimos a una nueva carpeta my-folder del proyecto de ejemplo, y las otras tres muestran qué más puede expresar el elemento:

<ItemGroup>
    <EmbeddedResource Include="my-folder\note.txt" />
    <EmbeddedResource Include="Resources\**\*" Exclude="Resources\skipme.txt" />
    <EmbeddedResource Include="..\..\README.md" Link="Docs\README.md" />
    <EmbeddedResource Include="Files\note.txt" LogicalName="note" />
</ItemGroup>

El glob sigue incrustando los archivos que se añadan más adelante a Resources, Exclude vuelve a excluir uno, Link le da a un archivo externo una carpeta virtual dentro del ensamblado y LogicalName sustituye por completo el nombre calculado. Las dos últimas opciones cambian el nombre con el que termina el recurso, y la tabla que aparece más abajo muestra qué produce cada una. Nuestro my-folder\note.txt es el caso más sencillo: se incrusta como Embedded_Resources_in_NET.my_folder.note.txt, porque un nombre de carpeta que no es un identificador válido se reescribe para convertirlo en uno.

Recursos incrustados fuera del proyecto

Si modificamos el archivo .csproj, podemos incrustar archivos que no están en las subcarpetas del proyecto, algo que la ventana Properties no ofrece. Editar el archivo nos permite incluir archivos de otras ubicaciones, así:

<ItemGroup>
    <EmbeddedResource Include="Files\text-file.txt" />
    <EmbeddedResource Include="Resources\Pdf\pdf-file.pdf" />
    <EmbeddedResource Include="Resources\text-file.txt" />
    <EmbeddedResource Include="my-folder\note.txt" />
    <EmbeddedResource Include="..\Embedded_Resources_in_NET.sln" />
    <EmbeddedResource Include="..\..\README.md" />
</ItemGroup>

Con esta configuración, incrustamos un archivo de solución, que está una carpeta más arriba, e incluso un archivo README.md que está dos carpetas más arriba. El repositorio de ejemplo tiene ambos; en nuestras propias carpetas, creamos el que falte antes de compilar, y basta con un archivo vacío. Al ejecutar este código, obtenemos:

Resources in Embedded_Resources_in_NET, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Embedded_Resources_in_NET.Files.text-file.txt
Embedded_Resources_in_NET.Resources.text-file.txt
Embedded_Resources_in_NET.my_folder.note.txt
Embedded_Resources_in_NET.Resources.Pdf.pdf-file.pdf
Embedded_Resources_in_NET.Embedded_Resources_in_NET.sln
Embedded_Resources_in_NET.README.md

Fíjate en que todos estos recursos reciben un nombre como si estuvieran en la misma carpeta que el archivo de proyecto. Los metadatos Link lo solucionan: <EmbeddedResource Include="..\..\README.md" Link="Docs\README.md" /> lo incrusta, en cambio, como Embedded_Resources_in_NET.Docs.README.md.

Sin embargo, esto solo era una prueba para explorar la posibilidad. Aunque es factible, no es recomendable incrustar recursos de fuera del proyecto. No deberíamos incrustar recursos de fuera del proyecto, porque no podemos garantizar que estén disponibles en tiempo de compilación. Basta con un sparse checkout o un checkout parcial para que falle: el compilador se detiene con error CS1566: Error reading resource ... Could not find file.

¿Cómo leer recursos incrustados de otro ensamblado?

Nada de GetManifestResourceNames() está ligado al ensamblado en el que se ejecuta nuestro código. Con una referencia a cualquier ensamblado cargado, la misma llamada lista los recursos de ese ensamblado.

Assembly.Load("Name") obtiene la referencia a partir de un nombre de ensamblado simple, y typeof(SomeTypeInside).Assembly la obtiene sin ninguna cadena cuando ya hacemos referencia al proyecto.

AppDomain.CurrentDomain.GetAssemblies() parece la forma de recorrer toda la aplicación, pero no lo es. Devuelve los ensamblados ya cargados en este proceso, que no son el mismo conjunto que los ensamblados que compila nuestra solución. Un proyecto referenciado cuyos tipos todavía no ha usado el código no aparece en ese array, aunque su DLL esté en la carpeta de salida junto a la nuestra, porque el runtime carga los ensamblados la primera vez que se usan.

Un ensamblado satélite es otra cosa distinta: un .resources.dll sin código que contiene los recursos de una cultura, que se genera a partir de archivos .resx con el sufijo de la cultura y que se coloca en una subcarpeta con el nombre de esa cultura.

Los ensamblados satélite provienen de la localización, así que, si es eso lo que nos ha traído hasta aquí, explicamos por separado cómo localizar una aplicación con archivos .resx específicos de cada cultura, así como cómo volver a leer las cadenas con ResourceManager.

Para este experimento, creemos un nuevo proyecto junto a nuestro proyecto de consola e incrustemos un archivo de texto en ese ensamblado:

dotnet new classlib -o Embedded_Resources_in_NET_Library

A continuación, abre este nuevo proyecto en Visual Studio e incrusta un archivo de texto en Resources\text-file.txt. Como alternativa, podemos editar el archivo .csproj:

<ItemGroup>
    <EmbeddedResource Include="Resources\text-file.txt" />
</ItemGroup>

Esta configuración incrustará text-file.txt en nuestra biblioteca de clases.

Leer la lista de recursos incrustados de un ensamblado referenciado

Podemos aplicar el mismo enfoque a cualquier ensamblado, incluidas las bibliotecas de clases referenciadas.

Ya tenemos el método ListResourcesInAssembly() para esto. Solo tenemos que añadir en el proyecto de consola una referencia de proyecto a la biblioteca e indicar el ensamblado correcto:

private static Assembly ReferencedAssembly =>
    Assembly.Load("Embedded_Resources_in_NET_Library");

Después, podemos llamar al método:

public static void ListResourcesInReferencedAssembly()
    => ListResourcesInAssembly(ReferencedAssembly);

Como la biblioteca de clases contiene un solo recurso incrustado, obtendremos una lista con un único elemento:

Resources in Embedded_Resources_in_NET_Library, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Embedded_Resources_in_NET_Library.Resources.text-file.txt

Esto demuestra que podemos leer recursos incrustados de cualquier ensamblado, lo que aporta flexibilidad en la gestión de recursos.

Leer la lista de recursos incrustados de todos los ensamblados cargados

Para listar todo lo que hay incrustado en todos los ensamblados que forman la solución, primero podemos obtener la lista de todos los ensamblados del dominio de aplicación actual:

private static Assembly[] AllAssembliesOfCurrentAppDomain
    => AppDomain.CurrentDomain.GetAssemblies();

Después, podemos iterar por esta lista y llamar al método ListResourcesInAssembly para cada ensamblado:

public static void ListResourcesInAllAssemblies()
    => AllAssembliesOfCurrentAppDomain.ToList().ForEach(ListResourcesInAssembly);

Esto abarca todos los ensamblados que el proceso ha cargado hasta ahora, que no es lo mismo que todos los ensamblados de la solución. Llamar a Assembly.Load() sobre los que nos interesan, antes del recorrido, es lo que completa la lista.

¿Cómo leer el contenido de un recurso incrustado?

GetManifestResourceStream() es el complemento de GetManifestResourceNames(): le pasamos uno de esos nombres y nos devuelve un Stream sobre los bytes incrustados.

Igual que obtenemos la lista completa de nombres con el método GetManifestResourceNames(), podemos obtener el contenido de un recurso con el método GetManifestResourceStream().

Una vez que tenemos un objeto Stream, podemos realizar diversas operaciones, como leerlo, transformarlo, mostrarlo, guardarlo en disco, enviarlo por la red y mucho más. Los Streams ofrecen una forma flexible y potente de manejar datos en C#.

The Web API Production Checklist, ebook gratuito en inglés

Ebook gratis

¿Tu Web API está lista para producción?

33 puntos que comprobar antes de desplegarla, con la solución de cada uno. Un PDF gratuito de 76 páginas para .NET 10.

El ebook está en inglés.

Descarga la checklist gratis

PDF gratuito. Un solo correo para enviártelo. Puedes darte de baja cuando quieras.

Buscar un recurso incrustado en los ensamblados

Para simplificar la búsqueda de un recurso incrustado concreto en todos los ensamblados que ha cargado el proceso, podemos implementar un método auxiliar:

private static Stream? FindResource(Func<string[]?, string?> finder)
{
    foreach (var assembly in AllAssembliesOfCurrentAppDomain)
    {
        var resourceNames = assembly.GetManifestResourceNames();
        var resourceName = finder(resourceNames);

        if (resourceName is not null)
        {
            Console.WriteLine($"Resource {resourceName} found in {assembly.FullName}");
            return assembly.GetManifestResourceStream(resourceName);
        }
    }

    return null;
}

El método recorre cada ensamblado, obtiene la lista de nombres de recursos de ese ensamblado con GetManifestResourceNames() y después pasa esa lista a un método finder externo.

Si el método finder encuentra el recurso buscado, imprimimos un mensaje que lo indica y devolvemos el Stream correspondiente con GetManifestResourceStream(). Si el recurso no se encuentra en ningún ensamblado, devolvemos null.

Buscar un recurso por su nombre completo

Para encontrar un recurso incrustado por su nombre completo, podemos crear un método buscador que devuelva el nombre si coincide exactamente:

FindResource(names => names?.FirstOrDefault(rn => rn == resourceName));

Este método usa la función LINQ FirstOrDefault() para encontrar el primer nombre de recurso que coincide con el resourceName proporcionado.

Buscar un recurso por una parte de su nombre

Del mismo modo, para encontrar un recurso indicando solo una parte de su nombre, como ‘pdf-file.pdf’, podemos modificar el método buscador para que compruebe si el nombre la contiene:

FindResource(names => names?.FirstOrDefault(rn => rn.Contains(partialResourceName)));

Aquí usamos el método Contains para comprobar si algún nombre de recurso contiene el partialResourceName indicado. Si hay coincidencia, se devuelve ese nombre de recurso.

Mostrar el contenido de un recurso incrustado

Una vez que tenemos un objeto Stream que representa el recurso incrustado, mostrar su contenido en pantalla es sencillo:

private static void DisplayResource(string resourceName, Stream resourceStream)
{
    using var reader = new StreamReader(resourceStream);
    var resourceContent = reader.ReadToEnd();
    Console.WriteLine($"Resource {resourceName} content:");
    Console.WriteLine(resourceContent);
}

El método lee el contenido del stream del recurso con un StreamReader. Después, imprime en la consola el nombre del recurso y su contenido.

Mostrar el archivo PDF

Para mostrar el archivo PDF incrustado, primero tenemos que guardar su contenido en disco y después abrir el archivo guardado con una aplicación de PDF. Podemos hacerlo con dos métodos:

private static string SaveResourceToAFile(string partialResourceName, Stream resourceStream)
{
    var tempFileName = Path.Combine(Path.GetTempPath(), partialResourceName);
    using var fileStream = new FileStream(tempFileName, FileMode.Create, FileAccess.Write);
    resourceStream.CopyTo(fileStream);
    fileStream.Close();

    return tempFileName;
}

private static void ShowFile(string fileName)
    => Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true });

El método SaveResourceToAFile() recibe el nombre parcial del archivo y el Stream que representa el recurso incrustado. Crea un archivo temporal en la carpeta temporal del usuario actual y escribe en él el contenido del stream del recurso. Después, devuelve el nombre del archivo recién creado.

El método ShowFile() inicia un nuevo proceso con la aplicación predeterminada del sistema para abrir archivos PDF y le pasa el nombre del archivo como argumento. Así, el archivo PDF se abre con el visor de PDF predeterminado instalado en el sistema.

¿Por qué GetManifestResourceStream() devuelve null?

Porque la cadena que pasamos no es un nombre del manifiesto, y el método lo indica devolviendo null en lugar de lanzar una excepción.

Cuatro diferencias explican casi todos los fallos. El prefijo es el espacio de nombres raíz del proyecto, no el nombre de su ensamblado, y los dos pueden no coincidir aunque nadie haya cambiado ninguno de ellos. Los nombres de carpeta se convierten en identificadores, así que un guion o un espacio se convierte en un guion bajo, y a un dígito inicial se le antepone uno. El separador es un punto, nunca una barra ni una barra invertida. Y la coincidencia distingue mayúsculas de minúsculas, así que una sola mayúscula equivocada hace que no se devuelva nada.

Sin un código de cultura, el nombre del archivo no se toca, guiones incluidos, y por eso la mitad de un nombre incorrecto parece correcta.

Lo más fiable es dejar de componer la cadena a mano. Llama a GetManifestResourceNames() una vez, imprime lo que devuelve y copia la entrada que corresponda.

Ese null es un comportamiento documentado, no accidental. La referencia de Assembly.GetManifestResourceStream de Microsoft dice lo siguiente sobre el valor devuelto:

null si no se especificaron recursos durante la compilación o si el recurso no es visible para el llamador.

Esto es lo que produce el compilador a partir de cada tipo de entrada. Dos propiedades de MSBuild cambian el resultado: EnableDefaultEmbeddedResourceItems, que desactiva el glob automático .resx, y EmbeddedResourceUseDependentUponConvention, que es lo que hace que un .resx situado junto a un archivo de código fuente tome su nombre del tipo declarado en ese archivo.

El archivo en el proyectoSu nombre de recurso en el manifiestoLa regla
Resources\text-file.txtMyApp.Resources.text-file.txtEspacio de nombres raíz, luego cada carpeta y luego el nombre del archivo, unidos con puntos
my-folder\a.txtMyApp.my_folder.a.txtLos nombres de carpeta se convierten en identificadores: un guion o un espacio se convierte en _
two words\a.txtMyApp.two_words.a.txtLa misma regla, espacios incluidos
2digits\a.txtMyApp._2digits.a.txtA una carpeta que empieza por un dígito se le antepone _
Files\my-file-name.txtMyApp.Files.my-file-name.txtSin un código de cultura, el nombre del archivo no se toca; sus guiones se conservan
..\..\README.mdMyApp.README.mdLas carpetas de fuera del proyecto se descartan
..\..\README.md con Link="Docs\README.md"MyApp.Docs.README.mdLink elige la carpeta virtual de un archivo externo
Files\note.txt con LogicalName="note"noteLogicalName sustituye todo el nombre calculado
Auto.resx, sin ninguna entrada en el archivo de proyectoMyApp.Auto.resourcesEl SDK incrusta todos los .resx de forma predeterminada y los compila, así que la extensión cambia
Forms\Form1.resx junto a Forms\Form1.cs, que declara Some.Ns.Form1Some.Ns.Form1.resourcesEl nombre sale del tipo ubicado junto a él, no de la ruta de carpetas

Conclusión

Incrustar recursos en ensamblados .NET es sencillo. Podemos seleccionarlos en Visual Studio o añadirlos manualmente como etiquetas XML en el archivo .csproj.

De forma predeterminada, todos los recursos incrustados, salvo los archivos .resx y cualquier archivo con sufijo de cultura, como note.fr.txt, que va a un ensamblado satélite, reciben un nombre que concatena el espacio de nombres raíz del proyecto con la ruta de carpetas dentro del proyecto y el nombre del archivo, separados por puntos, y cada nombre de carpeta se convierte en un identificador válido. Por ejemplo, Embedded_Resources_in_NET.Resources.Pdf.pdf-file.pdf.

Al invocar el método GetManifestResourceNames() del objeto Assembly, obtenemos la lista de todos los nombres de recursos de un ensamblado. Del mismo modo, el método GetManifestResourceStream() obtiene el stream del recurso incrustado.

Un mismo mecanismo lleva archivos de texto, imágenes y archivos binarios como los PDF dentro del propio ensamblado. Cada ensamblado sigue siendo un solo archivo, y todos los recursos que necesita la aplicación viajan con el código que los lee.

Probado con .NET 10.0.10.