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 desenvolvedores que implementam carregamento dinâmico, especialmente 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 tempo de execução para 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 armazenamento em cache de assemblies gerenciados e outras dependências.
  • Para dar suporte ao carregamento e descarregamento dinâmico de código, 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 única AssemblyLoadContext instância é limitada a carregar exatamente uma versão de um Assembly nome de assembly por simples. Quando uma referência de assembly é resolvida em relação a uma AssemblyLoadContext instância que já tem um assembly desse nome carregado, a versão solicitada é comparada à versão carregada. A resolução só terá êxito se a versão carregada for igual ou superior à versão solicitada.

Quando você precisa de várias instâncias 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 de forma independente, e os módulos podem depender de diferentes versões de um Assemblyarquivo . Isso geralmente é um problema quando módulos diferentes dependem de versões diferentes de uma biblioteca comumente usada.

Para suportar o carregamento dinâmico de código, a AssemblyLoadContext API fornece o carregamento de versões conflitantes de um Assembly no mesmo aplicativo. Cada AssemblyLoadContext instância fornece um dicionário exclusivo que mapeia cada AssemblyName.Name uma 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 descarga posterior.

A instância AssemblyLoadContext.Default

A AssemblyLoadContext.Default instância é preenchida automaticamente pelo tempo de execução na inicialização. Ele usa sondagem padrão para localizar e localizar todas as dependências estáticas.

Ele resolve os cenários de carregamento de dependência mais comuns.

Dependências dinâmicas

AssemblyLoadContext tem vários eventos e funções virtuais que podem ser substituídas.

A AssemblyLoadContext.Default instância suporta apenas a substituição dos eventos.

Os artigos Managed assembly loading algorithm, Satellite assembly loading algorithm e Unmanaged (native) library loading algorithm referem-se 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 essa informação.

Esta secção abrange os princípios gerais para os 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 em particular, estamos criando um Assembly cache. A chave de cache é um nome de assembly simples, AssemblyName.Name.
  • Normalmente não jogue. Espera-se que essas funções retornem null em vez de serem lançadas quando não for possível encontrar a dependência solicitada. O lançamento encerrará prematuramente a pesquisa e propagará uma exceção ao chamador. O lançamento deve ser restrito a erros inesperados, como um assembly corrompido ou uma condição de falta de memória.
  • 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 acionam a recursão. Seu código normalmente deve chamar funções de carregamento AssemblyLoadContext que exigem um caminho específico ou argumento de referência de memória.
  • Carregue no AssemblyLoadContext correto. A escolha de onde carregar dependências é específica do aplicativo. A escolha é implementada por esses eventos e funções. Quando seu código chama as funções load-by-path AssemblyLoadContext , chame-as na instância em que você deseja que o código seja carregado. Em algum momento, retornar null e deixar a AssemblyLoadContext.Default carga lidar com a carga pode ser a opção mais simples.
  • Esteja atento às corridas de fios. O carregamento pode ser acionado por vários threads. O AssemblyLoadContext lida com corridas de threads adicionando atomicamente assemblies ao seu cache. A instância do perdedor da corrida é descartada. Na sua lógica de implementação, não adicione lógica extra que não lide com 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. Eles só ficam isolados por não se encontrarem pelo nome.

Em cada um deles AssemblyLoadContext:

Dependências compartilhadas

As dependências podem ser facilmente compartilhadas entre AssemblyLoadContext instâncias. O modelo geral é para carregar AssemblyLoadContext uma dependência. O outro compartilha a dependência usando uma referência ao assembly carregado.

Esse compartilhamento é necessário para os assemblies de tempo de execução. 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 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ídas. 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 carregamento para simplificar o padrão de compartilhamento comum. Para obter mais informações, consulte 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 name, 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 mensagens de exceção sobre esses tipos incompatíveis podem ser confusas. Os tipos são referidos nas mensagens de exceção por seus nomes de tipo simples. A mensagem de exceção comum neste caso é da forma:

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

Depurar problemas de conversão de tipo

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

Dado 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.

  1. Use tipos compartilhados comuns. Esse tipo compartilhado pode ser um tipo de tempo de execução primitivo ou pode envolver a criação de um novo tipo compartilhado em um assembly compartilhado. Muitas vezes, 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 técnicas de agrupamento para converter de um tipo para outro.