Manteniendo el estado con WCF (1 de 2)

En muchos de los ejemplos que podemos ver sobre servicios web las llamadas a los diferentes métodos que proveen son independientes entre ellas. En la vida real es posible que necesitemos mantener el estado entre diferentes llamadas y que, además, éstas tengan que seguir una determinada secuencia.

El Framework 3.0 nos provee de formas muy fáciles para poder controlar estos casos, solo falta añadir unos atributos a los métodos de nuestro servico o al servicio en sí.

Para poder enseñar como hacerlo de una manera fácil de entender he creado un pequeño código de ejemplo.
He creado un servicio web que emula un carrito de compra; un ejemplo básico donde se debe mantener el estado entre diferentes llamadas. No he implementado ningún control de excepciones ya que así nos centramos en lo que toca.

Esta primera parte tratará de mantener el estado y concurrencia, en la otra entrada veremos como definir el orden de ejecución.

Por una parte tendremos la lista de productos; un xml, por otro el servicio web; con su contrato y la clase que lo implementa, una aplicación host; que sirve el servicio usando wsHttpBinding y, por último, el cliente; que consumirá el servicio.

Primero he creado un xml como contenedor de datos, que contiene una lista de productos. Lo he creado serializando un objeto de tipo List<Product>, así luego lo podremos deserializar y trabajar con él de una forma más fácil:



<?xml version=»1.0″ encoding=»utf-8″?>
<ArrayOfProduct xmlns:xsi=»http://www.w3.org/2001/XMLSchema-instance» xmlns:xsd=»http://www.w3.org/2001/XMLSchema»>
  <Product>
    <Id>0</Id>
    <Name>Agua</Name>
    <Price>0.5</Price>
  </Product>
  <Product>
    <Id>1</Id>
    <Name>Zumo de naranja</Name>
    <Price>1.2</Price>
  </Product>
  <Product>
    <Id>2</Id>
    <Name>Olivas</Name>
    <Price>2.35</Price>
  </Product>
  <Product>
    <Id>3</Id>
    <Name>Patatas</Name>
    <Price>1.75</Price>
  </Product>
  <Product>
    <Id>4</Id>
    <Name>Cerveza</Name>
    <Price>0.8</Price>
  </Product>
  <Product>
    <Id>5</Id>
    <Name>Refresco</Name>
    <Price>0.7</Price>
  </Product>
</ArrayOfProduct>

Luego he creado un proyecto tipo librería en Visual Basic y he creado las dos clases que voy a necesitar para tratar los datos: Product y CartItem.



Public Class Product
 
    Private _id As Integer
    Public Property Id() As Integer
        Get
            Return _id
        End Get
        Set(ByVal value As Integer)
            _id = value
        End Set
    End Property
 
    Private _name As String
    Public Property Name() As String
        Get
            Return _name
        End Get
        Set(ByVal value As String)
            _name = value
        End Set
    End Property
 
    Private _price As Decimal
    Public Property Price() As Decimal
        Get
            Return _price
        End Get
        Set(ByVal value As Decimal)
            _price = value
        End Set
    End Property
End Class
 
Public Class CartItem
    Private _product As Product
    Public Property Product() As Product
        Get
            Return _product
        End Get
        Set(ByVal value As Product)
            _product = value
        End Set
    End Property
 
    Private _cuantity As Integer
    Public Property Cuantity() As Integer
        Get
            Return _cuantity
        End Get
        Set(ByVal value As Integer)
            _cuantity = value
        End Set
    End Property
End Class

Después he definido el contrato: la Interface.



<ServiceContract()> _
Public Interface IShoppingCart
    <OperationContract()> _
    Function AddItem(ByVal itemId As Integer) As Boolean
    <OperationContract()> _
    Function RemoveItem(ByVal itemId As Integer) As Boolean
    <OperationContract()> _
    Function GetShopingCart() As String
    <OperationContract()> _
    Function Checkout() As Boolean
End Interface

Y la clase que la implementa: el servicio.



Public Class ShoppingCartSrv
    Implements IShoppingCart
 
    Private shoppingCart As New List(Of CartItem)
 
    Private Function getProduct(ByVal productId As Integer) As Product
        Dim products As New List(Of Product)
        Dim filename As String = «products.xml»
        If File.Exists(filename) Then
            Dim reader As New StreamReader(filename)
            Dim serializer As New XmlSerializer(GetType(List(Of Product)))
            products = CType(serializer.Deserialize(reader), List(Of Product))
            reader.Close()
        End If
 
        Dim product As Product = Nothing
        For Each p As Product In products
            If p.Id = productId Then
                Return p
            End If
        Next
        Return product
    End Function
 
    Public Function AddItem(ByVal itemId As Integer) As Boolean Implements IShoppingCart.AddItem
        For Each item As CartItem In shoppingCart
            If item.Product.Id = itemId Then
                item.Cuantity += 1
                Return True
            End If
        Next
        Dim product As Product = getProduct(itemId)
        If Not IsNothing(product) Then
            shoppingCart.Add(New CartItem With {.Product = product, .Cuantity = 1})
            Return True
        End If
        Return False
    End Function
 
    Public Function GetShopingCart() As String Implements IShoppingCart.GetShopingCart
        Dim list As String = «»
        Dim total As Double = 0
        For Each item As CartItem In shoppingCart
            list += String.Format(«Id: {0} Name: {1} Price: {2} Cuantity: {3}{4}», _
                                  item.Product.Id, item.Product.Name, item.Product.Price, item.Cuantity, Environment.NewLine)
            total += (item.Product.Price * item.Cuantity)
        Next
        If list.Equals(«») Then
            list = «Empty Cart»
        Else
            list += String.Format(«Total: {0} Euro», total)
        End If
        Return list
    End Function
 
    Public Function RemoveItem(ByVal itemId As Integer) As Boolean Implements IShoppingCart.RemoveItem
        Dim item As CartItem = Nothing
        For Each i As CartItem In shoppingCart
            If i.Product.Id = itemId Then
                item = i
            End If
        Next
        If Not IsNothing(item) Then
            shoppingCart.Remove(item)
            Return True
        End If
        Return False
    End Function
 
    Public Function Checkout() As Boolean Implements IShoppingCart.Checkout
        shoppingCart.Clear()
        Return True
    End Function
End Class

Ahora que ya tenemos creado el servicio, he creado una aplicación para que lo sirva. Creo un nuevo proyecto de consola, agrego la dos referencias que necesito: System.ServiceModel y al proyecto de librería que representa mi servicio. Y creo el código que necesito para servir el servicio:



Imports ShoppingCartService
Imports System.ServiceModel
 
Module Host
 
    Sub Main()
 
        Dim host As New ServiceHost(GetType(ShoppingCartSrv))
        host.Open()
        Console.WriteLine(«Service running»)
        Console.WriteLine(«Press ENTER to stop the service»)
        Console.ReadLine()
        Console.Write(«Closing…»)
        host.Close()
        Console.WriteLine(«Closed»)
 
    End Sub
 
End Module

Por último configuro cómo voy a servir el servicio con mi fichero de configuración app.config en la aplicación que hace de host:



<?xml version=»1.0″ encoding=»utf-8″ ?>
<configuration>
  <system.serviceModel>
    <services>
      <service name=»ShoppingCartService.ShoppingCartSrv» behaviorConfiguration=»MyBehavior»>
        <host>
          <baseAddresses>
            <add baseAddress=»http://localhost:9669/ShoppingCartService»/>
          </baseAddresses>
        </host>
        <endpoint address=»ShoppingCartSrv»
                  binding=»wsHttpBinding»
                  contract=»ShoppingCartService.IShoppingCart»
                  name=»WsHttp_ShoppingCartEndpoint»/>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name=»MyBehavior»>
          <serviceMetadata httpGetEnabled=»true»/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

Ahora solo falta el cliente.
Creo otra aplicación de consola para el cliente. Ejecuto la aplicación de Host para que se empiece a servir el servicio y poder, así, añadir la referencia al servicio al cliente con la ayuda de Visual Studio:

reference


Escribimos el código que consumirá el servicio: añadirá productos a la cesta y mostrará ésta por pantalla:



Imports Client.ShoppingCartService
 
Module Client
 
    Sub Main()
 
        Console.WriteLine(«Press ENTER when the service has started»)
        Console.ReadLine()
        Try
            Dim proxy As New ShoppingCartClient(«WsHttp_ShoppingCartEndpoint»)
            Dim rand As New Random()
            Dim result As Boolean = True
            result = proxy.AddItem(rand.Next(0, 5))
            result = proxy.AddItem(rand.Next(0, 5))
            result = proxy.AddItem(rand.Next(0, 5))
            result = proxy.AddItem(rand.Next(0, 5))
            Console.WriteLine(proxy.GetShopingCart())
            ‘result = proxy.Checkout()
            ‘Console.WriteLine(proxy.GetShopingCart())
        Catch ex As Exception
            Console.WriteLine(ex.Message)
        End Try
        Console.WriteLine(«Press ENTER to finish»)
        Console.ReadLine()
 
    End Sub
 
End Module

Ahora ya tenemos preparado el entorno de nuestra aplicación: nuestro cliente y nuestro servicio. Ejecutémoslo a ver que le pasa al carrito, ¿se mantendrá el estado entre diferentes llamadas al servicio?, o sea: ¿al mostrar los elementos de la cesta estarán los añadidos anteriormente?…
(no os olvidéis de que tiene que estar ejecutándose la aplicación que hace de host del servicio antes de ejecutar el cliente)

first


…pues sí!.

Vamos a ver el por qué.

Si nos paramos a pensar qué debe estar pasando, el servicio crea una variable privada que es creada el crearse la instancia del servicio. Si dos clientes acceden al servicio el Framework crea una instancia del servicio para cada uno de ellos, así que cada uno de ellos va a tener un carrito de compra propio. La instancia del servicio se destruirá en el momento que el cliente cierre el proxy. Bueno,… cuando el Garbage Collector quiera. Pues ya está ¿no?, ya mantenemos el estado. Pero, ¿y si ahora tenemos 10.000 clientes?, tendremos 10.000 instancias del servicio corriendo: el cliente puede estarse media hora pensando qué comprar. No creo que pueda buscar muchos artículos en una máquina sin memoria.

¿Cómo podemos cambiar esto?, cambiando el ContextMode de nuestro servicio. Usando la propiedad InstanceContextMode del atributo ServiceBehavior de nuestro servicio:



<ServiceBehavior(InstanceContextMode:=InstanceContextMode.PerSession)> _
Public Class ShoppingCartSrv
    Implements IShoppingCart
‘…

o en C#



[ServiceBehavior(InstanceContextMode=InstanceContextMode.PerSession)]
public class ShoppingCartServiceImpl : IShoppingCartService
{
    //…

Esta propiedad puede tomar diferentes valores: InstanceContextMode.PerSession, InstanceContextMode.PerCall y InstanceContextMode.Single.

InstanceContextMode.PerSession
Es el que tiene por defecto. Mantiene una instancia por cliente y la destruye al desconectarse.

InstanceContextMode.PerCall
Crea una instancia solo para servir la petición de uno de sus métodos y la destruye una vez ejecutado.

InstanceContextMode.Single
Crea una instancia la primera vez que se llama al servicio y usa la misma para todos los clientes.

Ahora ya sabemos por qué se mantenía el estado. Pero al ver esto, se nos pueden plantear preguntas respecto a la concurrencia. Si un cliente crea dos «threads» que hagan diferentes llamadas a su servicio, ¿qué hará el servicio?, si le llega una llamada a un método que está ejecutando mantendrá a la llamada «en espera» y, posiblemente el cliente termine por time-out.

Para poder indicarle al servicio como tratar peticiones concurrenciales  tenemos otra propiedad del atributo ServiceBehavior llamada ConcurrencyMode. Y puede tomar dos valores ConcurrencyMode.Single (por defecto el servicio usa esta) o ConcurrencyMode.Multiple. Con la segunda opción el servicio será capaz de aplicar concurrencia a las peticiones del cliente.

Podemos probar ahora de cambiar la propiedad InstanceContextMode a nuestro servicio y ver como el resultado del cliente va a cambiar. Si aplicamos InstanceContextMode.PerCall veremos que al imprimir el carrito este sale vacío. En cambio, si usamos InstanceContextMode.Single veremos que aunque cerremos el proxy y abrimos otro, o ejecutamos dos o más clientes, éstos comparten el carrito y entre todos van añadiendo elementos.

Con los diferentes valores de InstanceContextMode podemos controlar el tiempo de vida de nuestro servicio. Esta propiedad es global a todos los servicios y es el Framework el que se encarga de crear o destruir el servicio, pero no tiene en cuenta qué método esta ejecutando el cliente.
Podemos controlar cuando se destruye la instancia con una la propiedad ReleaseInstanceMode del atributo OperationContract de nuestros métodos definidos en el contrato.

Esta propiedad puede tomar cuatro valores:

ReleaseInstanceMode.AfterCall
Una vez el cliente acaba de utilizar el método, el runtime de WCF libera los recursos del servicio para que lo destruya el Garbage Collector. Si el cliente invoca otro método el runtime creará una instancia nueva del servicio.

ReleaseInstanceMode.BeforeCall
Si existe alguna instancia del servicio cuando el cliente invoca el método el runtime, primero destruye esa instancia y luego crea una nueva para servir la petición.

ReleaseInstanceMode.BeforeAndAfterCall
La combinación de las dos anteriores.

ReleaseInstanceMode.None
Este es el valor por defecto de esta propiedad. La instancia del servicio es creada y destruida de acuerdo con el valor de la propiedad InstanceContextMode del atributo ServiceBehavior del servicio.

La idea es jugar con el código del cliente y las propiedades del servicio, y ver como reacciona a los diferentes valores que pueden tomar las propiedades.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *