lunes, 19 de diciembre de 2011

Servicios web en ASP.NET

En este post veremos los servicios web de ASP.NET.

Los servicios web de ASP.NET, también llamados simplemente servicios web o servicios web XML, son los ficheros con extensión .asmx.

No hay que confundir los servicios web de ASP.NET con los servicios WCF.

Lo cierto es que este post pretende ser una breve referencia para poder después comparar los servicios web con los servicios WCF. A este propósito, te recomiendo encarecidamente la lectura del libro Introducción a Windows Communication Foundation de Hadi Hariri en campusmvp.

Dicho esto, el siguiente post contendrá los siguientes puntos:

  • Crear el servicio web
  • Probar el servicio web
  • Consumir el servicio web
    • Aplicación de consola
    • Sitio web
    • ASP.NET AJAX
    • jQuery
  • Configurar el servicio web

Crear el servicio web

Para añadir un servicio web a nuestro proyecto:

Agregar > Nuevo elemento > Servicio Web

En una aplicación web:

clip_image002[6]

En un sitio web:

clip_image004[6]

El código del servicio web está:

  • WebService1.asmx.vb (aplicación web) .
  • WebService1.vb (sitio web).

Para nuestro ejemplo, trabajaremos con la aplicación web y crearemos 2 métodos:

  • Un método que recibe un tipo simple.
  • Un método que recibe un tipo complejo.

Imports System.Web.Services

Imports System.Web.Services.Protocols

Imports System.ComponentModel

' Para permitir que se llame a este servicio Web desde un script, usando ASP.NET AJAX, quite la marca de comentario de la línea siguiente.

' <System.Web.Script.Services.ScriptService()> _

<System.Web.Services.WebService(Namespace:="http://tempuri.org/")> _

<System.Web.Services.WebServiceBinding(ConformsTo:=WsiProfiles.BasicProfile1_1)> _

<ToolboxItem(False)> _

Public Class WebService1

    Inherits System.Web.Services.WebService

    <WebMethod()> _

    Public Function Saludar(ByVal nombre As String) As String

        Return String.Format("Hola {0}", nombre)

    End Function

    <WebMethod()> _

    Public Function SaludarPersona(ByVal persona As Persona) As String

        Return String.Format("Hola {0}, {1}", persona.Nombre, persona.Apellidos)

    End Function

End Class


Public
Class Persona

    Public Property Nombre As String

    Public Property Apellidos As String

End Class


Como vemos en el código anterior, la clase del servicio web hereda de
System.Web.Services.WebService. Esta clase nos da acceso a los objetos comunes de ASP.NET como Server, Session, etc. Aunque no es obligatorio que nuestro servicio herede de esta clase, está claro que nos será de gran ayuda.

Probar el servicio web

Para probar el servicio web simplemente tenemos que acceder a través del navegador a nuestro fichero WebService1.asmx y ASP.NET nos devolverá automáticamente una página con la lista de métodos disponibles.

clip_image006[6]

Esta página autogenerada por el runtime de ASP.NET es posible porque a través de WSDL (Web Service Description Language), es posible descubrir que operaciones expone un servicio web.

De hecho, si escribimos en el navegador la siguiente dirección: http://localhost/WebApplication1/WebService1.asmx?WSDL, podemos ver como nos devuelve un fichero XML que corresponde con la información del servicio web, que es la información que utiliza ASP.NET para autogenerar la página anterior y que utilizará posteriormente para generar automáticamente un proxy que nos permite consumir el servicios desde las aplicaciones cliente.

clip_image008[6]

Si pulsamos en el método “Saludar”, navegaremos a la dirección WebService1.asmx?op=Saludar y de nuevo ASP.NET genera una página donde podemos probar el método en cuestión.

clip_image010[6]

Si utilizamos Fiddler para investigar la petición y respuesta, obtenemos los siguientes resultados

POST http://localhost:58638/WebService1.asmx/Saludar HTTP/1.1

Accept: text/html, application/xhtml+xml, */*

Referer: http://localhost:58638/WebService1.asmx?op=Saludar

Accept-Language: es-ES

User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)

Content-Type: application/x-www-form-urlencoded

Accept-Encoding: gzip, deflate

Host: localhost:58638

Content-Length: 13

Connection: Keep-Alive

Pragma: no-cache

nombre=Sergio

HTTP/1.1 200 OK

Server: ASP.NET Development Server/10.0.0.0

Date: Mon, 19 Dec 2011 07:04:30 GMT

X-AspNet-Version: 4.0.30319

Cache-Control: private, max-age=0

Content-Type: text/xml; charset=utf-8

Content-Length: 96

Connection: Close

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

<string xmlns="http://tempuri.org/">Hola Sergio</string>


A partir de estos datos podemos concluir que en la página de pruebas autogenerada por ASP.NET:
 

  • No se utiliza SOAP ni en la petición ni la respuesta.
  • La petición es un simple envío del formulario a través del método POST.
  • La respuesta es devuelta en XML.

También es importante ver cómo es posible llamar a un método web con a través de la url ServicioWeb/Método. En nuestro caso WebService1.asmx/Saludar (esto será importante más adelante cuando queramos consumir el servicio desde jQuery)

Para el método SaludarPersona y puesto que trabaja con un tipo complejo, ASP.NET no genera ningún formulario de forma automática para probar nuestro método.

Si por algún motivo no deseas que ASP.NET genere automáticamente la página de pruebas o el WSDL, puedes hacerlo. Más información en este blog.

Consumir el servicio web

El primer paso para consumir el servicio será publicarlo en nuestro IIS local.

Una vez publicado creamos los siguientes clientes:

  • Una aplicación de consola.
  • Un sitio web.
  • ASP.NET AJAX.
  • jQuery.

Aplicación de consola

Agregar referencia de servicio… > Avanzadas… > Agregar referencia web …

clip_image012[6]

Al agregar la referencia, se agregan referencias a los siguientes ensamblados:

  • System.EnterpriseServices
  • System.Web.Services

Además, también se crea la carpeta “Web References”:

clip_image014[6]

El fichero más relevante es Reference.vb donde se ha creado una clase Proxy para llamar a nuestro servicio, además de las clases dependientes como por ejemplo Persona.

Por último, se ha agregado el siguiente código a nuestro fichero app.config, para configurar en tiempo de ejecución la url donde se aloje el servicio web.

<applicationSettings>

  <ConsoleApplication1.My.MySettings>

    <setting name="ConsoleApplication1_ReferenciaWeb_WebService1" serializeAs="String">

      <value>http://localhost/WebApplication1/WebService1.asmx</value>

    </setting>

  </ConsoleApplication1.My.MySettings>

</applicationSettings>


Llamar al servicio web no puede ser más sencillo.

Dim p As New ReferenciaWeb.Persona

p.Nombre = "Sergio"

p.Apellidos = "León"

Dim s As New ReferenciaWeb.WebService1

s.SaludarPersona(p)

 

Para ver con Fiddler el tráfico generado por la llamada, es necesario cambiar localhost por nuestro nombre de máquina en el setting del fichero app.config. Más información en http://fiddler2.com/fiddler/help/hookup.asp#Q-LocalTraffic

POST http://lobezno/WebApplication1/WebService1.asmx HTTP/1.1

User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; MS Web Services Client Protocol 4.0.30319.488)

Content-Type: text/xml; charset=utf-8

SOAPAction: "http://tempuri.org/SaludarPersona"

Host: lobezno

Content-Length: 377

Expect: 100-continue

Connection: Keep-Alive

<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><SaludarPersona xmlns="http://tempuri.org/"><persona><Nombre>Sergio</Nombre><Apellidos>León</Apellidos></persona></SaludarPersona></soap:Body></soap:Envelope>

HTTP/1.1 200 OK

Cache-Control: private, max-age=0

Content-Type: text/xml; charset=utf-8

Server: Microsoft-IIS/7.5

X-AspNet-Version: 4.0.30319

X-Powered-By: ASP.NET

Date: Mon, 19 Dec 2011 07:37:14 GMT

Content-Length: 386

<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><SaludarPersonaResponse xmlns="http://tempuri.org/"><SaludarPersonaResult>Hola Sergio, León</SaludarPersonaResult></SaludarPersonaResponse></soap:Body></soap:Envelope>

 

Ahora podemos ver cómo se utiliza SOAP tanto para llamada como la respuesta.

En cualquier momento podemos actualizar la referencia al servicio web desde el explorador de soluciones:

clip_image016[6]

Sitio web

Al agregar la referencia al servicio web se crean las siguientes carpetas y ficheros:

clip_image018[6]

Además, en el fichero web.config se añade la siguiente información:

<appSettings>

  <add key="ReferenciaWeb.WebService1" value="http://localhost/WebApplication1/WebService1.asmx"/>

</appSettings>

 

En un sitio web, de nuevo utilizaremos SOAP tanto para la llamada como para la respuesta y el código para llamar al servicio es el mismo que el especificado para la aplicación de consola.

ASP.NET AJAX

Para consumir un servicio web desde un formulario web a través ASP.NET AJAX hay que tener en cuenta que hay que hacerlo desde el mismo proyecto en el que está alojado el servicio web.

Los pasos necesarios son:

Descomentar la línea de código del servicio web:

<System.Web.Script.Services.ScriptService()> _

 

Agregar a la página un control ScriptManager y agregar un elemento ServiceReference a nuestro control ScriptManager.

<asp:ScriptManager ID="Scriptmanager1" runat="server">

<Services>

<asp:ServiceReference Path="~/WebService1.asmx" />

</Services>

</asp:ScriptManager>

Escribir el siguiente código Javascript:

function pageLoad() {

    //WebApplication1.WebService1.Saludar(nombre, onSuccess, onFailed, userContext)

    WebApplication1.WebService1.Saludar("Sergio", function (result, userContext, methodName) {

        alert(result);

    }, function (result, userContext, methodName) {

        alert(result.get_message());

        alert(result.get_stackTrace());

        alert(result.get_exceptionType());

        alert(result.get_statusCode());

    });

    //WebApplication1.WebService1.Saludar(persona, onSuccess, onFailed, userContext)

    var p = new WebApplication1.Persona();

    p.Nombre = "Sergio";

    p.Apellidos = "León";

    WebApplication1.WebService1.SaludarPersona(p, function (result, userContext, methodName) {

        alert(result);

    }, function (result, userContext, methodName) {

        alert(result.get_message());

        alert(result.get_stackTrace());

        alert(result.get_exceptionType());

        alert(result.get_statusCode());

    });

}

 

Como podemos ver, ASP.NET AJAX ha creado una clase Proxy para llamar a nuestro servicio web (la clase WebApplication1.WebService1) y también ha generado las clases dependientes como la clase Persona.

Aquí cabe mencionar que si el proyecto es de tipo aplicación web, entonces se creará la clase <NombreProyecto>.<NombreServicio>, mientras que si hablamos de un sitio web, el nombre de clase será simplemente <NombreServicio>.

Todo esto es posible porque el control ScriptManager incluye automáticamente el siguiente código en la página:

<script src=" /WebApplication1/WebService1.asmx/jsdebug" type="text/javascript"></script>


Este código descarga un fichero javascript que es un Proxy de nuestro servicio web para utilizar el servicio desde Javascript.

El parámetro /jsdebug estará activo si estamos depurando la aplicación, en modo Release el parámetro será /js que devuelve una versión minimizada y optimizada para producción.

La traza con Fiddler para la llamada al método Saludar es la siguiente: 

POST http://localhost:58638/WebService1.asmx/Saludar HTTP/1.1

Accept: */*

X-Requested-With: XMLHttpRequest

Content-Type: application/json; charset=utf-8

Referer: http://localhost:58638/WebForm1.aspx

Accept-Language: es

Accept-Encoding: gzip, deflate

User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)

Host: localhost:58638

Content-Length: 19

Connection: Keep-Alive

Pragma: no-cache

{"nombre":"Sergio"}

{"persona”: {__type”:”Persona”,”Nombre”:”Sergio”,”Apellidos”:”León”}}

HTTP/1.1 200 OK

Server: ASP.NET Development Server/10.0.0.0

Date: Mon, 19 Dec 2011 09:35:15 GMT

X-AspNet-Version: 4.0.30319

Cache-Control: private, max-age=0

Content-Type: application/json; charset=utf-8

Content-Length: 19

Connection: Close

{"d":"Hola Sergio"}

{"d":"Hola Sergio, León"}

 

Se ha resaltado en amarillo la información clave y además en naranja la llamada y respuesta para el método SaludarPersona.

Como podemos observar, al llamar a nuestro servicio web desde ASP.NET AJAX, tanto la llamada como la respuesta han utilizado JSON en vez de SOAP.

jQuery

Nuestro último cliente será jQuery y lo cierto es que el más sencillo de todos.

La ventaja de jQuery frente a ASP.NET AJAX es que puede consumir servicios que no estén en la solución actual y además tampoco es necesario ningún cambio en el servicio web, es decir, no es necesario descomentar la línea que sí tuvimos que descomentar para trabajar con ASP.NET AJAX.

Además, para proyectos de ASP.NET MVC sólo tenemos disponible está opción

$(document).ready(function () {

    var options = {

        type: "POST",

        url: "/WebService1.asmx/Saludar",

        data: "{ 'nombre': 'Sergio' }",

        contentType: "application/json; charset=utf-8",

        dataType: "json",

        success: function (data, textStatus, jqXHR) { //do something...
        },

        error: function (jqXHR, textStatus, errorThrown) { //do something...
        }

    }

    $.ajax(options);

});

 

Lo más relevante del este código es:

  • Se utiliza JSON para la llamada según indica el parámetro contentType.
  • Se pasan los datos en la propiedad data, de nuevo como un objeto JSON pero en una cadena.
  • El nombre del parámetro es case-sensitive, así que nombre funcionará pero Nombre fallará.
  • Se espera que la respuesta sea en formato JSON según indica el parámetro dataType.

Si quisiéramos llamar al método SaludarPersona que recibe un objeto del tipo Persona, habría que escribir lo siguiente en la propiedad data:

data: "{ 'persona' : { 'Nombre' : 'Sergio' , 'Apellidos' : 'León' } }",

 

Configurar el servicio web

Hasta ahora, la única configuración que hemos realizado en el servicio web ha sido la siguiente:  

  • Agregar el atributo WebMethod para aquellos métodos que queremos exponer al cliente.
  • Descomentar <System.Web.Script.Services.ScriptService()> si queremos habilitar nuestro servicio web para ser consumido por ASP.NET AJAX.

Lo cierto es que en la mayoría de los casos será suficiente con estos parámetros, pero es recomendable conocer que posibilidades de configuración tenemos a nuestra disposición para un servicio web.

 

BufferResponse

  • Por defecto True.
  • Esta propiedad tiene un comportamiento idéntico a la propiedad de la directiva @Page de una página .aspx. Determina si está activo el buffer durante la respuesta HTTP al cliente. Sino está activo se envía la salida en trozos de 16KB, en caso contrario sólo se envía una única respuesta con todo el contenido

CacheDuration

  • Por defecto 0 (no caché).
  • Determina cuantos segundos se cacheará la salida del método web.
  • Cabe mencionar que se cachea por cada conjunto único de parámetros de entrada del método.

Description

  • Por defecto “”.
  • Es un texto descriptivo asociado al método para el WSDL asociado al servicio web.

EnableSession

  • Por defecto True.
  • Determina si la sesión estará disponible en el servicio web.
  • En caso de estar activada podremos utilizar HttpContext.Current.Session o Session directamente si nuestro servicio web hereda de la clase WebService.

ResponseFormat

  • Especifica el formato de la respuesta cuando el servicio es consumido desde Javascript.
  • Por defecto la respuesta es devuelta en Json.
  • Si se utiliza Json, los datos de respuesta son serializados con la clase JavascriptSerializer, si se especifica Xml los datos de respuesta son serializados con la clase XmlSerializer.

UseHttpGet

  • Por defecto False.
  • Especifica si ASP.NET AJAX llama a los métodos del servicio web con el verbo GET, en modo contrario lo llama con el verbo POST (comportamiento predeterminado).

La mayoría de estas propiedades hablan por sí solas, pero quizás ResponseFormat y UseHttpGet requieran su propia explicación.

 

Por defecto, las llamadas a métodos de los servicios web se harán con el verbo POST (en ASP.NET AJAX), mientras que en jQuery somos nosotros mismos quien especifica el verbo en llamada a $.ajax(). En este situación, si intentamos con jQuery llamar a un método web con el verbo GET obtendremos un error diciéndonos que no está habilitado el método para el verbo seleccionado (en ASP.NET AJAX simplemente no podemos decidir el verbo puesto que está “harcodeado” en la clase proxy que genera automáticamente el control ScriptManager). Siendo así, si utilizamos UseHttpGet con el valor True lo que estamos diciendo es que ahora el único verbo permitido será GET (en vez de POST), luego se “harcodeará” en ASP.NET AJAX y lo tendremos que escribir a mano con jQuery y $.ajax(). Es decir, o va por POST o va por GET, pero no por ambos (todo esto hablando de peticiones de cliente, porque clientes pesados – como una aplicación de consola - siempre harán peticiones POST con independencia de UseHttpGet).


Respecto a ResponseFormat, cambiaremos el método SaludarPersona para que la respuesta sea en Xml.

 

Si especificamos que la respuesta será Xml (con ResponseFormat) podemos, bien devolver un XmlDocument o bien delegar en el framework para que devuelva Xml bien formado.

La ventaja de devolver un XmlDocument es que nosotros estamos controlando el Xml devuelto, mientras que si simplemente devolvemos un tipo básico o complejo lo que haremos es delegar en el framework la respuesta. Si por ejemplo devolvemos un entero sin más, el Xml devuelto sería <?xml version="1.0" encoding="utf-8"?><int>5</int>, y para una cadena podría ser <?xml version="1.0" encoding="utf-8"?><string>Hola Sergio</string> o simplemente “Hola Sergio”, en función del atributo XmlSerializeString (por defecto False, luego “Hola Sergio”). Para un tipo complejo devolvería el resultado de serializar el tipo con XmlSerializer. Por ejemplo y para nuestro tipo Persona:

<?xml version="1.0" encoding="utf-8"?>
<Persona xmlns:xsi="
http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Nombre>panico</Nombre>
<Apellidos>enlaxbox</Apellidos>
</Persona>

A continuación un ejemplo de como devolver un XmlDocument en vez de delegar en la serialización de XmlSerializer

<WebMethod()> _
<Script.Services.ScriptMethod(ResponseFormat:=Script.Services.ResponseFormat.Xml)> _
Public Function SaludarPersona(ByVal persona As Persona) As XmlDocument

        Dim xmlDoc As New XmlDocument

        Dim xmlString As String = _

            "<?xml version=""1.0"" encoding=""utf-8"" ?>" + _

            " <respuesta>" + _

            " <saludo>" + _

            " " & persona.Nombre & " " & persona.Apellidos & _

            " </saludo>" + _

            " </respuesta>"

        xmlDoc.LoadXml(xmlString)

        Return xmlDoc

    End Function


Si ahora vemos ahora la respuesta con Fiddler, podemos observar que la respuesta para ASP.NET AJAX y para jQuery ha sido la siguiente:

HTTP/1.1 200 OK

Server: ASP.NET Development Server/10.0.0.0

Date: Mon, 19 Dec 2011 10:34:01 GMT

X-AspNet-Version: 4.0.30319

Cache-Control: private, max-age=0

Content-Type: text/xml; charset=utf-8

Content-Length: 100

Connection: Close

<?xml version="1.0" encoding="utf-8"?><respuesta><saludo>Sergio León</saludo></respuesta>

 

En jQuery ha sido necesario cambiar dataType: “json” por dataType: “xml” para indicar que la respuesta esperada es Xml (aunque la llamada se sigue realizando en formato Json).

Lo cierto es que no veo mucha utilidad a la respuesta en Xml cuando estamos trabajando desde Javascript, pero es bueno saber que está ahí por si algún día es necesaria.

En cualquier caso, te dejo aquí un enlace donde muestran como trabajar con Xml desde Javascript.

Resumiendo que es gerundio:

  • Si nuestro cliente es ASP.NET AJAX o jQuery, por defecto tanto la llamada como la respuesta utilizarán JSON, pero podremos configurar el servicio para utilizar XML.
  • Si nuestro cliente es cualquier otro (aplicación de consola por ejemplo), tanto la llamada como la respuesta utilizará SOAP.

Por último, quiero hablar del famoso warning de tempuri.org. Si navegamos a tempuri.org podemos comprobar que existe y es un dominio de Microsoft que nos advierte de que cada servicio web necesita un único espacio de nombres para que las aplicaciones clientes puedan distinguir nuestros servicios en la web. Puedes ampliar más información en http://stackoverflow.com/questions/180985/what-is-tempuri-org pero está claro que la solución pasa por cambiar tempuri.org por algo como http://miaplicación.midominio.org.

Un saludo!

6 comentarios:

  1. Sergio, hay forma de definir el "codepage" en un archivo ASMX? Por ejemplo en el siguiente caso:

    <WebMethod(Description := "Suma dos numeros" ...

    Quiero poner "número" pero la u con tilde se representa mal cuando veo la página en el explorador.

    Gracias..

    ResponderEliminar
    Respuestas
    1. Ya me respondí!!! Es en el web.config con <globalization
      :)

      Eliminar
  2. el metodo jQuery no Funciona si esta fuera del Dominio, alguien sabe como?

    ResponderEliminar
  3. Sergio Una pregunta, se podria hacer lo mismo desde una página HTML5?

    ResponderEliminar
    Respuestas
    1. Puedes llamar el servicio web con ajax desde una pagina web html5 puesto que se esta llamando a un web service el cual es un archivo diferente y tiene la extension .asmx como en el ejemplo url: "/WebService1.asmx/Saludar", caso diferente es si tuvieras un [WebMethod] en mipagina.aspx.cs , el codigo javascript debe estar ubicado en la página mipagina.aspx

      Eliminar
  4. Este comentario ha sido eliminado por el autor.

    ResponderEliminar