Sobre System.Runtime.Loader.AssemblyLoadContext
A classe AssemblyLoadContext foi introduzida no .NET Core e não está disponível no .NET Framework. Este artigo complementa a documentação da API AssemblyLoadContext com informações conceituais.
Este artigo é relevante para os desenvolvedores que implementam o carregamento dinâmico, especialmente os desenvolvedores de estrutura de carregamento dinâmico.
O que é o AssemblyLoadContext?
Cada aplicativo .NET 5+ e .NET Core usa implicitamente o AssemblyLoadContext. É o provedor do runtime para localizar e carregar dependências. Sempre que uma dependência é carregada, uma instância AssemblyLoadContext é invocada para localizá-la.
- AssemblyLoadContext fornece um serviço de localização, carregamento e cache de assemblies gerenciados e outras dependências.
- Para dar suporte ao carregamento e descarregamento de código dinâmico, ela cria um contexto isolado para carregar o código e as dependências em sua própria instância AssemblyLoadContext.
Regras de controle de versão
Uma só instância AssemblyLoadContext é limitada a carregar exatamente uma versão de um Assembly por nome de assembly simples. Quando uma referência de assembly é resolvida em uma instância AssemblyLoadContext que já tem um assembly com esse nome carregado, a versão solicitada é comparada à versão carregada. A resolução terá êxito apenas se a versão carregada for igual ou superior à solicitada.
Quando você precisa de várias instâncias do AssemblyLoadContext?
A restrição de que uma instância AssemblyLoadContext pode carregar apenas uma versão de um assembly pode se tornar um problema ao carregar módulos de código dinamicamente. Cada módulo é compilado de maneira independente e os módulos podem depender de versões diferentes de um Assembly. Isso costuma ser um problema quando módulos diferentes dependem de versões diferentes de uma biblioteca de uso comum.
Para dar suporte ao carregamento dinâmico de código, a API AssemblyLoadContext carrega versões conflitantes de um Assembly no mesmo aplicativo. Cada instância AssemblyLoadContext fornece um dicionário exclusivo que mapeia cada AssemblyName.Name para uma instância específica de Assembly.
Ela também fornece um mecanismo prático para agrupar as dependências relacionadas a um módulo de código para descarregamento posterior.
A instância AssemblyLoadContext.Default
A instância AssemblyLoadContext.Default é preenchida automaticamente pelo runtime na inicialização. Ela usa a investigação padrão para localizar e encontrar todas as dependências estáticas.
Ela resolve os cenários mais comuns de carregamento de dependência.
Dependências dinâmicas
AssemblyLoadContext tem vários eventos e funções virtuais que podem ser substituídos.
Há suporte apenas para substituição dos eventos na instância AssemblyLoadContext.Default.
Os artigos Algoritmo de carregamento de assembly gerenciado, Algoritmo de carregamento de assembly satélite e Algoritmo de carregamento de biblioteca não gerenciado (nativo) se referem a todos os eventos e funções virtuais disponíveis. Os artigos mostram a posição relativa de cada evento e função nos algoritmos de carregamento. Este artigo não reproduz essas informações.
Esta seção aborda os princípios gerais dos eventos e funções relevantes.
- Seja repetível. Uma consulta para uma dependência específica deve resultar sempre na mesma resposta. A mesma instância de dependência carregada deve ser retornada. Esse requisito é essencial para a consistência do cache. Para assemblies gerenciados especificamente, estamos criando um cache Assembly. A chave de cache é um nome simples de assembly, AssemblyName.Name.
- Normalmente, não gere. Espera-se que essas funções retornem
null
, em vez de serem geradas quando não for possível encontrar a dependência solicitada. A geração encerrará prematuramente a pesquisa e propagará uma exceção para o chamador. A geração deve ser restrita a erros inesperados, como um assembly corrompido ou uma condição de memória insuficiente. - Evite a recursão. Lembre-se de que essas funções e esses manipuladores implementam as regras de carregamento para localizar dependências. A implementação não deve chamar APIs que disparam a recursão. Normalmente, o código deve chamar as funções de carga do AssemblyLoadContext que exigem um argumento de referência de memória ou caminho específico.
- Carregue no AssemblyLoadContext correto. A escolha de onde carregar as dependências é específica do aplicativo. A escolha é implementada por esses eventos e essas funções. Quando o código chama as funções load-by-path do AssemblyLoadContext, chame-as na instância em que você deseja carregar o código. Às vezes, retornar
null
e deixar o AssemblyLoadContext.Default lidar com a carga pode ser a opção mais simples. - Lembre-se das corridas de thread. O carregamento pode ser disparado por vários threads. O AssemblyLoadContext lida com as corridas de thread adicionando atomicamente os assemblies ao cache. A instância do perdedor da corrida é descartada. Na lógica de implementação, não adicione outra lógica que não lide com vários threads corretamente.
Como as dependências dinâmicas são isoladas?
Cada instância AssemblyLoadContext representa um escopo exclusivo para instâncias Assembly e definições Type.
Não há isolamento binário entre essas dependências. Elas só são isoladas por não se encontrarem pelo nome.
Em cada AssemblyLoadContext:
- AssemblyName.Name pode se referir a uma instância diferente Assembly.
- Type.GetType pode retornar uma instância de tipo diferente para o mesmo tipo
name
.
Dependências compartilhadas
As dependências podem ser compartilhadas com facilidade entre instâncias AssemblyLoadContext. O modelo geral é para que um AssemblyLoadContext carregue uma dependência. O outro compartilha a dependência usando uma referência ao assembly carregado.
Esse compartilhamento é exigido dos assemblies do runtime. Esses assemblies só podem ser carregados no AssemblyLoadContext.Default. O mesmo é necessário para estruturas como ASP.NET
, WPF
ou WinForms
.
É recomendável que as dependências compartilhadas sejam carregadas no AssemblyLoadContext.Default. Esse compartilhamento é o padrão de design comum.
O compartilhamento é implementado na codificação da instância personalizada AssemblyLoadContext. AssemblyLoadContext tem vários eventos e funções virtuais que podem ser substituídos. Quando qualquer uma dessas funções retorna uma referência a uma instância Assembly que foi carregada em outra instância AssemblyLoadContext, a instância Assembly é compartilhada. O algoritmo de carga padrão adia para AssemblyLoadContext.Default, para carregamento, a fim de simplificar o padrão de compartilhamento comum. Para obter mais informações, confira o Algoritmo de carregamento de assembly gerenciado.
Problemas de conversão de tipo
Quando duas instâncias AssemblyLoadContext contêm definições de tipo com o mesmo name
, elas não são do mesmo tipo. Elas são do mesmo tipo, se e somente se vieram da mesma instância Assembly.
Para pior a situação, as mensagens de exceção sobre esses tipos incompatíveis podem ser confusas. Os tipos são referenciados nas mensagens de exceção pelos nomes simples de tipo. A mensagem de exceção comum, nesse caso, é do formulário:
O objeto do tipo 'IsolatedType' não pode ser convertido no tipo 'IsolatedType'.
Depurar problemas de conversão de tipo
Considerando um par de tipos incompatíveis, também é importante saber:
- O Type.Assembly de cada tipo.
- O AssemblyLoadContext de cada tipo, que pode ser obtido por meio da função AssemblyLoadContext.GetLoadContext(Assembly).
Considerando dois objetos a
e b
, avaliar o seguinte no depurador será útil:
// In debugger look at each assembly's instance, Location, and FullName
a.GetType().Assembly
b.GetType().Assembly
// In debugger look at each AssemblyLoadContext's instance and name
System.Runtime.Loader.AssemblyLoadContext.GetLoadContext(a.GetType().Assembly)
System.Runtime.Loader.AssemblyLoadContext.GetLoadContext(b.GetType().Assembly)
Resolver problemas de conversão de tipo
Há dois padrões de design para resolver esses problemas de conversão de tipo.
Use os tipos compartilhados comuns. Esse tipo compartilhado pode ser um tipo de runtime primitivo ou pode envolver a criação de um novo tipo compartilhado em um assembly compartilhado. Geralmente, o tipo compartilhado é uma interface definida em um assembly de aplicativo. Para obter mais informações, leia sobre como as dependências são compartilhadas.
Use as técnicas de marshalling para converter um tipo em outro.