Contexto de instância durável
O exemplo Durable demonstra como personalizar o tempo de execução do Windows Communication Foundation (WCF) para habilitar contextos de instância duráveis. Ele usa o SQL Server 2005 como seu armazenamento de suporte (SQL Server 2005 Express neste caso). No entanto, ele também fornece uma maneira de acessar mecanismos de armazenamento personalizados.
Nota
O procedimento de instalação e as instruções de compilação para este exemplo estão localizados no final deste artigo.
Este exemplo envolve a extensão da camada de canal e da camada de modelo de serviço do WCF. Por conseguinte, é necessário compreender os conceitos subjacentes antes de entrar nos pormenores da implementação.
Contextos de instância duráveis podem ser encontrados nos cenários do mundo real com bastante frequência. Um aplicativo de carrinho de compras, por exemplo, tem a capacidade de pausar as compras no meio do caminho e continuar em outro dia. Para que, quando visitarmos o carrinho de compras no dia seguinte, o nosso contexto original seja restaurado. É importante notar que o aplicativo de carrinho de compras (no servidor) não mantém a instância do carrinho de compras enquanto estamos desconectados. Em vez disso, ele persiste seu estado em uma mídia de armazenamento durável e o usa ao construir uma nova instância para o contexto restaurado. Portanto, a instância de serviço que pode servir para o mesmo contexto não é a mesma que a instância anterior (ou seja, não tem o mesmo endereço de memória).
O contexto de instância durável é possibilitado por um pequeno protocolo que troca um ID de contexto entre o cliente e o serviço. Este ID de contexto é criado no cliente e transmitido ao serviço. Quando a instância de serviço é criada, o tempo de execução do 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 do SQL Server 2005). Se nenhum estado estiver disponível, a nova instância terá seu estado padrão. A implementação de serviço usa um atributo personalizado para marcar operações que alteram o estado da implementação de serviço para que o tempo de execução possa salvar a instância de serviço depois de invocá-las.
Pela descrição anterior, dois passos podem ser facilmente distinguidos para alcançar o objetivo:
- Altere a mensagem que vai no fio para carregar o ID de contexto.
- Altere o comportamento local do serviço para implementar a lógica de instanciação personalizada.
Como o primeiro da lista afeta as mensagens no fio, ele deve ser implementado como um canal personalizado e ser conectado à camada do canal. Este último afeta apenas o comportamento local do serviço e, portanto, pode ser implementado estendendo vários pontos de extensibilidade do serviço. Nas próximas seções, cada uma dessas extensões é discutida.
Canal InstanceContext durável
A primeira coisa a olhar é uma extensão de camada de canal. O primeiro passo para escrever um canal personalizado é decidir a estrutura de comunicação do canal. Como um novo protocolo de fio está sendo introduzido, o canal deve funcionar com quase qualquer outro canal na pilha de canais. Por conseguinte, deve suportar todos os padrões de troca de mensagens. No entanto, a funcionalidade central do canal é a mesma, independentemente da sua estrutura de comunicação. Mais especificamente, do cliente ele deve escrever o ID de contexto para as mensagens e do serviço ele deve ler esse ID de contexto das mensagens e passá-lo para os níveis superiores. Por isso, é criada uma DurableInstanceContextChannelBase
classe que atua como a classe base abstrata para 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 e para mensagens.
class DurableInstanceContextChannelBase
{
//…
protected void ApplyContext(Message message)
{
//…
}
protected string ReadContextId(Message message)
{
//…
}
}
Esses dois métodos fazem uso de IContextManager
implementações para escrever e ler o ID de contexto para ou da mensagem. IContextManager
( é uma interface personalizada usada para definir o contrato para todos os gerentes de contexto.) O canal pode incluir o 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 ContextManagerBase
classe que contém a funcionalidade comum para todos os gerenciadores de contexto. O GetContextId
método nesta classe é usado para originar o ID de contexto do cliente. Quando um ID de contexto é originado pela primeira vez, esse método o 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 @).
Mais tarde, quando o ID de contexto for necessário para o mesmo ponto de extremidade remoto, ele verificará se existe um arquivo apropriado. Se isso acontecer, ele lê a ID de contexto e retorna. Caso contrário, ele retorna um ID de contexto recém-gerado e o salva 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 é configurável usando o elemento de ligação.
O mecanismo usado para transportar o ID de contexto é configurável. Ele pode ser gravado no cabeçalho do cookie HTTP ou em um cabeçalho SOAP personalizado. A abordagem de cabeçalho SOAP personalizado torna possível usar esse protocolo com protocolos não-HTTP (por exemplo, TCP ou pipes nomeados). Existem duas classes, a saber, MessageHeaderContextManager
e HttpCookieContextManager
, que implementam estas duas opções.
Ambos escrevem o ID de contexto para a mensagem apropriadamente. Por exemplo, a MessageHeaderContextManager
classe grava-o em um cabeçalho SOAP no WriteContext
método.
public override void WriteContext(Message message)
{
string contextId = this.GetContextId();
MessageHeader contextHeader =
MessageHeader.CreateHeader(DurableInstanceContextUtility.HeaderName,
DurableInstanceContextUtility.HeaderNamespace,
contextId,
true);
message.Headers.Add(contextHeader);
}
Ambos os ApplyContext
métodos e ReadContextId
na DurableInstanceContextChannelBase
classe invocam o IContextManager.ReadContext
e IContextManager.WriteContext
, respectivamente. No entanto, esses gerentes de DurableInstanceContextChannelBase
contexto não são criados diretamente pela classe. Em vez disso, ele usa a ContextManagerFactory
classe para fazer esse trabalho.
IContextManager contextManager =
ContextManagerFactory.CreateContextManager(contextType,
this.contextStoreLocation,
this.endpointAddress);
O ApplyContext
método é invocado pelos canais de envio. Ele injeta o ID de contexto para as mensagens enviadas. O ReadContextId
método é invocado pelos canais de receção. Esse método garante que a ID de contexto esteja disponível nas mensagens de entrada e a adiciona à Properties
coleção da Message
classe. Ele também lança um CommunicationException
em caso de falha na leitura do ID de contexto e, portanto, faz com que o canal seja abortado.
message.Properties.Add(DurableInstanceContextUtility.ContextIdProperty, contextId);
Antes de prosseguir, é importante entender o uso da Properties
coleção na Message
classe. Normalmente, essa Properties
coleção é usada ao passar dados dos níveis inferior para superior da camada do canal. Desta 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 o 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 Properties
coleção.
Agora com a DurableInstanceContextChannelBase
classe no lugar, todas as dez interfaces necessárias (IOutputChannel, IInputChannel, IOutputSessionChannel, IInputSessionChannel, IRequestChannel, IReplyChannel, IRequestSessionChannel, IReplySessionChannel, IDuplexChannel, IDuplexSessionChannel) devem ser implementadas. Eles se assemelham a todos os padrões de troca de mensagens disponíveis (datagrama, simplex, duplex e suas variantes de sessão). Cada uma dessas implementações herda a classe base descrita anteriormente e chama e apropriadamente ApplyContext
ReadContextId
. Por exemplo, DurableInstanceContextOutputChannel
- que implementa a interface IOutputChannel - chama o ApplyContext
método 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 IInputChannel
interface - chama o ReadContextId
método 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 para o canal abaixo deles na pilha de canais. No entanto, as variantes de sessão têm uma lógica básica para garantir que a ID de contexto seja enviada e seja lida somente para a primeira mensagem que faz com que a sessão seja criada.
if (isFirstMessage)
{
//…
this.ApplyContext(message);
isFirstMessage = false;
}
Essas implementações de canal são então adicionadas ao tempo de execução do canal WCF pela DurableInstanceContextBindingElement
classe e DurableInstanceContextBindingElementSection
classe apropriadamente. Consulte a documentação de exemplo do canal HttpCookieSession para obter mais detalhes sobre elementos de ligação e seções de elementos de ligação.
Extensões da camada do modelo de serviço
Agora que o ID de contexto percorreu a camada de canal, o comportamento de serviço pode ser implementado para personalizar a instanciação. Neste exemplo, um gerenciador de armazenamento é usado para carregar e salvar o estado de ou para o armazenamento persistente. Conforme explicado anteriormente, este exemplo fornece um gerenciador de armazenamento que usa o SQL Server 2005 como seu armazenamento de backup. No entanto, também é possível adicionar mecanismos de armazenamento personalizados a esta extensão. Para fazer isso, é declarada uma interface pública, que deve ser implementada por todos os gerentes de armazenamento.
public interface IStorageManager
{
object GetInstance(string contextId, Type type);
void SaveInstance(string contextId, object state);
}
A SqlServerStorageManager
classe contém a implementação padrão IStorageManager
. Em seu SaveInstance
método, o objeto dado é serializado usando o XmlSerializer e é salvo no banco de dados do 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, os GetInstance
dados serializados são lidos para um determinado ID de contexto e o objeto construído a partir dele é 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 StorageManagerFactory
classe, que abstrai os detalhes de criação do gerenciador de armazenamento. Essa classe tem um membro estático, GetStorageManager
, que cria uma instância de um determinado tipo de gerenciador de armazenamento. Se o parâmetro type for null
, esse método criará uma instância da classe padrão SqlServerStorageManager
e a retornará. Ele também valida o tipo dado para certificar-se de que implementa a IStorageManager
interface.
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 medidas necessárias para mudar o comportamento do serviço têm de ser tomadas.
Como primeiro passo desse processo, temos que salvar o ID de contexto, que veio através da camada de canal para o InstanceContext atual. InstanceContext é um componente de tempo de execução 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 para a instância de serviço. Isso é essencial porque na comunicação de sessão o ID de contexto é enviado apenas com a primeira mensagem.
O WCF permite estender seu componente de tempo de execução InstanceContext adicionando um novo estado e comportamento usando seu padrão de objeto extensível. O padrão de objeto extensível é usado no WCF para estender classes de tempo de execução existentes com nova funcionalidade ou para 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 recuperar IExtensions por seu tipo.
Portanto, uma classe InstanceContextExtension deve ser criada que implementa a interface IExtension e define o estado necessário para salvar a ID de contexto. Essa classe também fornece o estado para manter o gerenciador de armazenamento que está sendo usado. Uma vez que o novo estado é salvo, não deve 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, acessível somente usando 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 de 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 Properties
coleção da Message
classe e passada para o construtor da classe extension. 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 tempo de execução usando a interface IInstanceProvider. A nova InstanceProvider
classe é implementada para fazer esse trabalho. O tipo de serviço esperado do provedor de instância é aceito no construtor. Mais tarde, isso é usado para criar novas instâncias. GetInstance
Na implementação, uma instância de um gerenciador de armazenamento é criada procurando uma instância persistente. Se ele 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 InstanceContextExtension
classes , InstanceContextInitializer
e InstanceProvider
no tempo de execução do modelo de serviço. Um atributo personalizado pode ser usado para marcar as classes de implementação de serviço para instalar o comportamento. O DurableInstanceContextAttribute
contém a implementação para este atributo e implementa a IServiceBehavior
interface para estender todo o tempo de execução do serviço.
Essa classe tem uma propriedade que aceita o tipo do gerenciador de armazenamento a ser usado. Desta forma, a implementação permite que os usuários especifiquem sua própria IStorageManager
implementação como parâmetro desse atributo.
ApplyDispatchBehavior
Na implementação, o InstanceContextMode
atributo atual ServiceBehavior
está sendo verificado. Se essa propriedade estiver definida como Singleton, a habilitação da instanciação durável não será possível e um InvalidOperationException
será lançado 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, do inicializador de contexto da instância e do provedor de instância são criadas e instaladas DispatchRuntime
no 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 conexão personalizado para troca de ID de contexto personalizado 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. Como discutido anteriormente, já existe a funcionalidade necessária para salvar o estado em uma IStorageManager
implementação. Agora devemos integrar isso com o tempo de execução do WCF. Outro atributo é necessário que é 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 SaveStateAttribute
classe implementa essa funcionalidade. Ele também implementa a classe para modificar o tempo de IOperationBehavior
execução do WCF para cada operação. Quando um método é marcado com esse atributo, o tempo de execução do WCF invoca o ApplyBehavior
método enquanto o apropriado DispatchOperation
está sendo construído. Há uma única linha de código nesta implementação de método:
dispatch.Invoker = new OperationInvoker(dispatch.Invoker);
Esta instrução cria uma instância do OperationInvoker
tipo e a atribui à Invoker
propriedade do que está sendo DispatchOperation
construído. A OperationInvoker
classe é um wrapper para o invocador de operação padrão criado para o DispatchOperation
. Esta classe implementa a IOperationInvoker
interface. Na implementação do Invoke
método, a invocação do método real é delegada ao invocador de operação interna. No entanto, antes de retornar os resultados, o InstanceContext
gerenciador de armazenamento no é 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 da camada de canal e da camada de modelo de serviço são feitas e agora podem ser usadas em aplicativos WCF. Os serviços devem adicionar o canal à pilha de canais usando uma associação personalizada e, em seguida, 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 na pilha de canais usando uma associação personalizada. Para configurar o canal declarativamente no arquivo de configuração, a seção do elemento binding deve ser adicionada à coleção binding element extensions.
<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 binding pode ser usado com uma vinculação personalizada assim como outros elementos de vinculaçã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 melhorada permitindo que os usuários especifiquem a IStorageManager
implementação usando uma seção de configuração. Isso torna possível modificar o armazenamento 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 muda. Dessa forma, você pode evitar o uso do SaveState
atributo e executar o trabalho persistente com mais precisão (por exemplo, você pode persistir o estado quando o estado é realmente alterado em vez de salvá-lo cada vez que um método com o SaveState
atributo é chamado).
Quando você executa o exemplo, a saída a seguir é exibida. O cliente adiciona dois itens ao seu carrinho de compras e, em seguida, 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
Nota
A reconstrução do serviço substitui o arquivo de banco de dados. Para observar o estado preservado em várias execuções da amostra, certifique-se de não reconstruir a amostra entre as execuções.
Para configurar, compilar e executar o exemplo
Certifique-se de ter executado o procedimento de instalação única para os exemplos do Windows Communication Foundation.
Para criar a solução, siga as instruções em Criando os exemplos do Windows Communication Foundation.
Para executar o exemplo em uma configuração de máquina única ou cruzada, siga as instruções em Executando os exemplos do Windows Communication Foundation.
Nota
Você deve estar executando o SQL Server 2005 ou o SQL Express 2005 para executar este exemplo. Se você estiver executando o SQL Server 2005, deverá modificar a configuração da cadeia de conexão do serviço. Ao executar entre máquinas, o SQL Server só é necessário na máquina do servidor.