Los 5 errores más comunes en servicios web ASP.NET

Traducción del artículo publicado por Paul Ballard el 27 de mayo de 2005 en TheServerSide.NET. Además de la traducción inglés-castellano, he traducido los ejemplos de VB.NET a C#. Puedes encontrar el original aquí: Top 5 Web Service mistakes.

En este artículo vamos a comentar los 5 errores más comunes que comenten las empresas cuando desarrollan servicios web en .NET. Es importante resaltar que no son errores de lógica o sintaxis. No hay errores de compilación que te digan que algo está mal y no se lanzarán excepciones. Son problemas de arquitectura, no de tecnología. La clave, por tanto, es evitarlos desde el principio, ya que intentar corregirlos después de su puesta en producción puede ser mucho más complicado.

1. Usando tipos específicos de .NET.

El concepto fundamental detrás de los servicios web es la creación de lógica de aplicación accesible en cualquier momento, desde cualquier sitio, por cualquier otra aplicación independientemente de la tecnología. Esto incluye también a aplicaciones no .NET. El aspecto más importante para garantizar la interoperabilidad en servicios web no se encuentra en el código, o al menos no directamente. Para que un servicio sea interoperable con cualquier tecnología el contrato de datos que define debe estar basado en tipos definidos en un XML Schema, no en el framework .NET. Por tanto, si usas tipos específicos de .NET como parámetros o valores de retorno para un servicio web, el XML que produce puede no ser interoperable con Java u otra tecnología.

Quizá has visto las demos de cómo de fácil es crear un simple servicio web que consulta a una base de datos y devuelve los datos como un Dataset de ADO.NET. Un Dataset es una versión totalmente desconectada de datos relacionales, incluyendo tablas, claves, y relaciones y demás, por lo que puede parecer el objeto perfecto para transmitir datos entre distintas localizaciones. En el caso de los servicios web, las apariencias engañan.

Pasar un Dataset como resultado o parámetro en un servicio web crea distintos problemas. El primero ocurre en el WSDL. WSDL es el schema XML que define como los clientes pueden interactuar con tu servicio y qué parametros y valores de retorno utiliza. WSDL es a menudo utilizado para generar clases proxy del lado del cliente, por lo que mientras más descriptivo sea, más fácil será interactuar con nuestro servicio.

Veamos un ejemplo de un simple servicio con un método que devuelve un Dataset que contiene un fila de la tabla Customers de la base de datos Northwind. A continuación se muestra el WSDL para ese servicio. Fíjate en la elemento GetCustomersResponse.

<s:element name=GetCustomersResponse>

  <s:complexType>

    <s:sequence>

      <s:element minOccurs=0 maxOccurs=1 name=GetCustomersResult>

        <s:complexType>

          <s:sequence>

            <s:element ref=s:schema/>

            <s:any/>

          </s:sequence>

        </s:complexType>

      </s:element>

    </s:sequence>

  </s:complexType>

</s:element>

Debido a que la estructura del Dataset no se conoce hasta tiempo de ejecución, no hay manera para el WSDL de describir su estructura interna. Para resolver esto, el valor de retorno (elemento GetCustomersResponse) del método GetCustomers tiene dos elementos, un elemento de tipo any (<s:any/>) que es en efecto un tipo de objeto y un XML Schema (<s:schema>) que en tiempo de ejecución describirá la estructura del Dataset de retorno. Esto es el equivalente en servicios web a late binding.

Debido a que el desarrollador del cliente no sabe la estructura del objeto que tu método devolverá, no podrán generar un proxy muy útil. Podrán usar tu servicio web, pero deberán parsear el XML de retorno para encontrar los datos que están buscando. La seguridad de tipos se ha tirado por el retrete y has hecho que el trabajo de cualquier desarrollador de un cliente no .NET sea más difícil.

Ahora deberías estar diciéndote a tí mismo: “Si el WSDL no define un tipo de retorno Dataset, ¿cómo funcionan todas esas demos?”. La respuesta está en el XML devuelto en tiempo de ejecución. A continuación se muestra el resultado de llamar a nuestro método GetCustomers.

<?xml version=1.0 encoding=utf-8?>

<DataSet xmlns=http://tempuri.org/>

  <xs:schema id=NewDataSet xmlns=“” xmlns:xs=http://www.w3.org/2001/XMLSchema xmlns:msdata=urn:schemas-microsoft-com:xml-msdata>

    <xs:element name=NewDataSet msdata:IsDataSet=true msdata:UseCurrentLocale=true>

      <xs:complexType>

        <xs:choice minOccurs=0 maxOccurs=unbounded>

          <xs:element name=Table>

            <xs:complexType>

              <xs:sequence>

                <xs:element name=CustomerID type=xs:string minOccurs=0 />

 

                <xs:element name=CompanyName type=xs:string minOccurs=0 />

                <xs:element name=ContactName type=xs:string minOccurs=0 />

                <xs:element name=ContactTitle type=xs:string minOccurs=0 />

                <xs:element name=Address type=xs:string minOccurs=0 />

                <xs:element name=City type=xs:string minOccurs=0 />

                <xs:element name=Region type=xs:string minOccurs=0 />

                <xs:element name=PostalCode type=xs:string minOccurs=0 />

                <xs:element name=Country type=xs:string minOccurs=0 />

                <xs:element name=Phone type=xs:string minOccurs=0 />

 

                <xs:element name=Fax type=xs:string minOccurs=0 />

              </xs:sequence>

            </xs:complexType>

          </xs:element>

        </xs:choice>

      </xs:complexType>

    </xs:element>

  </xs:schema>

  <diffgr:diffgram xmlns:msdata=urn:schemas-microsoft-com:xml-msdata xmlns:diffgr=urn:schemas-microsoft-com:xml-diffgram-v1>

 

    <NewDataSet xmlns=“”>

      <Table diffgr:id=Table1 msdata:rowOrder=0>

        <CustomerID>ALFKI</CustomerID>

        <CompanyName>Alfreds Futterkiste</CompanyName>

        <ContactName>Maria Anders</ContactName>

        <ContactTitle>Sales Representative</ContactTitle>

        <Address>Obere Str. 57</Address>

 

        <City>Berlin</City>

        <PostalCode>12209</PostalCode>

        <Country>Germany</Country>

        <Phone>030-0074321</Phone>

        <Fax>030-0076545</Fax>

      </Table>

 

    </NewDataSet>

  </diffgr:diffgram>

</DataSet>

Fíjate en que el elemento NewDataSet tiene un atributo específico de Microsoft llamado “IsDataSet”. Esta es una pista de Visual Studio de que el resultado debería ser deserializado como Dataset. Por supuesto, esto no significa nada si estás llamando a este servicio web desde un cliente no .NET como J2EE.

El segundo problema cuando pasamos Datasets a y desde un servicio web es la gran cantidad de datos que se envía. El XML que vimos anteriormente sólo describe la estructura de un dataset con una tabla. Mientras añades tablas, relaciones y claves, el schema se vuelve más complejo y más grande. De hecho, rápidamente puede convertirse en mayor que los datos que se devuelven. Recuerda, mientras más datos envíes a / desde un servicio más lenta se volverá la petición.

El Dataset es probablemente la forma más común de usar tipos específicos de .NET en servicios web pero no es el único caso. Tiene mucho sentido para un desarrollador .NET querer usar las características avanzadas y las clases provistas por el framework .NET. Sin embargo, si la interoperabilidad real es tu objetivo, tendrás que controlar tus impulsos. Para interoperabilidad máxima, la clave es asegurarse que todos los tipos que se utilizan como parámetros y valores de retorno pueden describirse usando los tipos básicos del XML Schema estándar. Puedes encontrar la definición de todos esos tipos en http://www.w3.org/TR/xmlschema-2/

2. No aprovecharse de ASP.NET

La mayor desventaja de consumir servicios web es el rendimiento. Cualquier mejora que se pueda realizar para incrementar el rendimiento merece el esfuerzo. Los servicios web desarrollados en .NET se despliegan en ASP.NET y por tanto puedes acceder a todas esas grandes características de cualquier aplicación web ASP.NET. Para el rendimiento, las características más importantes son el Caching y el SessionState.

Los servicios web pueden usar dos tipos de cacheo. El primero y el más sencillo es cacheo de salida. El cacheo de salida permite que el resultado de un servicio web se almacene en la caché del servidor. Las siguientes peticiones a ese método que usen los mismos parámetros obtendrán los valores cacheados en el servidor sin llamar al método del servicio y por tanto, sin consulta de datos, lógica, etc. Cuando los parámetros del método cambian se añade una entrada a la caché del servidor con el tiempo de expiración especificado. Para usar esta característica, sólo añade la propiedad “CacheDuration” a la clase WebMethodAttribute. Aquí hay un ejemplo simple que cachea el resultado de un método durante 120 segundos.

        [WebMethod(CacheDuration=120)]

        public Quote GetQuote ()

También puedes usar el cacheo de datos de aplicación para almacenar datos específicos entre peticiones. Datos que se utilizan a menudo pero que no cambian tan frecuentemente son los primeros candidatos a ser almacenados en la caché de la aplicación.

Para ilustrar esto vamos a construir un servicio ficticio para obtener cotizaciones de acciones. Una función que ofrece este servicio es la posibilidad de obtener el símbolo de una empresa utilizando su nombre. Una función como ésta debe ser llamada a menudo, a pesar de que los cambios en los datos apenas varían. En este caso querremos consultar la base de datos una vez al inicio y devolver un array conteniendo todos los nombres de empresas y sus correspondientes símbolos y almacenar ese array en la caché con una expiración diaria. Cuando un cliente busca el símbolo de una empresa, en vez de hacer una llamada a la base de datos con cada petición podemos buscarla en el array almacenado en caché. A continuación se muestra el código necesario para implementar este tipo de cacheo.

        [WebMethod]

        public Symbol[] GetAllSymbols ()

        {

            Symbol [] symbols;

            if (HttpContext.Current.Cache [“AllSymbols”] == null)

            {

                symbols = _QuotesData.GetAllSymbols(); // extrae datos de la BBDD

                HttpContext.Current.Cache.Add(“AllSymbols”, symbols, null, DateTime.MaxValue,

                    new TimeSpan(24, 0, 0), System.Web.Caching.CacheItemPriority.Normal, null);

            }

            else

            {

                symbols = HttpContext.Current.Cache [“AllSymbols”] as Symbol[];

            }

            return symbols;

        }

Los servicios web también pueden usar la persistencia en el estado de la sesión ofrecida a través de ASP.NET incluyendo estado fuera del proceso del servidor y SQL Server. Para habilitar esta característica añade “EnableSession=True” al atributo WebMethod. Esto es muy útil para almacenar información específica del cliente entre peticiones. Por ejemplo, continuando el desarrollo de nuestro sistema de análisis de stock vamos a decir que nuestro servicio web permite a los clientes crear una lista personalizada de stocks que están siguiendo. Cuando el cliente solicita la lista de stocks para ese usuario la primera vez, almacenamos la lista en la sesión antes de devolverlo al cliente. Ahora podemos modificar nuestro método para obtener la lista de los stocks desde el SessionState si el cliente no ha mandado una nueva lista. Esto puede aumentar el rendimiento de las actualizaciones sustancialmente. La siguiente figura muestra ejemplos de métodos que utilizan el estado de la sesión.

        [WebMethod(CacheDuration = 60, EnableSession = true)]

        public Quote [] GetQuotes (string [] symbols)

        {

            List<Quote> lQuotes = new List<Quote>();

            foreach (string symbol in symbols)

            {

                lQuotes.Add(GetQuote(symbol));

            }

            // Store in SessionState just in case the client wants to recall without

            // sending the list again

            HttpContext.Current.Session [“SymbolList”] = symbols;

            return lQuotes.ToArray();

        }

 

        [WebMethod(CacheDuration = 60, EnableSession = true)]

        public Quote [] GetQuotesFromSession ()

        {

            string [] symbols = (string[]) HttpContext.Current.Session [“SymbolList”];

            return GetQuotes(symbols);

        }

ASP.NET utiliza cookies en el cliente para identificar de manera única cada sesión, por tanto, para utilizar esta característica, necesitarás cierto trabajo en el cliente. Con .NET es muy simple, sólo necesitamos crear un nuevo CookieContainer para almacenar las cookies recibidas desde el servicio. Aquí está el código de cliente necesario para hacer esto:

            myService.CookieContainer = new System.Net.CookieContainer();

Aunque usar el estado de la sesión puede aumentar el rendimiento, “rompe” las reglas de la orientación a servicios atendiendo a que los servicios no deben tener estado. Es muy importante recordar también que no todos los clientes son capaces de aceptar tus cookies. La mejora de rendimiento es sustancial y con algunas advertencias, creo que merece la pena usarlo.

Advertencia 1: Cualquier método web que use el estado de la sesión debe tener su correspondiente versión sin estado.

Advertencia 2: Por claridad, debes nombrar a tus métodos con estado de modo que el desarrollador del cliente sepa que el uso del estado de la sesión está habilitado.

3. Not Enough Bang for the Buck

Si tú personalmente tuvieras que seguir a pie el camino que las peticiones a tu servicio web hacen a través de la compañía o a través del mundo, una cosa es cierta: querrías hacer el viaje sólo una vez. Esta es la misma razón por la que hacemos una lista antes de ir al supermercado en vez de hacer el viaje para cada producto. Hay siempre una disminución de rendimiento o un aumento del tráfico de red con cada petición.

Tus servicios web deberían diseñarse para maximizar la cantidad de trabajo que se realiza con cada petición. Asegúrate de que la funcionalidad que estás ofreciendo a través del servicio valga el tiempo y el esfuerzo que el cliente gastó en hacerte la petición. Como ejemplo, vamos a trabajar en nuestro servicio de cotizaciones.

Probablemente has visto demos que muestran lo sencillo que es enviar un símbolo y recibir una cotización actual de mercado. Pero ¿y si quieres conocer el precio de 10 símbolos a la vez? Usando la versión de la demo, deberías hacer diez peticiones al servidor. ¿Querrías dar esa caminata?

Para mejor rendimiento, considera combinar esas pequeñas peticiones en una única petición mayor. En el caso de los precios de las acciones, es sencillo combinar los símbolos de entrada en un array de cadenas y devolver un array de Quotes en el método. Esta implementación del método GetQuotes se puede ver en el fragmento de código anterior.

En casos más complejos, tus servicios pueden comenzar a asemejarse al patrón Request-Broker y de hecho muchos servicios web se construyen de este modo. Cada petición contiene una o varias peticiones más pequeñas. Los valores de retorno se integran en una única respuesta y devueltos al cliente. Precaución, pues esto puede convertirse en un servicio con una interfaz que es tan genérica que básicamente recibe y envía cualquier mensaje. Una vez más, llegamos a la situación en la que la aplicación cliente es incapaz de crear un proxy útil y es dependiente de documentación externa para la interoperabilidad. Estimar cómo equilibrar la agregación de peticiones para mejorar el rendimiento con la complejidad creciente es una de las tareas del arquitecto de la aplicación y sólo puede ser determinado estudiando caso por caso.

4. Usando servicios web para acceso a datos.

Este error viene de un acerca de los servicios web. Algunos arquitectos y desarrolladores tienden a ver los servicios web como un modo de compartir datos. La confusión es comprensible: Microsoft misma ensucia el agua con “mejoras” como la nueva característica de SQL Server 2005 que permite a procedimientos almacenados ser accedidos como servicios web con una par de clicks. Esto permite grandes demos pero mal diseño de aplicaciones.

Los servicios web no proveen simplemente acceso a datos: proveen acceso al conocimiento del negocio de la organización. Organizaciones de todos los tamaños, desde departamentos hasta grandes corporaciones, están definidas por sus conocimientos del negocio. No vas a Starbucks por tus necesidades bancarias, ni el Banco de España hace cafés. Pero las organizaciones a menudo quieren compartir su experiencia con otras organizaciones. Starbucks puede querer que el Banco de España valide una compra con tarjeta de crédito de una de sus bebidas. Esta información compartida significa que Starbucks no tiene que reinventar cómo los pagos con tarjeta se procesan y puede concentrarse en hacer cafés.

¿Cómo una organización implementa su experiencia en el negocio? Una manera es desarrollando aplicaciones personalizadas que creen, mejoren o automaticen el uso de esa experiencia. Ahí es donde aparecen los servicios web y la orientación a servicios: proveyendo acceso a esas aplicaciones a otras organizaciones están compartiendo su conocimiento.

Quizá el mejor ejemplo de un servicio web exponiendo funcionalidad y no datos es el servicio web de Google para buscar en sitios web. ¿Cómo sería de útil el servicio si sólo devolviera datos, información sobre millones de páginas, y tuvieras que buscar a través de ellas? Google pone sus amplios conocimientos de cómo buscar en la web y de ahí viene el valor de su servicio web, no de sus datos.

Cuando desarrollas tus servicios web, recuerda que lo que tus clientes quieren de tu servicio es tu conocimiento del negocio, no sólo tus datos. Deberías cuestionar cada método de un servicio web que únicamente consulta y devuelve datos. Estás colocando la lógica de negocio en el lado erróneo del servicio y forzando a tus clientes a reinventar tu conocimiento del negocio.

5. Confiar en la aplicación cliente

La responsabilidad de un servicio web va más allá de permitir acceso a nuestras reglas de negocio. Es también responsable de proteger el negocio y la integridad de los datos. Muchas veces un servicio web se desarrolla a la vez que la interfaz de usuario que lo consume. El servicio web actúa como “back end” de la aplicación y comúnmente se deja la seguridad a la interfaz de usuario. Si la seguridad se aplica sólo en el lado del cliente de la aplicación, tu servicio web es vulnerable a ataques y la integridad de tu negocio está en bragas.

La clave es separar, tanto en tu cabeza como en el diseño, la seguridad de la interfaz de usuario de la seguridad del servicio web. Cada componente requiere una aplicación correcta de las técnicas de seguridad incluyendo autenticación, autorización, encriptación de los datos… Ten en mente que otras aplicaciones desarrolladas por otras organizaciones podrían utilizar tu “back end”, por tanto asegúrate de que está protegido.

La seguridad es un tema complejo fuera del ámbito de este artículo, pero aquí tienes algunos puntos en los que centrarte:

  • Debes asumir que no puedes confiar en los datos que recibes como parámetros en los métodos del servicio. Los servicios web se despliegan en ASP.NET y por tanto son susceptibles de los mismos tipos de ataques que cualquier aplicación web ASP.NET, incluyendo SQL Injection, DoS, Replay Attacks… Afortunadamente, también puedes utilizar las mismas contramedidas para defender tus servicios de dichos ataques.
  • Los datos que se devuelven deben ser protegidos. Si te preocupa la integridad de los datos a través de la red, la mejor manera es usar SSL para encriptar los datos entre el cliente y el servidor. También puedes aliviar algunas amenazas de SQL Injection o XPath Injection no devolviendo objetos definidos dinámicamente como Datasets y XmlDocuments.
  • La auditoría es una importante y a menudo infravalorada medida de seguridad. Mediante la habilidad de controlar el uso de tus servicios web permites controlar las violaciones de acceso y tienes la capacidad de responder a esos ataques cuando se producen. Claras violaciones de los datos podrían mandar un mensaje a un administrador o cancelar el acceso del usuario al servicio. Ataques DoS se eliminan mediante mecanismos basados en el uso.

Desde el punto de vista de la arquitectura, la clave para evitar problemas de seguridad después del despliegue de un servicio es tener en cuenta la seguridad desde los principios. No te engañes pensando que securizando el cliente basta.

Conclusión

Los servicios web proveen una mejora sin precedentes en cómo se construyen las aplicaciones distribuidas físicamente o en las organizaciones. Visual Studio.NET y el .NET framework permiten desarrollar servicios web de una manera simple y sencilla, incluso los mal hechos. Desarrollando tus servicios evitando estos clásicos errores asegurará a tu organización lo mejor de los servicios web.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: