Previsão: nublado

Desacoplando a nuvem com o MEF

Joseph Fultz
Chris Mabry

Baixar o código de exemplo

Joseph FultzPelos últimos meses, um colega e eu temos trabalhado em um projeto que aproveita o Microsoft Extensibility Framework (MEF). Neste artigo, daremos uma olhada em como você poderia usar o MEF para tornar uma implantação na nuvem um pouco mais gerenciável e flexível. O MEF, e estruturas similares como o Unity, é a malha de software que isenta os desenvolvedores de gerenciar a resolução de dependências, a criação de objetos e a instanciação. De vez em quando talvez você se encontre escrevendo um método de fabricação ou criando objetos dependentes dentro de um construtor ou método de inicialização requerido, mas na maioria das vezes tal trabalho não é mais necessário graças a estruturas como o MEF.

Ao usar o MEF em nossa implantação em conjunto com a API StorageClient, podemos implantar e disponibilizar novas classes sem reciclar ou reimplantar nossas funções da Web. Além disso, podemos implantar versões atualizadas de tipos na nuvem sem uma reimplantação completa e, em vez disso, simplesmente reciclar o aplicativo. Observe que embora estejamos usando o MEF aqui, seguir uma estrutura similar usando Unity, Castle Windsor, StructureMap ou qualquer dos outros contêineres similares deve dar os mesmos resultados, com as diferenças principais sendo a sintaxe e a semântica do registro de tipo.

Design e implantação

Com diz o ditado: para conseguir tirar um pouco mais, precisa colocar um pouco mais. Neste caso, isso requer certos padrões de construção e algum trabalho adicional em torno da implantação. Primeiro, se estiver acostumado a usar uma injeção de dependência (DI) ou um contêiner de composição, é bem provável que esteja bem interessado em manter a implementação e a interface separadas em seu código. Não nos desviamos deste objetivo aqui, todas as nossas implementações de classe concreta têm herança que leva a um tipo de interface. Isso não significa que toda classe herdará diretamente de uma interface, mas as classes terão, geralmente, camadas de abstração que seguem um padrão como Interface “ Virtual “ Concreta.

A Figura 1 mostra que não apenas a classe principal em que estou interessado tem tal cadeia, mas na realidade, uma de suas propriedades requeridas também está abstraída. Toda a abstração facilita substituir partes ou adicionar funcionalidade adicional na forma de uma nova biblioteca que exporta o contrato desejado (nesse caso, a interface). Além da composição, um bom efeito colateral de ser meticuloso em abstrair seu design de classe é que permite um melhor teste por meio de interfaces simuladas.

Class Diagram
Figura 1 Diagrama de classes

A parte mais difícil do requisito é a alteração no modelo de implantação do aplicativo. Como queremos criar nosso catálogo de importações e exportações em tempo de execução, e atualizá-lo sem ter de implantá-lo novamente, temos de implantar os binários que mantêm nossas classes concretas fora da implantação da função da Web. Isso também força um pequeno trabalho extra do aplicativo na inicialização. A Figura 2 mostra o trabalho de inicialização no Global.asax ao chamar uma classe auxiliar que criamos chamada MEFContext.

Building the Catalog at Startup
Figura 2 Criando o catálogo na inicialização

Composição em tempo de execução

Como estaremos carregando o catálogo a partir de arquivos no armazenamento, teremos de colocar esses arquivos em nosso contêiner de armazenamento em nuvem. Portanto, colocar os arquivos no local de armazenamento do Windows Azure precisa se tornar parte do processo de implantação. Provavelmente, isso é feito de forma mais fácil com os cmdlets do Windows Azure PowerShell (wappowershell.codeplex.com) e algumas etapas pós-compilação. Para atender aos nossos objetivos, moveremos manualmente os binários usando o Windows Azure Storage Explorer (azurestorageexplorer.codeplex.com).

Criamos um projeto que contém uma classe de diagnóstico comum, uma entidade Customer e algumas bibliotecas de regras. Todas as bibliotecas de regras têm de herdar de uma interface do tipo IBusinessRule<t> e exportá-la, em que t representa a entidade em relação à qual as regras são impostas. Aqui estão as partes de importação da declaração de classe de uma regra:

[Export(typeof(IBusinessRule<ICustomer>))]
public class CustomerNameRule : IBusinessRule<ICustomer>
{
  [Import(typeof(IDiagnostics))]
  IDiagnostics _diagnostics;
    ...
}

Você pode ver a exportação assim como a dependência de diagnóstico que o MEF injetará para nós quando pedirmos o objeto de regra. É importante saber o que está sendo exportado, pois isso será, por sua vez, o contrato pelo qual você resolve as instâncias desejadas. O Microsoft .NET Framework 4.5 trará alguns aprimoramentos ao MEF que permitirão uma flexibilização de algumas das restrições atualmente em torno de genéricos no contêiner. Por exemplo, atualmente é possível registrar e recuperar algo como IBusinessRule<ICustomer>, mas não algo como IBusiness-Rule<t>. Algumas vezes você deseja todas as instâncias de um tipo além de seu tipo de modelo real. Atualmente, a maneira mais fácil de realizar isso é registrar um nome de contrato de cadeia de caracteres que é uma convenção aceita em seu projeto ou solução. Em nosso exemplo, uma declaração como a anterior funcionará.

Temos duas regras, uma para número de telefone e uma para nome, e uma biblioteca de diagnósticos, cada uma das quais estará disponível através do contêiner do MEF. A primeira coisa que temos de fazer é retirar as bibliotecas do Armazenamento do Windows Azure e colocá-las em um recurso local (diretório local), para que possamos carregá-las com um DirectoryCatalog. Para isso, incluímos algumas funções de chamada no Application_Start do Global.asax:

// Store the local directory for later use (directory catalog)
MEFContext.CacheFolderPath = 
  RoleEnvironment.GetLocalResource("ResourceCache").RootPath.ToLower();
MEFContext.InitializeContainer();

Estamos apenas pegando o caminho de recurso necessário, o qual é configurado como parte da função da Web e, então, chamando o método para configurar o contêiner. O método de inicialização, por sua vez, chama UpdateFromStorage para obter os arquivos e BuildContainer para criar o catálogo e, em seguida, o contêiner do MEF.

O método UpdateFromStorage olha em um contêiner predeterminado e itera nos arquivos do contêiner, baixando cada um deles na pasta do recurso local. A primeira parte desse método é mostrada na Figura 3.

Figura 3 Primeira metade de UpdateFromStorage

// Could also pull from config, etc.
string containerName = CONTAINER_NAME;
// Using development storage account
CloudStorageAccount storageAccount = 
  CloudStorageAccount.DevelopmentStorageAccount;
// Create the blob client and use it to create the container object
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
// Note that here is where the container name is passed
// in order to get to the files we want
CloudBlobContainer blobContainer = new CloudBlobContainer(
  storageAccount.BlobEndpoint.ToString() + 
  "/" + containerName,
  blobClient);
// Create the options needed to get the blob list
BlobRequestOptions options = new BlobRequestOptions();
options.AccessCondition = AccessCondition.None;
options.BlobListingDetails = BlobListingDetails.All;
options.UseFlatBlobListing = true;
options.Timeout = new TimeSpan(0, 1, 0);

Na primeira metade, configuramos o cliente de armazenamento para buscar o que precisamos. Para esse cenário, estamos pedindo o que quer que esteja lá. Nos casos em que você está trazendo arquivos da memória para um recurso local, talvez valha a pena fazer uma passada completa e obter tudo. Para uma busca mais direcionada dos arquivos, você poderia atribuir alguma condição IfMatch à propriedade options.AccessCondition. Isso iria exigir que etags fossem definidas nos blobs ao serem carregadas. Além disso, você poderia otimizar o lado da atualização da reconstrução do contêiner do MEF armazenando o horário da última atualização e aplicando um AccessCondition de IfModifiedSince.

A Figura 4 mostra a segunda metade de UpdateFromStorage.

Figura 4 Segunda metade de UpdateFromStorage

// Iterate over the collect
// Grab the files and save them locally
foreach (IListBlobItem item in blobs)
{
  string fileAbsPath = item.Uri.AbsolutePath.ToLower();
  // Just want the file name ...
  fileAbsPath = 
    fileAbsPath.Substring(fileAbsPath.LastIndexOf('/') + 1);
  try
  {
    Microsoft.WindowsAzure.StorageClient.CloudPageBlob pageblob =
      new CloudPageBlob(item.Uri.ToString());
    pageblob.DownloadToFile(MEFContext.CacheFolderPath + fileAbsPath, 
      options);
  }
  catch (Exception)
  {
    // Ignore exceptions, if we can't write it's because
    // we've already got the file, move on
    }
}

Quando o cliente de armazenamento estiver pronto, simplesmente iremos iterar nos itens do blob e baixá-los no recurso. Dependendo das condições e dos objetivos do download geral, você poderia replicar as estruturas de pastas localmente nessa operação ou criar uma estrutura de pastas baseada em convenção. Algumas vezes uma estrutura de pastas é um requisito para evitar conflitos de nomes. Estamos apenas continuando com o método forçado e pegando todos os arquivos e os colocando em um lugar, pois sabemos que são apenas duas ou três DLLs nesse exemplo.

Com isso, temos os arquivos no lugar e apenas precisamos criar o contêiner. No MEF, o contêiner de composição é criado de um ou mais catálogos. Nesse caso, vamos usar um DirectoryCatalog porque isso torna mais fácil simplesmente apontar o catálogo para o diretório e carregar os binários que estão disponíveis. Assim, o código para registrar os tipos e preparar o contêiner é curto e simples:

// Store the container for later use (resolve type instances)
var catalog = new DirectoryCatalog(CacheFolderPath);
MEFContainer = new CompositionContainer(catalog);
MEFContainer.ComposeParts();

Agora, executaremos o site e deveremos ver um despejo dos tipos disponíveis no contêiner, conforme mostrado na Figure 5.

Initial Exports
Figura 5 Exportações iniciais

Não estamos despejando o contêiner inteiro aqui, mas, em vez disso, pedindo especificamente a interface IDiagnostics e, então, todas as exportações do tipo IBusinessRule<ICustomer>. Como pode ser visto, temos um de cada um desses antes de carregar uma nova biblioteca de regras de negócios no contêiner de armazenamento.

Colocamos NewRules.dll no local de armazenamento e agora precisamos carregá-lo no aplicativo. Idealmente, você quer disparar a reconstrução do contêiner fazendo um pouco de monitoramento de arquivo no contêiner de armazenamento. Novamente, isso é facilmente realizado com uma rápida pesquisa usando o IfModifiedSince AccessCondition. No entanto, optamos pelo processo mais manual de clicar em Update Catalog em seu aplicativo de teste. A Figura 8 mostra os resultados.

Updated Rules Exports
Figura 8 Exportações de regras atualizadas

Apenas repetimos as etapas para criar o catálogo e inicializar o contêiner, e agora temos uma nova biblioteca de regras para impor. Observe que não reiniciamos o aplicativo ou o reimplantamos, mas temos novo código sendo executado no ambiente. A única pendência aqui é que um método de sincronização é necessário, pois não podemos ter código tentando usar o contêiner de composição enquanto estamos substituindo a referência:

var catalog = new DirectoryCatalog(CacheFolderPath);
CompositionContainer newContainer = 
  new CompositionContainer(catalog);
newContainer.ComposeParts();
lock(MEFContainer)
{
  MEFContainer = newContainer;
}

A razão principal para criar um contêiner secundário e, então, simplesmente substituir a referência é reduzir o quantum de bloqueio e retornar o contêiner para uso imediatamente.

Para evoluir a base de código ainda mais, a próxima etapa seria implementar seu próprio tipo de catálogo personalizado, por exemplo, AzureStorageCatalog, conforme mostrado na Figura 9. Infelizmente, o modelo de objeto atual não tem uma interface adequada ou uma base facilmente reutilizada definida, assim, usar um pouco de herança além de algum encapsulamento talvez seja a melhor opção. Implementar uma classe similar à listagem AzureStorageCatalog iria permitir que um modelo simples instanciasse o catálogo personalizado e o usasse diretamente no contêiner de composição.

Figura 9 AzureStorageCatalog

public class AzureStorageCatalog:ComposablePartCatalog
{
  private string _localCatalogDirectory = default(string);
  private DirectoryCatalog _directoryCatalog = 
    default(DirectoryCatalog);
  AzureStorageCatalog(string StorageSetting, string ContainerName)
    :base()
  {
    // Pull the files to the local directory
    _localCatalogDirectory = 
      GetStorageCatalog(StorageSetting, ContainerName);
    // Load the exports using an encapsulated DirectoryCatalog
    _directoryCatalog = new DirectoryCatalog(_localCatalogDirectory);
  }
  // Return encapsulated parts
  public override IQueryable<ComposablePartDefinition> Parts
  {
    get { return _directoryCatalog.Parts; }
  }
  private string GetStorageCatalog(string StorageSetting, 
    string ContainerName)
  {  }
}

Atualizando funcionalidade existente

Adicionar nova funcionalidade à nossa implantação foi muito fácil, mas não temos as mesmas boas novas para atualizar funcionalidades ou bibliotecas existentes. Embora o processo seja melhor do que uma reimplantação completa, ele ainda está bastante envolvido, pois temos de mover os arquivos para o armazenamento e as funções da Web relevantes têm de atualizar suas pastas de recursos locais. No entanto, reciclaremos também as funções, pois precisamos descarregar e recarregar o AppDomain para atualizar a definição de tipo armazenada no contêiner. Mesmo que você carregue o contêiner de composição e os tipos em um AppDomain secundário e tente carregar a partir daí, o AppDomain em que está solicitando o tipo o carregará de metadados carregados anteriormente. A única maneira que vemos de contornar isso seria enviar as entidades para o AppDomain secundário e adicionar algum marshaling personalizado em vez de usar os tipos exportados do AppDomain principal. Esse padrão nos parece problemático; o AppDomain duplo por si só parece problemático. Assim, uma solução mais simples é reciclar as funções depois de disponibilizar os novos binários.

Há boas novas em relação aos domínios de atualização do Windows Azure. Examinem a minha coluna de fevereiro de 2012, “Domínios de implantação do Windows Azure” (msdn.microsoft.com/magazine/hh781019), que descreve a movimentação nos domínios de atualização e o reinício de instâncias em cada um. No lado positivo, o site fica ativo sem necessidade de uma reimplantação completa. No entanto, você poderia experimentar potencialmente dois comportamentos diferentes durante a atualização. No entanto, esse é um risco aceitável, pois o mesmo seria verdadeiro durante uma atualização sem interrupção, se fosse feita uma implantação completa.

Você poderia configurar para isso acontecer dentro da implantação, mas o problema é de coordenação. Fazer isso iria exigir que os reinícios das instâncias fossem coordenados, de modo que as instâncias precisariam eleger um líder ou ter algum sistema de votação. Em vez de escrever alguma inteligência artificial nas funções da Web, sentimos que a tarefa é mais facilmente manipulada por um processo de monitoramento e pelos cmdlets do Windows Azure referenciados anteriormente.

Há muitas razões para se usar uma estrutura como o MEF que está além da pequena porção de funcionalidade que destacamos aqui. O que queremos destacar é que, usando os recursos inerentes do Windows Azure em combinação com uma composição/DI/Inversão de estrutura do tipo Control, é possível criar um aplicativo em nuvem dinâmico que possa responder facilmente às alterações de última hora que sempre costumam aparecer.

Joseph Fultz é arquiteto de software da Hewlett-Packard Co. e trabalha no grupo de TI global do HP.com. Anteriormente, era arquiteto de software na Microsoft, trabalhando com seus clientes empresariais e ISV de camada superior, para definir soluções de arquitetura e design.

Chris Mabry é um desenvolvedor líder na Hewlett-Packard Co. com um foco atual na liderança de uma equipe para fornecer uma rica experiência de interface do usuário baseada em estruturas de cliente habilitadas para serviços.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Chris Brooks