Compartilhar via


Contexto de instância durável

O exemplo Durável demonstra como personalizar o runtime do WCF (Windows Communication Foundation) para habilitar contextos de instância durável. Ele usa o SQL Server 2005 como repositório de suporte (SQL Server 2005 Express, neste caso). No entanto, ele também fornece uma maneira de acessar mecanismos de armazenamento personalizados.

Observação

O procedimento de instalação e as instruções de compilação dessa amostra estão localizadas no final deste artigo.

Este exemplo envolve estender tanto a camada do canal quanto a camada do modelo de serviço do WCF. Portanto, é necessário entender os conceitos subjacentes antes de entrar nos detalhes da implementação.

Os contextos de instância duráveis podem ser encontrados em cenários do mundo real com bastante frequência. Um aplicativo de carrinho de compras, por exemplo, pode pausar as compras no meio do caminho e continuar em outro dia. Para que quando visitarmos o carrinho de compras no dia seguinte, nosso contexto original seja restaurado. É importante observar que o aplicativo de carrinho de compras (no servidor) não mantém a instância do carrinho de compras enquanto estamos desconectados. Ele, na verdade, persiste seu estado em uma mídia de armazenamento durável e o usa ao criar uma nova instância para o contexto restaurado. Portanto, a instância de serviço que pode atender ao mesmo contexto não é a mesma instância anterior (ou seja, não tem o mesmo endereço de memória).

O contexto de instância durável é viabilizado por um protocolo pequeno que troca uma ID de contexto entre o cliente e o serviço. Essa ID de contexto é criada no cliente e transmitida ao serviço. Quando a instância de serviço é criada, o runtime de serviço tenta carregar o estado persistente que corresponde a essa ID de contexto de um armazenamento persistente (por padrão, é um banco de dados SQL Server 2005). Se nenhum estado estiver disponível, a nova instância terá seu estado padrão. A implementação do serviço usa um atributo personalizado para marcar operações que alteram o estado da implementação do serviço para que o runtime possa salvar a instância de serviço depois de invocá-la.

Pela descrição anterior, duas etapas podem ser facilmente diferenciadas para atingir a meta:

  1. Alterar a mensagem que vai na rede para carregar a ID de contexto.
  2. Alterar o comportamento local do serviço para implementar a lógica de instanciação personalizada.

Como a primeira na lista afeta as mensagens na transmissão, ela deve ser implementada como um canal personalizado e ser conectada à camada de canal. Essa última afeta apenas o comportamento local do serviço e, portanto, pode ser implementada com a extensão de vários pontos de extensibilidade de serviço. Nas próximas seções, cada uma dessas extensões será discutida.

Canal InstanceContext durável

A primeira coisa a ser observada é uma extensão de camada de canal. A primeira etapa na criação de um canal personalizado é decidir a estrutura de comunicação do canal. Como um novo protocolo de transmissão está sendo introduzido, o canal deve funcionar com quase todos os outros canais na pilha de canais. Portanto, ele deve dar suporte a todos os padrões de troca de mensagens. No entanto, a funcionalidade principal do canal é a mesma, independentemente de sua estrutura de comunicação. Mais especificamente: do cliente, ele deve gravar a ID de contexto nas mensagens e, do serviço, deve ler essa ID de contexto das mensagens e transmiti-la aos níveis superiores. Por isso, uma classe DurableInstanceContextChannelBase é criada para atuar como a classe base abstrata em todas as implementações de canal de contexto de instância durável. Essa classe contém as funções comuns de gerenciamento de máquina de estado e dois membros protegidos para aplicar e ler as informações de contexto de mensagens e para elas.

class DurableInstanceContextChannelBase
{
  //…
  protected void ApplyContext(Message message)
  {
    //…
  }
  protected string ReadContextId(Message message)
  {
    //…
  }
}

Esses dois métodos usam implementações IContextManager para gravar e ler a ID de contexto recebida da mensagem ou enviada por ela. (IContextManager é uma interface personalizada usada para definir o contrato de todos os gerentes de contexto.) O canal pode incluir a ID de contexto em um cabeçalho SOAP personalizado ou em um cabeçalho de cookie HTTP. Cada implementação do gerenciador de contexto herda da classe ContextManagerBase que contém a funcionalidade comum a todos os gerentes de contexto. O método GetContextId nessa classe é usado para originar a ID de contexto do cliente. Quando uma ID de contexto é originada pela primeira vez, esse método a salva em um arquivo de texto cujo nome é construído pelo endereço do ponto de extremidade remoto (os caracteres de nome de arquivo inválidos nos URIs típicos são substituídos por caracteres @).

Posteriormente, quando a ID de contexto for exigida para o mesmo ponto de extremidade remoto, ele verificará se existe um arquivo apropriado. Se isso acontecer, ele lerá a ID de contexto e retornará. Caso contrário, ele retornará uma ID de contexto recém-gerada e a salvará em um arquivo. Com a configuração padrão, esses arquivos são colocados em um diretório chamado ContextStore, que reside no diretório temporário do usuário atual. No entanto, esse local pode ser configurado com o uso do elemento de associação.

O mecanismo usado para transportar a ID de contexto é configurável. Ele pode ser gravado no cabeçalho de cookie HTTP ou em um cabeçalho SOAP personalizado. A abordagem de cabeçalho SOAP personalizado possibilita o uso desse protocolo com protocolos não HTTP (por exemplo, TCP ou Pipes Nomeados). Há duas classes, MessageHeaderContextManager e HttpCookieContextManager, que implementam essas duas opções.

Ambas gravam a ID de contexto na mensagem adequadamente. Por exemplo, a classe MessageHeaderContextManager a grava em um cabeçalho SOAP no método WriteContext.

public override void WriteContext(Message message)
{
  string contextId = this.GetContextId();

  MessageHeader contextHeader =
    MessageHeader.CreateHeader(DurableInstanceContextUtility.HeaderName,
      DurableInstanceContextUtility.HeaderNamespace,
      contextId,
      true);

  message.Headers.Add(contextHeader);
}

Os métodos ApplyContext e ReadContextId na classe DurableInstanceContextChannelBase invocam IContextManager.ReadContext e IContextManager.WriteContext, respectivamente. No entanto, esses gerentes de contexto não são criados diretamente pela classe DurableInstanceContextChannelBase. Em vez disso, ela usa a classe ContextManagerFactory para fazer esse trabalho.

IContextManager contextManager =
                ContextManagerFactory.CreateContextManager(contextType,
                this.contextStoreLocation,
                this.endpointAddress);

O método ApplyContext é invocado pelos canais de envio. Ele injeta a ID de contexto nas mensagens de saída. O método ReadContextId é invocado pelos canais de recebimento. Esse método garante que a ID de contexto estará disponível nas mensagens de entrada e a adiciona à coleção Properties da classe Message. Ele também gera uma CommunicationException em caso de falha ao ler a ID de contexto e, portanto, faz com que o canal seja anulado.

message.Properties.Add(DurableInstanceContextUtility.ContextIdProperty, contextId);

Antes de continuar, é importante entender o uso da coleção Properties na classe Message. Normalmente, essa coleção Properties é usada ao transmitir dados de níveis inferiores para níveis superiores da camada de canal. Dessa forma, os dados desejados podem ser fornecidos aos níveis superiores de forma consistente, independentemente dos detalhes do protocolo. Em outras palavras, a camada de canal pode enviar e receber a ID de contexto como um cabeçalho SOAP ou um cabeçalho de cookie HTTP. Mas não é necessário que os níveis superiores saibam sobre esses detalhes porque a camada de canal disponibiliza essas informações na coleção Properties.

Agora, com a classe DurableInstanceContextChannelBase, todas as dez interfaces necessárias (IOutputChannel, IInputChannel, IOutputSessionChannel, IInputSessionChannel, IRequestChannel, IReplyChannel, IRequestSessionChannel, IReplySessionChannel, IDuplexChannel, IDuplexSessionChannel) precisam ser implementadas. Elas se assemelham a cada padrão de troca de mensagens disponível (datagram, simplex, duplex e suas variantes de sessão). Cada uma dessas implementações herda a classe base descrita anteriormente e chama ApplyContext e ReadContextId apropriadamente. Por exemplo, DurableInstanceContextOutputChannel, que implementa a interface IOutputChannel, chama o método ApplyContext de cada método que envia as mensagens.

public void Send(Message message, TimeSpan timeout)
{
    // Apply the context information before sending the message.
    this.ApplyContext(message);
    //…
}

Por outro lado, DurableInstanceContextInputChannel, que implementa a interface IInputChannel, chama o método ReadContextId em cada método que recebe as mensagens.

public Message Receive(TimeSpan timeout)
{
    //…
      ReadContextId(message);
      return message;
}

Além disso, essas implementações de canal delegam as invocações de método ao canal abaixo delas na pilha de canais. No entanto, as variantes com sessão têm uma lógica básica para garantir que a ID de contexto seja enviada e lida somente para a primeira mensagem que gera a criação da sessão.

if (isFirstMessage)
{
//…
    this.ApplyContext(message);
    isFirstMessage = false;
}

Essas implementações de canal são adicionadas ao runtime do canal WCF pelas classes DurableInstanceContextBindingElement e DurableInstanceContextBindingElementSection adequadamente. Confira a documentação de exemplo do canal HttpCookieSession para obter mais detalhes sobre elementos de associação e seções de elemento de associação.

Extensões de camada do modelo de serviço

Agora que a ID de contexto percorreu a camada do canal, o comportamento do serviço pode ser implementado para personalizar a instanciação. Neste exemplo, um gerenciador de armazenamento é usado para carregar e salvar o estado enviado e recebido do repositório persistente. Conforme explicado anteriormente, esse exemplo fornece um gerenciador de armazenamento que usa o SQL Server 2005 como repositório de backup. No entanto, também é possível adicionar mecanismos de armazenamento personalizados a essa extensão. Para fazer isso, uma interface pública é declarada, o que precisa ser implementado por todos os gerenciadores de armazenamento.

public interface IStorageManager
{
    object GetInstance(string contextId, Type type);
    void SaveInstance(string contextId, object state);
}

A classe SqlServerStorageManager contém a implementação padrão IStorageManager. Em seu método SaveInstance, o objeto fornecido é serializado com o uso do XmlSerializer e é salvo no banco de dados SQL Server.

XmlSerializer serializer = new XmlSerializer(state.GetType());
string data;

using (StringWriter writer = new StringWriter(CultureInfo.InvariantCulture))
{
    serializer.Serialize(writer, state);
    data = writer.ToString();
}

using (SqlConnection connection = new SqlConnection(GetConnectionString()))
{
    connection.Open();

    string update = @"UPDATE Instances SET Instance = @instance WHERE ContextId = @contextId";

    using (SqlCommand command = new SqlCommand(update, connection))
    {
        command.Parameters.Add("@instance", SqlDbType.VarChar, 2147483647).Value = data;
        command.Parameters.Add("@contextId", SqlDbType.VarChar, 256).Value = contextId;

        int rows = command.ExecuteNonQuery();

        if (rows == 0)
        {
            string insert = @"INSERT INTO Instances(ContextId, Instance) VALUES(@contextId, @instance)";
            command.CommandText = insert;
            command.ExecuteNonQuery();
        }
    }
}

No método GetInstance, os dados serializados são lidos para determinada ID de contexto e o objeto construído com base nela é retornado ao chamador.

object data;
using (SqlConnection connection = new SqlConnection(GetConnectionString()))
{
    connection.Open();

    string select = "SELECT Instance FROM Instances WHERE ContextId = @contextId";
    using (SqlCommand command = new SqlCommand(select, connection))
    {
        command.Parameters.Add("@contextId", SqlDbType.VarChar, 256).Value = contextId;
        data = command.ExecuteScalar();
    }
}

if (data != null)
{
    XmlSerializer serializer = new XmlSerializer(type);
    using (StringReader reader = new StringReader((string)data))
    {
        object instance = serializer.Deserialize(reader);
        return instance;
    }
}

Os usuários desses gerenciadores de armazenamento não devem instanciá-los diretamente. Eles usam a classe StorageManagerFactory, que abstrai dos detalhes de criação do gerenciador de armazenamento. Essa classe tem um membro estático, GetStorageManager, que cria uma instância de determinado tipo de gerenciador de armazenamento. Se o parâmetro de tipo for null, esse método criará uma instância da classe padrão SqlServerStorageManager e a retornará. Ele também valida o tipo fornecido para garantir que ele implementará a interface IStorageManager.

public static IStorageManager GetStorageManager(Type storageManagerType)
{
IStorageManager storageManager = null;

if (storageManagerType == null)
{
    return new SqlServerStorageManager();
}
else
{
    object obj = Activator.CreateInstance(storageManagerType);

    // Throw if the specified storage manager type does not
    // implement IStorageManager.
    if (obj is IStorageManager)
    {
        storageManager = (IStorageManager)obj;
    }
    else
    {
        throw new InvalidOperationException(
                  ResourceHelper.GetString("ExInvalidStorageManager"));
    }

    return storageManager;
}
}

A infraestrutura necessária para ler e gravar instâncias do armazenamento persistente é implementada. Agora, as etapas necessárias para alterar o comportamento do serviço precisam ser seguidas.

Como a primeira etapa desse processo, precisamos salvar a ID de contexto, que veio por meio da camada de canal para o InstanceContext atual. O InstanceContext é um componente de runtime que atua como o link entre o dispatcher do WCF e a instância de serviço. Ele pode ser usado para fornecer estado e comportamento adicionais à instância de serviço. Isso é essencial porque, na comunicação com sessão, a ID de contexto é enviada somente com a primeira mensagem.

O WCF permite estender seu componente de runtime InstanceContext com a adição de um novo estado e comportamento por meio do padrão de objeto extensível. O padrão de objeto extensível é usado no WCF para estender classes de runtime existentes com nova funcionalidade ou adicionar novos recursos de estado a um objeto. Há três interfaces no padrão de objeto extensível: IExtensibleObject<T>, IExtension<T>e IExtensionCollection<T>:

  • A interface IExtensibleObject<T> é implementada por objetos que permitem extensões que personalizam sua funcionalidade.

  • A interface IExtension<T> é implementada por objetos que são extensões de classes do tipo T.

  • A interface IExtensionCollection<T> é uma coleção de IExtensions que permite a recuperação de IExtensions por seu tipo.

Portanto, uma classe InstanceContextExtension deve ser criada para implementar a interface IExtension e definir o estado necessário a fim de salvar a ID de contexto. Essa classe também fornece o estado para manter o gerenciador de armazenamento em uso. Depois que o novo estado for salvo, não será possível modificá-lo. Portanto, o estado é fornecido e salvo na instância no momento em que está sendo construído e, em seguida, só poderá ser acessado com o uso de propriedades somente leitura.

// Constructor
public DurableInstanceContextExtension(string contextId,
            IStorageManager storageManager)
{
    this.contextId = contextId;
    this.storageManager = storageManager;
}

// Read only properties
public string ContextId
{
    get { return this.contextId; }
}

public IStorageManager StorageManager
{
    get { return this.storageManager; }
}

A classe InstanceContextInitializer implementa a interface IInstanceContextInitializer e adiciona a extensão de contexto da instância à coleção Extensions do InstanceContext que está sendo construído.

public void Initialize(InstanceContext instanceContext, Message message)
{
    string contextId =
  (string)message.Properties[DurableInstanceContextUtility.ContextIdProperty];

    DurableInstanceContextExtension extension =
                new DurableInstanceContextExtension(contextId,
                     storageManager);
    instanceContext.Extensions.Add(extension);
}

Conforme descrito anteriormente, a ID de contexto é lida da coleção Properties da classe Message e transmitida ao construtor da classe de extensão. Isso demonstra como as informações podem ser trocadas entre as camadas de maneira consistente.

A próxima etapa importante é substituir o processo de criação da instância de serviço. O WCF permite implementar comportamentos de instanciação personalizados e conectá-los ao runtime usando a interface IInstanceProvider. A nova classe InstanceProvider é implementada para fazer esse trabalho. O tipo de serviço esperado do provedor de instância é aceito no construtor. Posteriormente, isso é usado para criar outras instâncias. Na implementação GetInstance, uma instância de um gerenciador de armazenamento é criada para procurar uma instância persistente. Se ela retornar null, uma nova instância do tipo de serviço será instanciada e retornada ao chamador.

public object GetInstance(InstanceContext instanceContext, Message message)
{
    object instance = null;

    DurableInstanceContextExtension extension =
    instanceContext.Extensions.Find<DurableInstanceContextExtension>();

    string contextId = extension.ContextId;
    IStorageManager storageManager = extension.StorageManager;

    instance = storageManager.GetInstance(contextId, serviceType);

    instance ??= Activator.CreateInstance(serviceType);
    return instance;
}

A próxima etapa importante é instalar as classes InstanceContextExtension, InstanceContextInitializer e InstanceProvider no runtime do modelo de serviço. Um atributo personalizado pode ser usado para marcar as classes de implementação de serviço a fim de instalar o comportamento. O DurableInstanceContextAttribute contém a implementação desse atributo e implementa a interface IServiceBehavior para estender todo o runtime de serviço.

Essa classe tem uma propriedade que aceita o tipo do gerenciador de armazenamento a ser usado. Dessa forma, a implementação permite que os usuários especifiquem sua própria implementação IStorageManager como parâmetro desse atributo.

Na implementação ApplyDispatchBehavior, o InstanceContextMode do atributo ServiceBehavior atual está sendo verificado. Se essa propriedade estiver definida como Singleton, a habilitação de instanciamento durável não será possível e uma InvalidOperationException será lançada para notificar o host.

ServiceBehaviorAttribute serviceBehavior =
    serviceDescription.Behaviors.Find<ServiceBehaviorAttribute>();

if (serviceBehavior != null &&
     serviceBehavior.InstanceContextMode == InstanceContextMode.Single)
{
    throw new InvalidOperationException(
       ResourceHelper.GetString("ExSingletonInstancingNotSupported"));
}

Depois disso, as instâncias do gerenciador de armazenamento, o inicializador de contexto da instância e o provedor de instância serão criados e instalados no DispatchRuntime criado para cada ponto de extremidade.

IStorageManager storageManager =
    StorageManagerFactory.GetStorageManager(storageManagerType);

InstanceContextInitializer contextInitializer =
    new InstanceContextInitializer(storageManager);

InstanceProvider instanceProvider =
    new InstanceProvider(description.ServiceType);

foreach (ChannelDispatcherBase cdb in serviceHostBase.ChannelDispatchers)
{
    ChannelDispatcher cd = cdb as ChannelDispatcher;

    if (cd != null)
    {
        foreach (EndpointDispatcher ed in cd.Endpoints)
        {
            ed.DispatchRuntime.InstanceContextInitializers.Add(contextInitializer);
            ed.DispatchRuntime.InstanceProvider = instanceProvider;
        }
    }
}

Em resumo até agora, este exemplo produziu um canal que habilitou o protocolo de transmissão personalizada para trocas de ID de contexto personalizadas e também substitui o comportamento de instanciação padrão para carregar as instâncias do armazenamento persistente.

O que resta é uma maneira de salvar a instância de serviço no armazenamento persistente. Conforme discutido anteriormente, já existe a funcionalidade necessária para salvar o estado em uma implementação IStorageManager. Agora, precisamos integrar isso ao runtime do WCF. Outro atributo exigido é aplicável aos métodos na classe de implementação de serviço. Esse atributo deve ser aplicado aos métodos que alteram o estado da instância de serviço.

A classe SaveStateAttribute implementa essa funcionalidade. Ela também implementa a classe IOperationBehavior a fim de modificar o runtime do WCF para cada operação. Quando um método é marcado com esse atributo, o runtime do WCF invoca o método ApplyBehavior enquanto a DispatchOperation apropriada está sendo construída. Há uma única linha de código nessa implementação de método:

dispatch.Invoker = new OperationInvoker(dispatch.Invoker);

Essa instrução cria uma instância do tipo OperationInvoker e a atribui à propriedade Invoker da DispatchOperation que está sendo construída. A classe OperationInvoker é um wrapper para o invocador de operação padrão criado para a DispatchOperation. Essa classe implementa a interface IOperationInvoker. Na implementação do método Invoke, a invocação do método real é delegada ao invocador de operação interna. No entanto, antes de retornar os resultados, o gerenciador de armazenamento no InstanceContext é usado para salvar a instância de serviço.

object result = innerOperationInvoker.Invoke(instance,
    inputs, out outputs);

// Save the instance using the storage manager saved in the
// current InstanceContext.
InstanceContextExtension extension =
    OperationContext.Current.InstanceContext.Extensions.Find<InstanceContextExtension>();

extension.StorageManager.SaveInstance(extension.ContextId, instance);
return result;

Usando a extensão

As extensões de camada do canal e de camada do modelo de serviço foram concluídas e agora podem ser usadas em aplicativos WCF. Os serviços precisam adicionar o canal à pilha de canais usando uma associação personalizada e marcar as classes de implementação de serviço com os atributos apropriados.

[DurableInstanceContext]
[ServiceBehavior(InstanceContextMode=InstanceContextMode.PerSession)]
public class ShoppingCart : IShoppingCart
{
//…
     [SaveState]
     public int AddItem(string item)
     {
         //…
     }
//…
 }

Os aplicativos cliente devem adicionar o DurableInstanceContextChannel à pilha de canais usando uma associação personalizada. Para configurar o canal declarativamente no arquivo de configuração, a seção de elemento de associação deve ser adicionada à coleção de extensões de elemento de associação.

<system.serviceModel>
 <extensions>
   <bindingElementExtensions>
     <add name="durableInstanceContext"
type="Microsoft.ServiceModel.Samples.DurableInstanceContextBindingElementSection, DurableInstanceContextExtension, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
   </bindingElementExtensions>
 </extensions>
</system.serviceModel>

Agora, o elemento de associação pode ser usado com uma associação personalizada, assim como outros elementos de associação padrão:

<bindings>
 <customBinding>
   <binding name="TextOverHttp">
     <durableInstanceContext contextType="HttpCookie"/>
     <reliableSession />
     <textMessageEncoding />
     <httpTransport />
   </binding>
 </customBinding>
</bindings>

Conclusão

Este exemplo mostrou como criar um canal de protocolo personalizado e como personalizar o comportamento do serviço para habilitá-lo.

A extensão pode ser aprimorada ainda mais ao permitir que os usuários especifiquem a implementação IStorageManager usando uma seção de configuração. Isso possibilita modificar o repositório de backup sem recompilar o código de serviço.

Além disso, você pode tentar implementar uma classe (por exemplo, StateBag) que encapsula o estado da instância. Essa classe é responsável por persistir o estado sempre que ele for alterado. Dessa forma, você pode evitar o uso do atributo SaveState e executar o trabalho persistente com mais precisão (por exemplo, você pode persistir o estado quando ele for realmente alterado em vez de salvá-lo sempre que um método com o atributo SaveState for chamado).

Quando você executa a amostra, a saída a seguir é exibida. O cliente adiciona dois itens ao carrinho de compras e obtém a lista de itens em seu carrinho de compras do serviço. Pressione ENTER em cada janela do console para desligar o serviço e o cliente.

Enter the name of the product: apples
Enter the name of the product: bananas

Shopping cart currently contains the following items.
apples
bananas
Press ENTER to shut down client

Observação

A recompilação do serviço substitui o arquivo de banco de dados. Para observar o estado preservado em várias execuções da amostra, não recompile a amostra entre execuções.

Para configurar, compilar, e executar o exemplo

  1. Verifique se você executou o Procedimento de instalação única para os exemplos do Windows Communication Foundation.

  2. Para compilar a solução, siga as instruções contidas em Como compilar as amostras do Windows Communication Foundation.

  3. Para executar o exemplo em uma configuração de computador único ou cruzado, siga as instruções em Como executar os exemplos do Windows Communication Foundation.

Observação

Você precisa estar executando o SQL Server 2005 ou o SQL Express 2005 para executar este exemplo. Se estiver executando o SQL Server 2005, você precisará modificar a configuração da cadeia de conexão do serviço. Ao executar entre computadores, SQL Server só será necessário no computador do servidor.