Migrar de Servicios Web a WCF

Imaginemos que tenemos desarrollados una serie de servicios web que estan siendo consumidos por una gran diversidad de clientes y quiero realizar la migración de estos servicios a WCF, como puedo hacerlo sin que afecten a esos clientes.

La manera es modficar nuestro servicio web decorandolo con los atributos de WCF pero manteniendo los atributos de Web Services. 

using System; 

using System.Web; 

using System.Collections; 

using System.Web.Services; 

using System.Web.Services.Protocols; 

   

using System.ServiceModel; 

   

[WebService(Namespace = "http://tempuri.org/")] 

[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] 

[ServiceContract(Namespace="http://tempuri.org")] 

public class WebService : System.Web.Services.WebService 

{    

   

    [WebMethod] 

    [OperationContract] 

    public string Hola(string nombre) 

    { 

        return string.Format("Hola, {0}",nombre); 

    } 

} 

En nuestro fichero de configuracion deberemos introducir la sección de WCF. 

 
  <system.serviceModel>

    <services>

      <service name="WebService"  behaviorConfiguration="returnFaults">

        <endpoint binding="basicHttpBinding" contract="WebService"/>

        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>

      </service>

    </services>

    <behaviors>

      <serviceBehaviors>

        <behavior name="returnFaults">

          <serviceDebug includeExceptionDetailInFaults="true"/>

          <serviceMetadata httpGetEnabled="true"/>

        </behavior>

      </serviceBehaviors>

    </behaviors>

  </system.serviceModel>

</configuration>

Ahora debemos remapear el BuilProvider de asmx para que use System.ServiceModel. 

<system.web>

  <compilation debug="true">

    <buildProviders>

      <remove extension=".asmx"/>

      <add extension=".asmx" type="System.ServiceModel.Activation.ServiceBuildProvider,
            System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />

    </buildProviders>

  </compilation>

</system.web>

 Y ya lo tenemos funcionando con WCF

WCF Manejando Generics en el Cliente

Una de las cosas que mas me molestaban, era definir mis operaciones en el servicio utilizando Generics y ver que en el cliente la generación de la clase me transformaba esos Generics en arrays asi que empece a curiosear a ver si podia utilizar Generics en el cliente. 

Una de las cosas nuevas de WCF es la posibilidad de no tener que definir un proxy a partir de un wsdl, de manera que si estoy haciendo una aplicación distribuida mia y solo mia, en la cual yo defino la  interfaz del servicio y nadie mas tiene que conocerla porque solo voy a acceder yo,  puedo llegar a compartir esta interfaz en ambos lados (servicio y cliente) haciendo que los proyectos, y las comunicaciones, queden más limpios y no haya que refrescar los proxys. Al ser la misma Interfaz la que manejo en el cliente vere Generics en vez de arrays en aquellas OperationContract que los utilizen.

Para ello voy a utilizar la clase ChannelFactory, esta clase me permite generar proxys en el cliente a partir de la interfaz.

 

using (ChannelFactory<IEchoServiceChannel> factory = 
new ChannelFactory<IEchoServiceChannel>(new NetTcpBinding()))
 {
  using (IEchoServiceChannel proxy = 
           factory.CreateChannel(new EndpointAddress(@"net.tcp://localhost:8080/Echo")))
    {
            proxy.Echo("Hola!");
    }                    
 }

En este caso no nos hace falta el fichero de configuración ya que estamos creando el proxy manualmente.

Pero esta solución no me gusta porque se pierde toda la facilidad que teniamos en los servicios de WCF de cambiar sus caracterisicas sin tener que recompilar nada de codigo ya que no tenemos fichero de configuración.

Para solucionar esto tenemos que tirar de la herramienta svcutil con estos parametros:

svcutil /o:holaproxy.cs /ct:System.Collections.Generic.List`1 http://localhost:8000/Hola/

Y nos creara la clase cliente con los tipos genericos al igual que habiamos definido en el contrato del servicio.

Excepciones en WCF. Heredar de Exception

Este post surgio ayer durante la charla que dimos Miguel Jimenez y yo sobre WCF para el grupo de usuarios Artalde.Net en la universidad de Deusto.

Rodrigo nos pregunto si a la hora de implementar una excepcion, la clase que implementamos para introducir en el FaultContract no podia heredar de la clase Exception. La respuesta es la siguiente. Si tenemos el siguiente ejemplo.

 [DataContract]
    public class HolaException:Exception
    {
        private string _descripcion;
        [DataMember]
        public string Descripcion
        {
            get { return _descripcion; }
            set { _descripcion = value; }
        }
	
    }

Se produce el siguiente error

El tipo ‘ExcepcionService.HolaException’ no puede ser ISerializable y tener el atributo DataContractAttribute

Para que funcione debemos de poner el atributo Serializable en nuestra Excepcion y en este caso ya funciona perfectamente el servicio.

Pero hay un problema, si utilizamos en el cliente la opcion Add Service Reference, esta no funciona y no nos genera ni la clase, ni el fichero de configuración. Debemos de realizar esta generación a partir de la herramienta svcutil, que funciona perfectamente

Si implementamos el cliente, nos tenemos que dar cuenta, que las propiedades de  la clase HolaException, no aparecen debido a que hemos marcado la clase como serializable y  no marcamos las propiedades con el atributo DataMember , para que el cliente pudiera acceder a esas propiedades deberiamos de implementar la serialización de la clase  nosotros mismos e incluir las propiedades en la serialización.

Manejando excepciones en WCF

Una de las cosas con las que me he encontrado mas agradables en WCF es el manejo de excepciones. Alguien recuerda como las teniamos que hacer para enviar una excepcion a traves de un servicio Web?? Bastante lioso o al menos para mi.

En cambio en WCF es bastante sencillo realizarlo, lo primero que vamso a ver es returnUnknownExceptionsAsFaults.

WCF nos ofrece una opción para lanzar automaticamente cualquier exceeción que se produzca en nuestro sercicio como una Soap Fault. Para habilitarlo debemos de introducir returnUnknownExceptionsAsFaults = ‘True’ en el behaivor del servicio en el fichero de configuración

<behaviors> 
<behavior name="OrderingServiceBehavior" 
returnUnknownExceptionsAsFaults="True"> 
</behavior> 
</behaviors>

De manera que cuando se produzca cualquier excepción nos mandara una UnknownFaultException. Realmente lo que nos enviara sera toda la información del error producido que esta muy bien cuando desarrollamos pero por favor en producción nunca,nunca nunca habiliteis esta opción porque en cuanto se produzca una excepción en vuestro servicio el cliente puede recibir informacion que nosotros no desaemos que tenga.

FaultContract

Nos va a permitir crear nuestras propias clases de excepciones con la información que nosotros queremos transmitir al cliente. Vamos a ver como la utilizamos en los siguientes pasos.

Primero creamos nuestra clase de excepción como si fuese un DataContract de nuestro servicio

 [DataContract]
    public class SomeError
    {
        [DataMember]
        public string Content;
    }

Lo siguiente es en el Interfaz del servicio en el metodo que queremos controlar la excepción, debemos introducir el atributo Faultcontract

 

[OperationContract(Name = "Error")]
[FaultContract(typeof(SomeError))]
decimal DividebyZero(decimal input);

Al introducir este atributo en esta operación, se esta informando a los clientes que el servicio puede retornar un mensaje de error que esta definido en el DataContract SomeError.

 Implementamos el metodo de la interfaz

 public decimal DividebyZero(decimal input)
        {
            try
            {
                decimal denominator=0;
                return input / denominator;

            }
            catch (Exception exception)
            {

                SomeError error = new SomeError();
                error.Content = exception.Message;
                throw new FaultException<SomeError>(error);
            }
        }

Vemos que el codigo introducido es el mismo que si no estuviesemos utilizando Indigo excepto que en el control de la excepción estoy relanzando una FaultException del tipo SomeError.

El cliente deberia tratarlo de la siguiente manera

try
    {
       proxy.DividebyZero(0);
    }
 catch (FaultException<SomeError> exception)
   {

        Console.WriteLine("Error : {0}", exception.Detail.Content);
    }

 

 Facil de gestionar !!!

Trazando un servicio WCF

Muchas veces nos preguntabamos como realizar logs en nuestras aplicaciones, en mi opinión es una tarea que debemos de realizar o preveer siempre que desarrollamos un proyecto por los beneficios que nos aporta los cuales ya explicamos resumidamente aqui.

WCF nos ofrece una manera muy sencilla de poder introducir logs en nuestro servicio y no debe de haber excusa para no utilizarlo cuando desarrollemos.

Os imaginareis como se especifica un log en un servicio WCF???. pues como siempre a traves del fichero de configuracion. Tan sencillo como añadir en nuestro app.config o web.config la entrada

<system.diagnostics>
	<sources>
		<source 
		name="System.ServiceModel.MessageLogging" 
		switchValue="Verbose">
		<listeners>
			<add 
			name="xml" 
			type="System.Diagnostics.XmlWriterTraceListener" 
			initializeData="c:logsmessage.log" />
		</listeners>
		</source>
	</sources>
		<trace autoflush="true" />
	</system.diagnostics>
	<system.serviceModel>
	<diagnostics>
	<messageLogging logEntireMessage="true"
			maxMessagesToLog="300"
			logMessagesAtServiceLevel="false"
			logMalformedMessages="true"
			logMessagesAtTransportLevel="true" />
</diagnostics>

 

Estamos indicando con esta entrada en el fichero de configuración que se realizen logs y se guarden en el fichero c.logsmessage.log.

Si ejecutamos ahora el servicio, podemos observar que nos genera el fichero log, pero si lo abrimos lo unico que vamos a ver es una ristra de datos que nos echan para atras

 

Con lo que el log generado no me serviria de nada, ya que me costaria mas descifrar lo que me dice que el beneficio que me iba a dar. Pero de solucionar esto se han encargado los chicos del equipo de WCF y han realizado la herramienta Service Trace Viewer la cual nos ofrece la visión del log de una manera clara y sencilla y para uestra un boton. Abrimos el mismo archivo que teniamos antes con Service Trace Viewer y obtenemos

Donde podemos ver las cabeceras SOAP que se pasan, el mensaje (en este caso vemos que no esta encriptado porque utilizo basicHttpBinding)…

En definitiva una gran herramienta para nuestros desarrollos 

 

Technorati tags:

Application Block de Loggin

 


 


A mi me encanta Enterprise Library, creo que es una de las librerias de Microsoft a las que se puede sacar jugo de una manera muy sencilla. En este post voy a intentar explicar como realizar logging en nuestras aplicaciones apoyandonos en Enterprise Library 2.0

¿A que necesidades nos solemos enfrentar cuando desarrollamos una aplicación y queremos implementar un sistema de Logging?. Respondiendo a esta pregunta tendríamos las respuestas:


  • Poder generar eventos de log en diferentes repositorios(bases de datos, log de eventos de Windows, archivos de texto), sin modificar código
  • Dar soporte a “Debugging” en producción
  • Permitir cambiar las reglas del logging sin tener que cambiar código
  • Permitir incluir información de contexto dentro de los mensajes de log
  • Por ultimo contar con un marco de trabajo organizado para toda la organización

Vamos a definir unos cuantos conceptos básicos:


  • Log Entry: Representa un evento que se desea almacenar en Bitácora. Un Log Entry tiene propiedades como el mensaje, si es información o error y también tiene un conjunto de propiedad extendidas al cual se le puede agregar información propia de la aplicación como un IdCliente
  • Trace Listener: Componentes que se encargan de persistir los eventos en cada uno de los destinos (Ej:EventLogTraceListener almacena eventos en el EventLog de Windows)
  • Trace: Representa una actividad a monitorizar, en un sector de código digo que quiero iniciar el trace y todo lo que haga mientras no cierre el trace aunque baje en el código (llamadas a funciones) va a mantener un contexto con un id del trace

Generar Un Log de Eventos


Primero debemos agregar la referencia a EnterPrise Library


Agremamos el Imports o using en el código

Introducimos nuestra primer código de logging

            ‘Declaramos una instancia de LogEntry
Dim entry As New LogEntry
‘Le asociamos un menaje a la entrada
entry.Message = “Se calculo el valor de pi
‘Indicamos la severidad de la entrada, es decir,
el tipo de entrada en este caso es una información
entry.Severity = TraceEventType.Information
‘Le indicamos la categoria nos sirve para ordenar
como queremos que se persistan estos
elementos en bitacora
entry.Categories.Add(Category.General)
‘Pr ultimo escribimos la entrada
Logger.Write(entry)

 


En este código no le hemos dicho donde lo tiene que escribir esa entrada que hemos creado, eso lo vamos a indicar en configuración, para ello debemos abrir el app.config con EntLibConfig.exe que es el configurador de Enterprise, como hablamos en el bloque de acceso a datos.


Una vez abierto vamos a introducir la entrada para configurar el Looging


Vemos que genera una serie de secciones



  • Formatters: Indica como se formatean los mensajes que envio al EventLog, por defecto tengo un formateador de texto que tiene un template que sigue el estandar que se muestra


Nos muestra el mensaje, categoría y nos permite insertar Tokens, pero también nos permite crear nuestros formateadores, por ejemplo podríamos crear un formateador que generase archivos XML


  • Trace Listeners: Es el encargado de persistir la información en los repositorios, por defecto tenemos el de Event Log, pero también esta el de Base de Datos, e-mail, MSMQ, WMI…

Nota:

En Trace Listener en el Event Log podemos indicar en el campo Source la aplicación que estamos loggeado y en MachineName un nombre de maquina a la cual tengamos permisos y se grabarán centralizados todo el Logging


  • Special Sources: Hay un source para todos los eventos, si el Application Block recibe un evento que no esta asociado a ninguna categoría el va a entrar en este nodo. Dentro de este nodo tenemos



    • Logging Errors & Warnings: Cuando se intenta grabar un evento y se da un error, por ejemplo intento grabar el Log en una base de datos y esta caída, lo que va ha hacer el Application Block de Logging es generar un Log de ese error y va a utilizar el Trace Listeners, obviamente si estamos intentando logear en una base de datos y esta caida, no hay que utilizar un Trace Listener de EventLog en vez de la base de datos y eso lo indicamos en FormatedEventLog Trace Listener que viene debajo de Logging Errors & Warnings, por defecto viene el Event Log


  • Category Sources: Se puede definir n categorias y para cada categoría se puede poner uno o mas listeners



Una vez configurada la aplicación, si yo la ejecuto y voy al visor de sucesos, veo que se ha grabado la entrada que había programado



Si quisieramos agregar propiedades extendidas a esta entrada, como por ejemplo el valor calculado de pi.

‘Declaramos una instancia de LogEntry
Dim entry As New LogEntry
‘Le asociamos un menaje a la entrada
entry.Message = “Se calculo el valor de pi
‘Indicamos la severidad de la entrada, es decir,
el tipo de entrada en este caso es una información
entry.Severity = TraceEventType.Information
‘Le indicamos la categoria nos sirve para ordenar
como queremos que se persistan estos elementos en bitacora
entry.Categories.Add(Category.General)
‘Creo una propiedad Extendida, para visualizar
el valor de Pi en el logging
entry.ExtendedProperties.Add(“Valor de Pi:“, args.Pi)
‘Pr ultimo escribimos la entrada
Logger.Write(entry)

Si yo ejecuto otra vez la aplicación, vemos que se genera una nueva entrada en el visor de sucesos con la propiedad extendida que hemos creado


De esta forma a un LogEntry le podemos incluir información de nuestra lógica de negocio, esta información esta lejos de poder guardarse en una base de datos porque esta tratada con el formateador de texto, deberíamos crear un formateador de base de datos para poder guardar esta información en una tabla con sus correspondientes columnas.

Trace


Si queremos definir una actividad (trace) y queremos guardar información como cuando se inicia, termina, información particular de los métodos que componen esa actividad.

Para declara un trace utilizaremos la sentencia using.

Using New Tracer(Category.Trace)

End Using

Todo el código y métodos que sean llamados en el ámbito del using, utilizaran el mismo identificador de Trace.

Vamos a ver como configuraria el Trace para que lo guarde en un archivo de Texto, generaria una nueva entrada en el Trace Listener de tipo Flat


Donde le pudo indicar el nombre del archivo donde quiero que lo guarde…

En la Category Sources añadimos la categoría de Trace y en la category Trace en el nodo SourceLevels le indicamos que solo vamos a mostrar información de Trace.


En la categoría le indicamos cual es el Trace Listener, en este caso el Flat Listeners


Ahora en el código cuando calculamos Pi, introducimos el trace

Public Function Calculate(ByVal digits As Integer) As String
Dim pi As New StringBuilder(“3“, digits + 2)
Dim result As String = Nothing

Try
If digits > 0 Then

Using New Tracer(Category.Trace)

pi.Append(“.“)
Dim i As Integer = 0
While (i < digits)
Dim args As CalculatingEventArgs
args = New CalculatingEventArgs(pi.ToString(), i + 1)
OnCalculating(args)

‘ Break out if cancelled
If args.Cancel = True Then
Exit While
End If

‘ Calculate next 9 digits
Dim nineDigits As Integer = NineDigitsOfPi.StartingAt((i + 1))
Dim digitCount As Integer = Math.Min(digits – i, 9)
Dim ds As String = String.Format(“{0:D9}“, nineDigits)
pi.Append(ds.Substring(0, digitCount))

i += 9
End While
End Using
End If

result = pi.ToString()

‘ Tell the world I’ve finished!
OnCalculated(New CalculatedEventArgs(result))
Catch ex As Exception
‘ Tell the world I’ve crashed!
OnCalculatorException(New CalculatorExceptionEventArgs(ex))
End Try

Return result
End Function


Si lo ejecutamos vemos que nos crea el archivo trace.log que habiamos indicado en el Trace listener


Si lo abrimos vemos el siguiente texto

—————————————-

Trace Start: 1 : Timestamp: 13/06/2006 9:02:44

Message: Start Trace: Activity ‘27465ceb-89f1-4d25-b13d-b368ed8c0257’ in method ‘Calculate’ at 213569458568 ticks

Category: Trace

Priority: 5

EventId: 1

Severity: Start

Title:TracerEnter

Machine: NOMBRE-FDEED211

App Domain: EnoughPI.vshost.exe

ProcessId: 2568

Process Name: C:Archivos de programaMicrosoft Enterprise Library January 2006labsvbLoggingexercisesex01beginEnoughPIbinEnoughPI.vshost.exe

Thread Name:

Win32 ThreadId:3684

Extended Properties:

—————————————-

—————————————-

Trace Stop: 1 : Timestamp: 13/06/2006 9:02:44

Message: End Trace: Activity ‘27465ceb-89f1-4d25-b13d-b368ed8c0257’ in method ‘Calculate’ at 213570572775 ticks (elapsed time: 0,311 seconds)

Category: Trace

Priority: 5

EventId: 1

Severity: Stop

Title:TracerExit

Machine: NOMBRE-FDEED211

App Domain: EnoughPI.vshost.exe

ProcessId: 2568

Process Name: C:Archivos de programaMicrosoft Enterprise Library January 2006labsvbLoggingexercisesex01beginEnoughPIbinEnoughPI.vshost.exe

Thread Name:

Win32 ThreadId:3684

Extended Properties:

—————————————-

Vemos la hora de comienzo y fin, el tiempo que ha tardado (0,311 seconds), además podemos ver que por cada actividad nos genera un identificador de esa actividad, esto nos será útil para mantener la correlación entre diferentes eventos. Por ejemplo vamos a crear un método Dummy y una llamada a este metodo dentro del using y en este metodo realizamos una entrada en el logging con el id de la actividad

Private Sub Dummy()

Logger.Write(string.Format(“El Id {0} es:”, trace.CorrelationManager.ActivityId)

End Sub

Si ejecutamos ahora el código vemos que me genera una nueva entrada en el fichero de trace con el Id de la actividad: 04ae6dcb-d803-4c11-90c3-e849608b44cc y si vamos al visor de sucesos vemos que tengo una entrada con esta información


Si quisieramos que los eventos se generasen en el fichero de trace debemos de ir a configuración y en la categoría de Trace indicarle en SourceLevels que quiero guardar todo


Si lo ejecutamos de nuevo la aplicación y vemos el fichero de trace tenemos la entrada que he introducido, con lo que podemos generar un contexto que genere muchos eventos ligados a una actividad y poder ver el orden de secuencia.

Trace Start: 1 : Timestamp: 13/06/2006 10:05:22

Message: Start Trace: Activity ‘9bfe5aaa-9ac4-411c-a982-24998aeaf5d0’ in method ‘Calculate’ at 227020326631 ticks

Category: Trace

Priority: 5

EventId: 1

Severity: Start

Title:TracerEnter

Machine: NOMBRE-FDEED211

App Domain: EnoughPI.vshost.exe

ProcessId: 1808

Process Name: C:Archivos de programaMicrosoft Enterprise Library January 2006labsvbLoggingexercisesex01beginEnoughPIbinEnoughPI.vshost.exe

Thread Name:

Win32 ThreadId:5180

Extended Properties:

—————————————-

—————————————-

Trace Information: 1 : Timestamp: 13/06/2006 10:05:22

Message: El Id 9bfe5aaa-9ac4-411c-a982-24998aeaf5d0 es:

Category: General, Trace

Priority: -1

EventId: 1

Severity: Information

Title:

Machine: NOMBRE-FDEED211

App Domain: EnoughPI.vshost.exe

ProcessId: 1808

Process Name: C:Archivos de programaMicrosoft Enterprise Library January 2006labsvbLoggingexercisesex01beginEnoughPIbinEnoughPI.vshost.exe

Thread Name:

Win32 ThreadId:5180

Extended Properties:

—————————————-

—————————————-

Trace Stop: 1 : Timestamp: 13/06/2006 10:05:22

Message: End Trace: Activity ‘9bfe5aaa-9ac4-411c-a982-24998aeaf5d0’ in method ‘Calculate’ at 227021301350 ticks (elapsed time: 0,272 seconds)

Category: Trace

Priority: 5

EventId: 1

Severity: Stop

Title:TracerExit

Machine: NOMBRE-FDEED211

App Domain: EnoughPI.vshost.exe

ProcessId: 1808

Process Name: C:Archivos de programaMicrosoft Enterprise Library January 2006labsvbLoggingexercisesex01beginEnoughPIbinEnoughPI.vshost.exe

Thread Name:

Win32 ThreadId:5180

Extended Properties:

Configurar Login Para Realizar el Logging en Base de Datos


Para ello recordamos que debemos crear un Trace Listener que se configure para la base de datos


Tendré que configurar la Base de Datos


Automáticamente me va a crear una entrada en DataAccessApplicationBlock, y tendremos que configurar el Connection String, para luego en la entrada DataBaseInstance asignarl el ConnectionString


Luego hay que decir como se llama el procedimiento almacenado que queremos que ejecute para añadir entradas a la base de datos, por defecto se llama WriteLog y estan los scripts de este procedimiento almacenados y de las tablas necesarios junto con EnterPrise Librery en el fichero LoggingDatabase.sql si yo quisiera por ejemplo añadir una columna a esa tabla deberiamos de utilizar un formateador diferente que nos permita pasar esa información a esa columna.

Vamos a ver como generamos un nuevo formateador, pero en este caso sobre ficheros XML

Lo primero es realizar las importaciones

using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;
using Microsoft.Practices.EnterpriseLibrary.Logging;
using Microsoft.Practices.EnterpriseLibrary.Logging.Configuration;
using Microsoft.Practices.EnterpriseLibrary.Logging.Formatters;

Debemos de crear una clase que derive de LogFormatter y deberemos sobrescribir el metodo Format(LogEntry log) que devuelve un string


Por ejemplo la clase de XML seria


[ConfigurationElementType(typeof(CustomFormatterData))]
public class XmlFormatter : LogFormatter
{
    private NameValueCollection Attributes = null;

    public XmlFormatter(NameValueCollection attributes)
    {
        this.Attributes = attributes;
    }

    public override string Format(LogEntry log)
    {
        string prefix = this.Attributes[“prefix”];
        string ns = this.Attributes[“namespace”];

        using (StringWriter s = new StringWriter())
        {
            XmlTextWriter w = new XmlTextWriter(s);
            w.Formatting = Formatting.Indented;
            w.Indentation = 2;

            w.WriteStartDocument(true);
            w.WriteStartElement(prefix, “logEntry”, ns);
            w.WriteAttributeString(“Priority”, ns,
                log.Priority.ToString(CultureInfo.InvariantCulture));
            w.WriteElementString(“Timestamp”, ns, log.TimeStampString);
            w.WriteElementString(“Message”, ns, log.Message);
            w.WriteElementString(“EventId”, ns,
                log.EventId.ToString(CultureInfo.InvariantCulture));
            w.WriteElementString(“Severity”, ns, log.Severity.ToString());
            w.WriteElementString(“Title”, ns, log.Title);
            w.WriteElementString(“Machine”, ns, log.MachineName);
            w.WriteElementString(“AppDomain”, ns, log.AppDomainName);
            w.WriteElementString(“ProcessId”, ns, log.ProcessId);
            w.WriteElementString(“ProcessName”, ns, log.ProcessName);
            w.WriteElementString(“Win32ThreadId”, ns, log.Win32ThreadId);
            w.WriteElementString(“ThreadName”, ns, log.ManagedThreadName);
            w.WriteEndElement();
            w.WriteEndDocument();

return s.ToString();
        }
    }
}

Una vez compilada nuestra clase, añadimos nuestro formateador en la configuración


Seleccionamos la propiedad Type y pulsamos el botón asociado, mostrando



Seleccionamos nuestro formateador y seleccionamos Atributtes y pulsamos el Botón


Añadimos los pares de clave/valor



    • Key = prefix, Value = x, and
    • Key = namespace, Value = EnoughPI/2.0


Ahora deberemos elegir en Trace Listeners nuestro formateador


Si lo ejecutamos vemos que nos muestra


Despliegue en Producción



Un aspecto importante es que a la hora de desplegar en servidores de producción si no tengo creado en el Visor de Sucesos el Source que he designado para la aplicación


Mocks

Despues de meterme en el mundo de las pruebas unitarias, me llego el momento de introducirme en los mocks. La primera vez que lo oi, ni sabia lo que era y creo que es una de las herramientas mas interesantes para la realización de pruebas unitarias.

Espero que quede claro con este post lo que es una pequeña introducción a este mundo. Dar las gracias a Javi Larrea desde aqui.

Introducción

Llamamos “mock” a un objeto falso que simula el comportamiento de un objeto verdadero.

A priori podemos pensar que utilizar un objeto falso no tiene sentido así que la primera pregunta que nos surge es: “¿Cuándo necesitamos usar objetos falsos?”.

Bien, supongamos que estamos desarrollando un código que forma parte de una aplicación más amplia. Supongamos también que nuestro código necesita utilizar objetos que están siendo desarrollados por otra persona, o mejor, que se van a desarrollar en otro momento posterior. Cuando terminemos de codificar nuestro código deberemos de ejecutar las pruebas necesarias que permitan verificar que es correcto y que hace lo que tiene que hacer, pero ¿como vamos a poder probarlo si nuestro código necesita utilizar objetos que todavía no están disponibles?.

Es en este momento donde tiene sentido el uso de objetos “mock”. Utilizaremos estos objetos en los proyectos de pruebas de manera que podamos probar nuestro código simulando el comportamiento de objetos que todavía no están disponibles a través del uso de los mocks.

Un mock deberá implementar el mismo interfaz del objeto que queremos simular. En la clase que define la prueba deberemos definir qué métodos del objeto real queremos que simule el mock, indicando para cada uno de ellos cual es la respuesta esperada cuando reciba unos parámetros predeterminados. Esa respuesta debe ser la misma que esperamos que devuelva el objeto real cuando esté disponible.

Al ejecutar la prueba, cuando el código que queremos probar llame a un objeto que todavía no esté disponible y que hemos simulado con el mock, esa llamada será “interceptada” por el objeto “mock” que hayamos definido y este devolverá la respuesta que hayamos definido y que se deberá corresponder con la que esperamos que sea devuelta por el objeto real cuando esté construido.

La utilización de mocks nos permite independizar el desarrollo de unas partes de la solución de otras. Podemos desarrollar una parte y verificar que funciona correctamente con independencia del resto de partes con las que tenga relación. De esta manera el responsable de cada parte de código puede centrarse en probar que el código que está desarrollando funcione correctamente con independencia del estado de otras partes de la aplicación con las que tenga relación.

 

RhinoMock Framework

Existen varias herramientas que permiten trabajar mediante el uso de objetos mock. De entre ellas hemos tomado la decisión de utilizar el framework “Rhino Mocks” principalmente porque es más fácil de usar que el resto de herramientas de este tipo.

A continuación vamos a explicar que pasos debemos seguir para implementar y utilizar un objeto mock en nuestros proyectos .NET . Para ello vamos a basarnos en el siguiente ejemplo:

“Disponemos de una clase de negocio, PacienteCN, que realiza varias acciones y entre otras cosas llama a la capa de acceso a datos, PacienteAD, para recuperar las operaciones programadas para un paciente en una fecha determinada.

Esta capa de acceso a datos dispone de un método que recibe un número de historia y una fecha y devuelve una lista con las operaciones programadas para un paciente en esa fecha.

Tras terminar de desarrollar la clase de negocio queremos probar que esta funciona correctamente pero nos encontramos con que la capa de acceso a datos no está terminada todavía.

Para poder probar nuestra clase de negocio vamos a implementar un objeto mock que simule el funcionamiento de la capa de acceso a datos de manera que cuando lancemos el método que devuelve la lista de operaciones programadas para un paciente y una fecha sea el objeto mock quien intercepte esa petición y devuelva una respuesta predeterminada que es la que deberá devolver el objeto real cuando esté desarrollado y funcione correctamente.

De esta manera podemos probar como reacciona el código de la clase de negocio cuando recibe una respuesta de la capa de datos, sin necesidad de que esta esté implementada todavía.”

Instalación de librerías necesarias

Lo primero que tenemos que hacer es copiar en nuestro proyecto la librería “Rhino.Mocks.dll” que nos proveerá de los métodos necesarios para la utilización de los mocks. Deberemos colocarla en el siguiente directorio:

C:Mis Documentos Visual Studio 2005 Projects MiAplicacion References

 

Definición del Interfaz del objeto que queremos simular

El objeto mock que vamos a implementar deberá imitar el comportamiento del objeto real al que va a sustituir durante las pruebas. Para asegurarnos de ello el objeto mock y el objeto real deberán cumplir el mismo interfaz.

Partimos del hecho de que conocemos cual va a ser el comportamiento del objeto real aunque todavía no se encuentre disponible, es decir, tenemos una definición de dicho objeto. A partir de esa definición creamos un interfaz que utilizaremos más adelante para definir el objeto mock que simule nuestro objeto real.

En nuestro ejemplo vamos a simular la capa de acceso a datos que obtiene los datos de un paciente, y en concreto vamos a simular el método que devuelve la lista de intervenciones programadas para una historia y una fecha concretas. El interfaz quedará definido de la siguiente manera:

public interface IPacientesAD

{

   string ObtenerIntervenciones(int historia, DateTime fecha);

 }

Preparación de la clase cuyo código queremos probar

Debemos preparar la clase cuyo código queremos probar de manera que trabaje de la misma manera con un objeto real que con un objeto mock.

Para hacer esto se deberán realizar los siguientes pasos:

1. Crear un atributo privado que implemente el interfaz del objeto que vamos a querer simular

2. Crear dos constructores de dicha clase que instancien el objeto que queremos simular. El primero constructor no tendrá ningún parámetro y será utilizado en la ejecución real del código ya que será el que cree el objeto real. El segundo de ellos tendrá como parámetro un objeto y será utilizado en la ejecución de las pruebas de manera que el objeto que se pasa sea el mock que hallamos definido en la prueba y la clase trabaje en ese caso con el objeto mock y no con el real.

En nuestro ejemplo la clase que queremos probar es la clase de Negocio PacienteCN. El código de esta clase quedará de la siguiente manera:

 

 
 
public class PacienteCN
	{
		
// Atributo de la clase que implementa el interfaz del 
// objeto que queremos simular, en nuestro caso la clase de
// acceso a datos PacienteAD

private IPacienteAD _pacienteAD;


		// Constructor de la clase de negocio que implementa un 
// objeto de acceso a datos real.

		public PacienteCN()
		{
			_pacienteAD = new PacienteAD();
		}


		// Constructor de la clase de negocio que implementa un 
// objeto de acceso a datos que recibe como parámetro y
// que será utilizado desde la clase de pruebas donde se 
// le pasará un objeto mock.

		public PacienteCN(IPacienteAD pacienteAD)
		{
			_ pacienteAD = pacienteAD;
		}
		


// Metodo de la clase de negocio que llama al objeto de
// acceso a datos. El código es el mismo con independencia
// de que el objeto utilizado sea real o sea un mock.

public string ObtenerIntervenciones (int historia, DateTime fecha)
		{
		 return _pacienteAD.ObtenerIntervenciones(historia, fecha);
		}

	} 

En resumen, hemos preparado la clase para que dependiendo de cómo la instanciemos trabaje de la misma manera contra un objeto real que contra un objeto mock. Hemos independizado la clase de negocio de la clase de acceso a datos. Esta podrá obtener la información de una base de datos oracle, de una de sqlserver, de un AS400 o de cualquier otro proveedor, pero nuestra clase de negocio funcionará de la misma forma sea cual sea el proveedor utilizado en el acceso a los datos. Esto se consigue porque el objeto mock utilizado cumple el mismo interfaz que el objeto real de acceso a datos, es decir, externamente se comporta igual que él.

Preparación de la clase de pruebas que valida el funcionamiento de nuestro código

En la clase de pruebas que vamos a preparar para testear nuestro código es donde vamos a definir el objeto mock y su comportamiento.

Lo primero que tendremos que hacer es añadir en nuestro proyecto de pruebas la referencia a la librería Rhino.Mocks.dll.

A continuación deberemos incluir en nuestro fichero de pruebas, a través de la directiva using, la referencia para poder utilizar las funciones definidas en el espacio de nombres Rhino.Mocks:

 

using Rhino.Mocks;

El siguiente paso consiste en definir dentro de nuestra clase de pruebas una serie de variables que vamos necesitar utilizar:

 

public class PacientesCNTest
	{
		// Variable que necesita Rhino mocks para su ejecución.
// Se trata de un repositorio de objetos mock que vayamos
// utilizar
		private MockRepository _mocks;

		// Esta Variable contendrá el objeto mock que vamos a
// utilizar. Este objeto debe cumplir el interfaz de 
// la clase que debe simular, en nuestro ejemplo el
// interfaz de la clase de acceso a datos de pacientes.
		private IPacientesAD _pacientesADMock;


		// Esta variable contendrá el objeto de negocio que vamos
		// a crear y utilizar durante la ejecución de las pruebas.
		private PacientesCN _pacientesCN;

El siguiente paso consiste en definir dentro de la clase de pruebas un método que inicialice el uso de nuestros mocks. Este método será el encargado de crear el repositorio de mocks, los mocks que vayamos a utilizar, inicializar los mocks y crear el objeto que queramos probar pasándole por parámetro el objeto mock que queremos que utilice. Para definir este método vamos a utilizar el marcador [TestInitialize]:

 

[TestInitialize]
	public void PacienteCNTestInitialize()
	{
		// Creamos el repositorio de mocks

		_mocks = new MockRepository();


		// Creamos nuestro mock. En nuestro ejemplo debe cumplir el
		// Interfaz de la clase de acceso a datos de los pacientes.

	_pacientesADMock = _mocks.CreateMock(typeof(IPacientesAD))
as IPacientesAD;
		

		// Inicializamos el uso de los objetos mocks	

		InitializeMockObject();
		_mocks.ReplayAll();


		// Creamos el objeto cuyo código queremos probar, en 
// nuestro caso la clase de negocio de pacientes pasándole
		// como parámetro el objeto mock.

		_pacientesCN = new PacientesCN(_pacientesConADMock);

	}

A continuación debemos definir el método que inicializa nuestro mock, es decir, el método que define cual va a ser el comportamiento del mock. La inicialización consiste en definir cual debe ser la respuesta que debe dar el objeto mock cuando se lanza un método determinado con unos parámetros concretos. Dicha respuesta será la que esperamos que devuelva el objeto real cuando esté en funcionamiento.

Esto lo definimos en el método InitializeMockObject() al que hemos llamado al inicializar la prueba:

 

private void InitializeMockObject()
	{
	
// Cuando nuestro objeto mock _pacientesADMock reciba una 
// llamada a su método ObtenerIntervenciones() con los parámetros de
// entrada historia = 595962 y fecha = 31/01/2006 hacemos que
// devuelva la salida que esperamos. 
	Expect.On(_pacientesADMock).Call(_pacientesADMock.ObtenerIntervenciones 
(595962,new DateTime(2006,01,31))).Return( “ALEJOENDEIZAARRUZAQUIROFANO1OPERACIONAMIGDALAS”);

	}

Por último solo nos queda crear el método de prueba que ejecuta nuestro código, en nuestro caso la clase PacienteCN. Se trata de un método de prueba normal en el que lanzamos el método que queremos probar con unos parámetros concretos y comparamos si la salida es la esperada.

 

[TestMethod]
	public void ObtenerIntervencionesCNTest()
	{
		string actual;
		actual = _pacientesCN.ObtenerIntervenciones (595962, new DateTime(2006, 1, 31));

		Assert.AreEqual(actual, “ALEJOENDEIZAARRUZAQUIROFANO1OPERACIONAMIGDALAS”, 
"EL paciente esperado no es el correcto");
			
			
	}

Al ejecutar la prueba llamamos al método ObtenerIntervenciones de la clase de negocio que queremos probar. En este ámbito de pruebas la clase de negocio la hemos creado con un constructor al que le hemos pasado un objeto mock. Hemos definido que ese objeto mock simula el comportamiento para el método ObtenerIntervenciones cuando recibe unos parámetros determinados. Al ejecutar la prueba y llamar a ese método concreto de nuestro código será el objeto mock quien “intercepte” la llamada y responda en lugar del objeto real. Así podemos efectuar pruebas de nuestro código con independencia de la capa de acceso a datos. No nos importa que ésta llame a un AS400 o a una base de datos o a cualquier otro proveedor de datos ya que siempre deberá cumplir el interfaz que hemos creado a partir de su definición y que hemos hecho que cumpla también nuestro objeto mock.

Más información acerca de Rhino Mocks

Si deseas buscar más información referente al frameworks “Rhino Mocks” la puedes encontrar en la siguiente dirección: http://www.ayende.com/projects/rhino-mocks.aspx

WCF DataContracts

En los posts anteriores hemos llegado a ver la implementación de un servidor y cliente sencillos de WCF. Los datos que eran intercambiados eran clases del framework (int, string…), pero como intercambiar datos de mis clases (Clientes, proveedores…).

El CLR debe generar un esquema XML a partir de un tipo, es lo que se conoce con el nombre Data Contracts. El Data Contract describe como los tipos CLR son mapeados al esquema XML.

Al igual que ocurria con los servicios Web,  las clases que representan nuestras entidades con solo marcarlas con el atributo [Serializable] ya podrian ser expuestas por los servicios para que los clientes puedan consumirlas.

Y es cierto, funciona ya que la clase XMLSerializer sera la encargada de serializar la clase. Pero en WCF existe el sucesor de esta clase y es la clase XMLFormatter, que es la encargada de seralizar estos Data Contracts, la diferencia con la anterior es que es un 10% mas rapida, pero XMLSerializer nos da un control mas granular sobre la serialización de los objetos. XMLFormatter esta orientada a la rapidez que al control del desarrollador (esto sera estudio de algun post posterior)

Si antes marcaba la clase con el atributo [Serializable], ahora la debo marcar con el atributo [DataContract] y debo marcar los miembros que quiero que formen parte del mensaje con el atributo [DataMember].

Un ejemplo

DataContract(Namespace="http://schemas.thatindigogirl.com/2005/12/LinkItems", Name="LinkItem")]
public class LinkItem
{
  [DataMember(Name="Id", IsRequired=false, Order=0)] 
  private long m_id;
  [DataMember(Name = "Title", IsRequired = true, Order = 1)] 
  private string m_title;
  [DataMember(Name="Description", IsRequired = true, Order = 2)] 
  private string m_description;
  [DataMember(Name="DateStart", IsRequired = true, Order = 3)] 
  private DateTime m_dateStart;
  [DataMember(Name="DateEnd", IsRequired = false, Order = 4)] 
  private Nullable<DateTime> m_dateEnd;
  [DataMember(Name="Url", IsRequired = false, Order = 5)] 
  private string m_url;
  [DataMember(Name="LinkType", IsRequired = false, Order=6)] 
  private string m_linkType;

  public DateTime DateStart
  {
    get { return m_dateStart; }
    set { m_dateStart = value; }
  }

  public string LinkType
  {
    get { return m_linkType; }
    set { m_linkType = value; }
   }

   public DateTime DateEnd
   {
     get { return m_dateEnd; }
     set { m_dateEnd = value; }
   }
       
   public string Url
   {
     get { return m_url; }
     set { m_url = value; }
   }
        
   public long Id
   {
     get { return m_id; }
     set { m_id = value; }
   }

   public string Title
   {
     get { return m_title; }
     set { m_title = value; }
   }

  public string Description
  {
    get { return m_description; }
    set { m_description = value; }
  }
}


 

Pero al DataContract Atributo se le pueden establecer propiedades, tales como el Name y el Namespace. por defecto el nombre es el nombre del tipo en este caso es redundante, pero podria indicarle que sea expuesto con otro nombre. Lo que si tiene importancia es el namespace, Los esquemas que describen tipos complejos pertenecen generalmente a un namespace específico. Por defecto, el namespace proporcionado para ti derivará de “http://schemas.datacontract.org/2004/07”. El esquema que se generaria sin ningun aributo seria el siguiente

 

 <?xml version="1.0" encoding="utf-8" ?> 
<xs:schema elementFormDefault="qualified" 
targetNamespace="http://schemas.datacontract.org/2004/07/EventsService"
 xmlns:xs="http://www.w3.org/2001/XMLSchema" 
xmlns:tns="http://schemas.datacontract.org/2004/07/EventsService" 
xmlns:ser="http://schemas.microsoft.com/2003/10/Serialization/">
<xs:complexType name="LinkItem">
<xs:sequence>
  <xs:element minOccurs="0" name="DateEnd" type="xs:dateTime" /> 
  <xs:element minOccurs="0" name="DateStart" type="xs:dateTime" /> 
  <xs:element minOccurs="0" name="Description" nillable="true" type="xs:string" /> 
  <xs:element minOccurs="0" name="Id" type="xs:long" /> 
  <xs:element minOccurs="0" name="Title" nillable="true" type="xs:string" /> 
  <xs:element minOccurs="0" name="Url" nillable="true" type="xs:string" /> 
</xs:sequence>
</xs:complexType>
<xs:element name="LinkItem" nillable="true" type="tns:LinkItem" /> 
</xs:schema>

 

Solamente los campos o las características marcados con el DataMemberAttribute se incluyen en la serialización (por lo tanto, en la definición del esquema). Por defecto, se serializan en orden alfabético. Para evitar dependencias cuando se cambia la definición de la clase, se puede indicar el orden del esquema con el atributo Order, ademas podemos indicar si el miembro es un elemento requerido con el atributo Requiered. Este atributo indicara al esquema que ese miembro debe aparecer en el mensaje, si dicho miembro no aparece se producira un error, rechazando el mensaje

El esquema anterior se convertiria en:

 

   <?xml version="1.0" encoding="utf-8" ?> 
<xs:schema elementFormDefault="qualified" targetNamespace=
"http://schemas.thatindigogirl.com/2005/12/LinkItems" xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:tns=http://schemas.thatindigogirl.com/2005/12/LinkItems 
xmlns:ser="http://schemas.microsoft.com/2003/10/Serialization/">
<xs:complexType name="LinkItem">
<xs:sequence>
  <xs:element minOccurs="0" name="Id" type="xs:long" /> 
  <xs:element name="Title" nillable="true" type="xs:string" /> 
  <xs:element name="Description" nillable="true" type="xs:string" /> 
  <xs:element name="DateStart" type="xs:dateTime" /> 
  <xs:element minOccurs="0" name="DateEnd" type="xs:dateTime" /> 
  <xs:element minOccurs="0" name="Url" nillable="true" type="xs:string" /> 
</xs:sequence>
</xs:complexType>
<xs:element name="LinkItem" nillable="true" type="tns:LinkItem" /> 
</xs:schema>

Podemos ver claramente las diferencias entre un esquema u otro.

 

 

Technorati tags:

WCF el Cliente

Una vez construido nuestro servicio y hecho el hosting del mismo, ahora nos queda realizar el cliente, es decir, como consumir ese servicio.

Para ello crearemos un aplicación de consola y el primer paso es añadir la serferencia del ensamblado System.ServiceModel. Si recordamos como funcionabamos con los servicios Web debiamos añadir la referencia para poder interactuar con el servicio Web que nos generaba el mapeo, clases… para poder utilizarlo en nuestra aplicación, ahora necesitamos dos archivos el proxy y el archivo de configuracion. Para ello utilizaremos la herramienta svcutil, es una herramienta de liena de comandos y para ejecutarla como todas las herramientas de linea de comandos de levantaremos la consola de comandos de Visual Studio 2005 y ejecutaremos el siguiente comando:

svcutil.exe http://localhost:8000/Derivatives/ 

Este comando nos genera dos archivos client.cs y output.config. Estos dos archivos los añadimos al proyecto de consola, renombrando el fichero output como app.config.

Con estos dos ficheros tenemos el ABC del cliente y ahora es tan sencillo como haciamos con los servicios Web.

 

 public static void Main(string[] args)
        {
            Console.WriteLine("Press any key when the service is ready.");
            Console.ReadKey();

            decimal result = 0;
            using (DerivativesCalculatorClient proxy =
                new DerivativesCalculatorClient("WSHttpBinding_IDerivativesCalculator"))
            {
                result = proxy.CalculateDerivative(
                    new string[] { "MSFT" },
                    new decimal[] { 3 },
                    new string[] { });
                proxy.Close();
            }
            Console.WriteLine(string.Format("Result: {0}", result));

            Console.WriteLine("Press any key to exit.");
            Console.ReadKey();

        }

Como podemos ver en el codigo, creamos un proxy a partir de la clase cliente que nos ha generado, y a partir de ahi se puede llamar a los metodos que ha expuesto nuestro servcio.

Pero la pregunta que nos tendriamos que hacer es como el cliente sabe a que dirección debe acceder y el protocolo a utilizar. Bueno, esto es sencillo, si recordamos que nos generaba la herramienta dos ficheros, uno de ellos era un fichero config (como todo en WCF), en el que esta toda la información que nos preguntabamos, de manera que nos permite reconfigurar el cliente, sin necesidad de programar una sola línea de código más.

El fichero de condifuración que nos ha generado contendria:

<configuration>
    <system.serviceModel>
        <bindings>
            <wsHttpBinding>
                <binding name="WSHttpBinding_IDerivativesCalculator" closeTimeout="00:01:00"
                    openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
                    bypassProxyOnLocal="false" transactionFlow="false" hostNameComparisonMode="StrongWildcard"
                    maxBufferPoolSize="524288" maxReceivedMessageSize="65536"
                    messageEncoding="Text" textEncoding="utf-8" useDefaultWebProxy="true"
                    allowCookies="false">
                    <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
                        maxBytesPerRead="4096" maxNameTableCharCount="16384" />
                    <reliableSession ordered="true" inactivityTimeout="00:10:00"
                        enabled="false" />
                    <security mode="Message">
                        <transport clientCredentialType="Windows" proxyCredentialType="None"
                            realm="" />
                        <message clientCredentialType="Windows" negotiateServiceCredential="true"
                            algorithmSuite="Default" establishSecurityContext="true" />
                    </security>
                </binding>
            </wsHttpBinding>
        </bindings>
        <client>
            <endpoint address="http://localhost:8000/Derivatives/Calculator"
                binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_IDerivativesCalculator"
                contract="IDerivativesCalculator" name="WSHttpBinding_IDerivativesCalculator">
                <identity>
                    <userPrincipalName value="WIN2K3STD221105Administrator" />
                </identity>
            </endpoint>
        </client>
    </system.serviceModel>
</configuration>

El codigo de la solución se encuentra disponible aqui

Ya hemos generado todo el camino completo de un WCF, en los siguientes post vamos a ir ampliando WCF

 

Technorati tags:

Error curioso en TFS

Hoy estabamos configurando una maquina virtual en la que hemos instalado TFS WorkGroup y nos hemos encontrado con el siguiente error

TF54000: Error TFS Clock time advanced

o en castellano

Exception Message: TF54000: No se puede actualizar porque el reloj del servidor se puede haber establecido incorrectamente

Era tan sencillo que al crear la maquina virtual no le pusimos la fecha correctamente, sino que le pusimos sin darnos cuenta 1 de Noviembre, con lo que al generar el proyecto la fecha de mi PC era diferente que la fecha del servidor.

Cambiamos la fecha del servidor y sin problemas, pero leyendo encontre que equipos deslocalizados en EEUU, uno en la coste Este y otro en la costa Oeste por la diferencia horaria tenian ese problema.

como nosotros los españolitos tenemos las misma hora sin problemas, pero un error curioso

 

Technorati tags: