C# – Ottieni un elenco di tipi definiti in un assembly senza caricarlo

C# – Ottieni un elenco di tipi definiti in un assembly senza caricarlo

Esistono due modi per ottenere informazioni sul tipo da un assembly senza caricarlo:

  • Solo riflesso/caricamento dei metadati.
  • Cerca un pattern tra i file di origine.

In questo articolo, mostrerò entrambi gli approcci per l'output di un elenco di tipi in un assembly.

Solo riflesso/caricamento dei metadati

Potrebbero esserci molte ragioni per cui non vorresti caricare l'assieme. Forse stai riscontrando errori nel tentativo di caricarlo utilizzando Assembly.LoadFrom(). Ad esempio, potresti avere problemi a risolvere le dipendenze o forse stai riscontrando un'eccezione di formato immagine errata.

Il punto di eseguire un caricamento di sola riflessione è che puoi leggere i metadati (come i tipi definiti) senza incorrere in tutti i problemi che derivano dal tentativo di caricare completamente l'assembly.

Questa operazione viene eseguita in modo diverso a seconda che si stia eseguendo questo codice da .NET Framework o .NET Core.

.NET Framework:utilizza Assembly.ReflectionOnlyLoadFrom()

Per eseguire un caricamento di sola riflessione da un progetto .NET Framework, utilizzare Assembly.ReflectionOnlyLoadFrom(), in questo modo:

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)

Questo restituisce il seguente nome di classe:

DatabaseHelperCode language: plaintext (plaintext)

.NET Core

Non puoi usare Assembly.ReflectionOnlyLoadFrom() in un progetto .NET Core. Se provi, otterrai la seguente eccezione di runtime:

Invece, puoi usare System.Reflection.Metadata.MetadataReader o System.Reflection.MetadataLoadContext. Mostrerò come entrambi si avvicinano di seguito.

Utilizza MetadataReader

Puoi usare la classe System.Reflection.Metadata.MetadataReader in questo modo:

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)

Questo restituisce tutti i tipi di calcestruzzo pubblico definiti dall'utente nell'assieme:

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

Nota:questo include tipi non di classe (come struct ed enum). Non sono riuscito a trovare un modo per utilizzare MetadataReader per determinare se fosse strettamente una classe o meno.

Non è così bello e pulito come usare Assembly.ReflectionOnlyLoadFrom(), ma fa il suo lavoro.

Utilizza System.Reflection.MetadataLoadContext

MetadataLoadContext esegue una lettura di sola riflessione dell'assembly e fornisce un oggetto Assembly in modo da poter utilizzare l'API di riflessione.

Innanzitutto, devi installare il pacchetto nuget System.Reflection.MetadataLoadContext. Puoi installarlo con il seguente comando in Package Manager Console (Visualizza> Altre finestre> Package Manager Console) :

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

Quindi puoi utilizzare MetadataLoadContext per generare l'elenco dei nomi dei tipi pubblici nell'assembly, in questo modo:

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)

Questo restituisce tutti i nomi dei tipi pubblici:

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

Cerca nei file di origine invece che nell'assieme

Se hai accesso ai file di origine del progetto, un'opzione alternativa per ottenere un elenco di informazioni sul tipo consiste nel cercare tra i file un modello regex. Un modo per farlo è utilizzare PowerShell:

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

Questo restituisce tutti i nomi delle classi pubbliche definiti nel progetto:

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)

Questo è un approccio euristico. È più semplice degli altri approcci mostrati, ma è anche meno accurato poiché può restituire falsi positivi. Ad esempio, restituirebbe classi che non sono nemmeno compilate nell'assembly, come le seguenti classi commentate e compilate in modo condizionale:

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

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

    }
#endif
Code language: C# (cs)

L'esecuzione della ricerca restituirebbe tutte le stringhe corrispondenti al modello regex "classe pubblica \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)

Si noti che includeva le classi commentate / compilate (evidenziate). Questo potrebbe essere o meno un problema a seconda di come stai utilizzando queste informazioni.