C#:obtenga una lista de tipos definidos en un ensamblaje sin cargarlo

C#:obtenga una lista de tipos definidos en un ensamblaje sin cargarlo

Hay dos formas de obtener información de tipo de un ensamblado sin cargarlo:

  • Solo reflexión/carga de metadatos.
  • Busca un patrón en los archivos de origen.

En este artículo, mostraré ambos enfoques para generar una lista de tipos en un ensamblaje.

Solo reflexión/carga de metadatos

Puede haber muchas razones por las que no desea cargar el ensamblaje. Tal vez te encuentres con errores al intentar cargarlo usando Assembly.LoadFrom(). Por ejemplo, podría tener problemas para resolver dependencias, o tal vez esté recibiendo la excepción de formato de imagen incorrecto.

El objetivo de realizar una carga de solo reflexión es que puede leer metadatos (como los tipos definidos) sin encontrarse con todos los problemas que surgen al intentar cargar completamente el ensamblaje.

Esto se hace de manera diferente dependiendo de si está ejecutando este código desde .NET Framework o .NET Core.

.NET Framework:utilice Assembly.ReflectionOnlyLoadFrom()

Para realizar una carga solo de reflexión desde un proyecto de .NET Framework, use Assembly.ReflectionOnlyLoadFrom(), así:

var assemblyPath = @"D:\Projects\TestLib\bin\Debug\TestLib.dll";

var assembly = Assembly.ReflectionOnlyLoadFrom(assemblyPath);

foreach (var type in assembly.GetTypes())
{
	Console.WriteLine(type.Name);
}
Code language: C# (cs)

Esto genera el siguiente nombre de clase:

DatabaseHelperCode language: plaintext (plaintext)

.NET Núcleo

No puede usar Assembly.ReflectionOnlyLoadFrom() en un proyecto de .NET Core. Si lo intenta, obtendrá la siguiente excepción de tiempo de ejecución:

En su lugar, puede usar System.Reflection.Metadata.MetadataReader o System.Reflection.MetadataLoadContext. Mostraré cómo ambos enfoques a continuación.

Usar MetadataReader

Puede usar la clase System.Reflection.Metadata.MetadataReader así:

using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;

var assemblyPath = @"D:\Projects\aspdotnet-background-dblogger\bin\Debug\net5.0\BackgroundDatabaseLogger.dll";

using (var sr = new StreamReader(assemblyPath))
{
	using (var portableExecutableReader = new PEReader(sr.BaseStream))
	{
		var metadataReader = portableExecutableReader.GetMetadataReader();

		foreach (var typeDefHandle in metadataReader.TypeDefinitions)
		{
			var typeDef = metadataReader.GetTypeDefinition(typeDefHandle);

			if (string.IsNullOrEmpty(metadataReader.GetString(typeDef.Namespace)))
				continue; //if it's namespace is blank, it's not a user-defined type

			if (typeDef.Attributes.HasFlag(TypeAttributes.Abstract) || !typeDef.Attributes.HasFlag(TypeAttributes.Public))
				continue; //Not a public concrete type

			Console.WriteLine(metadataReader.GetString(typeDef.Name));
		}
	}
}
Code language: C# (cs)

Esto genera todos los tipos concretos públicos definidos por el usuario en el ensamblaje:

Program
Program
Startup
DatabaseLoggerService
TestEnum
TestStruct
LogMessage
LogRepository
RecipesControllerCode language: plaintext (plaintext)

Nota:Esto incluye tipos que no son de clase (como struct y enum). No pude encontrar una manera de usar MetadataReader para determinar si era estrictamente una clase o no.

Esto no es tan bueno y limpio como usar Assembly.ReflectionOnlyLoadFrom(), pero hace el trabajo.

Usar System.Reflection.MetadataLoadContext

MetadataLoadContext realiza una lectura de solo reflexión del ensamblaje y le proporciona un objeto de ensamblaje para que pueda usar la API de reflexión.

Primero, debe instalar el paquete nuget System.Reflection.MetadataLoadContext. Puede instalar esto con el siguiente comando en la Consola del administrador de paquetes (Ver> Otras ventanas> Consola del administrador de paquetes) :

Install-Package System.Reflection.MetadataLoadContext
Code language: PowerShell (powershell)

Luego puede usar MetadataLoadContext para mostrar la lista de nombres de tipos públicos en el ensamblado, así:

using System.Reflection;
using System.Runtime.InteropServices;

var assemblyPath = @"D:\Projects\aspdotnet-background-dblogger\bin\Debug\net5.0\BackgroundDatabaseLogger.dll";

var resolver = new PathAssemblyResolver(new List<string>(Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll")) 
{
	assemblyPath
});

using (var metadataContext = new MetadataLoadContext(resolver))
{
	Assembly assembly = metadataContext.LoadFromAssemblyPath(assemblyPath);

	foreach (var type in assembly.GetTypes())
	{
		if (type.IsPublic)
		{
			Console.WriteLine(type.Name);
		}
	}
}
Code language: C# (cs)

Esto genera todos los nombres de tipos públicos:

Program
Startup
DatabaseLoggerService
ILoggerService
TestEnum
TestStruct
ILogRepository
LogMessage
LogRepository
RecipesControllerCode language: plaintext (plaintext)

Buscar en los archivos fuente en lugar del ensamblado

Si tiene acceso a los archivos fuente del proyecto, entonces una opción alternativa para obtener una lista de información de tipo es buscar en los archivos un patrón de expresión regular. Una forma de hacerlo es usando PowerShell:

 ls -r "D:\Projects\aspdotnet-background-dblogger\" | select-string -pattern "public class \w+" -raw
Code language: PowerShell (powershell)

Esto genera todos los nombres de clase públicos definidos en el proyecto:

public class Program
public class Startup
public class RecipesController : ControllerBase
public class DatabaseLoggerService : BackgroundService, ILoggerService
public class LogMessage
public class LogRepository : ILogRepositoryCode language: plaintext (plaintext)

Este es un enfoque heurístico. Es más simple que los otros enfoques que se muestran, pero también es menos preciso ya que puede arrojar falsos positivos. Por ejemplo, devolvería clases que ni siquiera están compiladas en el ensamblado, como las siguientes clases comentadas y compiladas condicionalmente:

/*
    [Route("[controller]")]
    [ApiController]
    public class WeatherController : ControllerBase
    {
       
    }
*/

#if MOVIES
    [Route("[controller]")]
    [ApiController]
    public class MoviesController : ControllerBase
    {

    }
#endif
Code language: C# (cs)

Ejecutar la búsqueda devolvería todas las cadenas que coinciden con el patrón de expresiones regulares "clase pública \w+":

public class Program
public class Startup
public class RecipesController : ControllerBase
public class WeatherController : ControllerBase
public class MoviesController : ControllerBase
public class DatabaseLoggerService : BackgroundService, ILoggerService
public class LogMessage
public class LogRepository : ILogRepository
Code language: plaintext (plaintext)

Tenga en cuenta que incluía las clases comentadas/compiladas (resaltadas). Esto puede o no ser un problema dependiendo de cómo estés usando esta información.