Compartilhar via


Sobre System.Runtime.Loader.AssemblyLoadContext

A AssemblyLoadContext classe foi introduzida no .NET Core e não está disponível no .NET Framework. Este artigo complementa a documentação da AssemblyLoadContext API com informações conceituais.

Este artigo é relevante para os desenvolvedores que implementam o carregamento dinâmico, especialmente os desenvolvedores da estrutura de carregamento dinâmico.

O que é o AssemblyLoadContext?

Cada aplicativo .NET 5+ e .NET Core usa AssemblyLoadContextimplicitamente. É o fornecedor do ambiente de execução responsável por localizar e carregar dependências. Sempre que uma dependência é carregada, uma AssemblyLoadContext instância é 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, ele cria um contexto isolado para carregar código e suas dependências em sua própria AssemblyLoadContext instância.

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 será bem-sucedida somente se a versão carregada for igual ou superior à versão solicitada.

Quando você precisa de várias instâncias de AssemblyLoadContext?

A restrição de que uma única AssemblyLoadContext instância 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 independentemente e os módulos podem depender de diferentes versões de um Assembly. Isso geralmente é um problema quando módulos diferentes dependem de diferentes versões de uma biblioteca comumente usada.

Para dar suporte ao carregamento dinâmico de código, a API AssemblyLoadContext permite o carregamento de versões conflitantes de um Assembly dentro do mesmo aplicativo. Cada instância AssemblyLoadContext fornece um dicionário exclusivo que mapeia cada AssemblyName.Name para uma instância específica Assembly.

Ele também fornece um mecanismo conveniente para agrupar dependências relacionadas a um módulo de código para descarregamento posterior.

A instância AssemblyLoadContext.Default

A AssemblyLoadContext.Default instância é preenchida automaticamente pelo runtime na inicialização. Ele utiliza a investigação padrão para localizar e identificar 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 sempre resultar na mesma resposta. A mesma instância de dependência carregada deve ser retornada. Esse requisito é fundamental 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 manipuladores implementam as regras de carregamento para localizar dependências. Sua 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 dependências é específica do aplicativo. A opção é implementada por esses eventos e funções. Quando o código chama as funções de carregamento por caminho do AssemblyLoadContext , chame-as na instância em que você deseja carregar o código. Em algum momento, retornar null e deixar que o AssemblyLoadContext.Default lide 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 lógica extra que não trate vários threads corretamente.

Como as dependências dinâmicas são isoladas?

Cada AssemblyLoadContext instância representa um escopo exclusivo para Assembly instâncias e Type definições.

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:

Dependências compartilhadas

As dependências podem ser compartilhadas com facilidade entre instâncias AssemblyLoadContext. O modelo geral é 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, WPFou WinForms.

É recomendável que as dependências compartilhadas sejam carregadas em 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 Assembly instância que foi carregada em outra AssemblyLoadContext instância, a Assembly instância é 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 AssemblyLoadContext instâncias contêm definições de tipo com o mesmo nametipo, elas não são do mesmo tipo. Eles são do mesmo tipo se e somente se vierem da mesma Assembly instância.

Para complicar as coisas, as mensagens de exceção sobre esses tipos incompatíveis podem ser confusas. Os tipos são referenciados nas mensagens de exceção por seus nomes de tipo simples. A mensagem de exceção comum nesse caso é da forma:

O objeto do tipo 'IsolatedType' não pode ser convertido no tipo 'IsolatedType'.

Depurar problemas de conversão de tipo

Dado um par de tipos incompatíveis, é importante também saber:

Considerando dois objetos a e b, avaliando 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.

  1. Use tipos compartilhados comuns. Esse tipo compartilhado pode ser um tipo primitivo de tempo de execução ou pode envolver a criação de um novo tipo compartilhado em um conjunto 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.

  2. Use as técnicas de marshalling para converter um tipo em outro.