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:
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)
...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: