Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
O exemplo Durable demonstra como personalizar o runtime do Windows Communication Foundation (WCF) para habilitar contextos de instância duráveis. Ele usa o SQL Server 2005 como seu banco de dados de apoio (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 Durável de InstanceContext
A primeira coisa a considerar é uma extensão da camada do 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, o cliente deve escrever o ID de contexto nas mensagens e o serviço deve ler este ID de contexto das mensagens e passá-lo para as camadas 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)
{
//…
}
}
Estes dois métodos utilizam implementações de IContextManager para escrever e ler o ID de contexto nas mensagens ou a partir delas.
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 na mensagem apropriadamente. Por exemplo, a classe MessageHeaderContextManager grava-o num 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);
}
Ambos os métodos ApplyContext e ReadContextId na classe DurableInstanceContextChannelBase invocam 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 ApplyContext e ReadContextId apropriadamente. 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 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 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 classe DurableInstanceContextBindingElement e pela classe DurableInstanceContextBindingElementSection adequadamente. 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 é criada e, em seguida, acessível apenas através de propriedades de leitura exclusivamente.
// 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á a ser 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, o ID de contexto é lido da coleção Properties da classe Message e passado para o 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 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, InstanceContextInitializer e InstanceProvider classes no runtime 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.
Na implementação ApplyDispatchBehavior, o atributo atual ServiceBehavior do InstanceContextMode está a ser 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 gestor de armazenamento, do inicializador de contexto de instância e do fornecedor de instância são criadas e instaladas 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 suma, este exemplo produziu um canal que ativou o protocolo de fio personalizado para a troca de ID de contexto específico e também sobrescreve o comportamento padrão de instância 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 IOperationBehavior para modificar o runtime 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 tipo OperationInvoker e atribui-a à propriedade Invoker do DispatchOperation que está a ser 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 os resultados serem retornados, utiliza-se o gerenciador de armazenamento no InstanceContext 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)
{
//…
}
//…
}
As aplicações cliente devem adicionar o DurableInstanceContextChannel à pilha de canais usando uma associação personalizada. Para configurar o canal de forma declarativa no arquivo de configuração, a secção do elemento de ligação deve ser adicionada à coleção de extensões de elementos de ligaçã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 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 guardar o estado sempre que ele se altera. 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 configuração única dos 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.