Introducción
En este artículo te voy a explicar cómo crear un
servicio Web usando un editor de textos.
Veremos "paso a paso" el código que tenemos que escribir. También veremos
cómo compilar desde la línea de comandos y cómo podemos crear un cliente que
use este servicio Web, todo por medio de la línea de comandos.
¿Por qué usar un editor de textos en lugar de Visual Studio .NET?
No es un capricho. En principio vamos a crear el servicio Web con un editor de
textos para "saltarnos" los extras que el Visual Studio .NET añade a un
proyecto del tipo Servicio Web, ya que, al menos desde mi punto de vista, le
añade cosas de más... que realmente no nos son necesarias.
¿Qué hace Visual Studio que no es necesario?
Cuando creamos un nuevo proyecto del tipo Servicio Web en Visual Studio, éste
crea el servicio Web en nuestro servidor local (localhost), pero lo hace
creando un "sitio Web", es decir, el servicio Web lo crea en algo parecido a
lo que sería un sitio Web "completo", para que lo entiendas, este sitio Web en
el que estás leyendo este artículo sería el equivalente al sitio Web que
Visual Studio crea para cualquier tipo de proyecto ASP.NET, por tanto, creo
que es más interesante que no cree algo que al final acabe de confundirnos, ya
que lo que nos interesa es crear el servicio Web para después usarlo en el
sitio que tengamos en Internet.
¿Perderemos funcionalidad al hacerlo "a mano"?
No. Porque el hecho de que usemos un editor de textos no nos impedirá que
hagamos lo mismo que hace Visual Studio, es decir, vamos a crear el servicio
Web usando un ensamblado (DLL) con el código que dicho servicio Web usará, que
es al fin y al cabo lo que hace Visual Studio.
Aunque también te explicaré cómo hacerlo más fácil, es decir, sin usar un
ensamblado DLL. En ese caso todo el código lo incluiremos en el fichero que
finalmente publicaremos en nuestro sitio de Internet.
Crear un servicio Web en un solo fichero
Vamos a empezar viendo cómo crear un servicio Web que incluye todo lo que
necesita en un mismo fichero, después "desglosaremos" el contenido del servicio
Web para que podamos
crear un ensamblado con el código.
Nota:
El servicio Web que vamos a crear usará la base de datos Northwind. El
servicio Web recibirá como parámetro una cadena de selección con los datos que
queremos (la típica cadena SELECT) y devolverá un DataSet con los datos
solicitados.
Lo primero que haremos es crear un fichero con la extensión .asmx, esta es la
extensión usada para los servicios Web, en nuestro ejemplo, el fichero se va a
llamar: Northwind.asmx
En este fichero vamos a crear una clase llamada Northwind con un solo método:
Empleados.
Este método recibirá un parámetro del tipo String en el que podemos indicar la
cadena de selección o bien podemos usar una cadena vacía, en cuyo caso se
usará una cadena de selección que devolverá algunos campos de la tabla
Employees. El valor devuelto por esa función será un objeto DataSet con los
datos indicados en la cadena de selección.
Nota:
Un servicio Web realmente lo que contiene es una clase con el código que
usaremos para darle "funcionalidad", por tanto, siempre debemos crear una
clase con cada servicio Web. Aunque el código del servicio Web puede contener
más de una clase, solamente una es la que se expone como parte a usar del
servicio Web.
Para que .NET se entere de que lo que contiene el fichero es un servicio Web
debemos usar una directiva de ASP.NET:
<%@ WebService
Con esto le indicamos que lo que contiene el fichero es un servicio Web, ya
que no solo basta que la extensión sea .asmx, aunque si no tiene esa extensión
no lo reconocerá como servicio Web.
A continuación, en la misma directiva ASP.NET le decimos el lenguaje de
programación que vamos a usar, en el caso de VB indicaremos:
Language = "VB"
Si vamos a usar C#, sustituimos VB por C#:
Language = "C#"
Por último tenemos que indicar el nombre de la clase en la que escribiremos
el código que utilizará este servicio Web:
Class = "NorthwindSW"
Y cerramos la directiva de ASP.NET:
%>
A partir de este momento, lo que debemos escribir es el código en el
lenguaje indicado para definir la clase y los métodos que el servicio Web
expondrá públicamente. Esos métodos serán los que podremos usar en una
aplicación cliente.
Como vamos a acceder a una base de datos de SQL Server, añadiremos los
espacios de nombres en los que están definidas las clases que vamos a usar,
para Visual Basic:
Imports System.Data
Imports System.Data.SqlClient
Para C#:
using System.Data;
using System.Data.SqlClient;
Como te comentaba antes, cuando definimos un servicio Web realmente estamos
definiendo una clase y dentro de esa clase indicaremos que métodos (funciones)
queremos exponer, para exponer esas funciones como parte del servicio Web,
tendremos que usar unos atributos que están definidos en el espacio de nombres
System.Web.Services, por tanto, para ahorrarnos la escritura de algunos
caracteres en nuestro código, también incluiremos una importación de ese
espacio de nombres:
Imports System.Web.Services ' para VB
using System.Web.Services; // para C#
Nota:
Como ya sabrás, (si no lo sabes te lo recuerdo yo), las importaciones de
espacios de nombres sirve para ahorrarnos escribir el nombre completo de una
clase. Ese nombre completo está formado por el espacio de nombres y el nombre
de la clase. Es como si incluyéramos un directorio en el PATH del sistema, así
podremos acceder a las clases que estén en ese "directorio/espacio de
nombres", sin necesidad de tener que estar repitiendo el sitio donde están las
clases que queremos usar.
Por ejemplo, para acceder a la clase DataSet, si tenemos la importación de
System.Data podemos usarla de forma directa: DataSet, o también usando el
nombre completo: System.Data.DataSet.
Una vez indicadas las importaciones de espacios de nombres, definimos la
clase, a la que le vamos a aplicar el atributo WebServiceAttribute para
indicar el espacio de nombres del servicio Web (recomendable al 100%) y una
descripción de lo que hace este servicio Web, con idea de que si alguien lo
"localiza", sepa para que sirve.
Las clases que definen un servicio Web deben estar derivadas de
System.Web.Services.WebService, por tanto debemos indicarlo también:
Para Visual Basic:
<WebService(Namespace:="http://elGuille/ServiciosWeb/", _
Description:="Acceso a la base de datos Northwind (local) desde un servicio Web")> _
Public Class NorthwindSW
Inherits System.Web.Services.WebService
Para C#:
[WebService(Namespace="http://elGuille/ServiciosWeb/",
Description="Acceso a la base de datos Northwind (local) desde un servicio Web")]
public class NorthwindSW : System.Web.Services.WebService
{
Nota:
A partir de aquí, te dejo adivinar cual es el código para Visual Basic .NET y
cual es para C#. Así que, si ves dos líneas o trozos de código que son
parecidos... usa el que tengas que usar dependiendo del lenguaje que hayas
elegido.
Una pista: El código de VB no termina en punto y coma (;) ni usa llaves ({ o
})
Si no te aclaras, no te preocupes, al final te muestro todo el código al
completo, el ir mostrando el código en los dos lenguajes es para que veas que
es prácticamente el mismo en VB que en C#, salvo por las diferencias
sintácticas.
Para acceder a la tabla de la base de datos usaremos un DataAdapter de SQL:
Private da As SqlDataAdapter
private SqlDataAdapter da;
Ahora definimos la función (o método) que se expondrá en el servicio Web, a
estas funciones "expuestas" se las llama métodos Web y debemos aplicarle el
atributo WebMethodAttribute, al que también le podemos añadir una
descripción para que sepamos que es lo que hace. El único método Web que
tendrá nuestra clase, será uno llamado Empleados el cual recibe una cadena
como parámetro, y devuelve un objeto DataSet con los datos que hayamos
obtenido al usar esa cadena como cadena de selección:
<WebMethod(Description:="Devuelve datos indicados en el parámetro de la base Northwind")> _
Public Function Empleados(sel As String) As DataSet
[WebMethod(Description="Devuelve datos indicados en el parámetro de la base Northwind")]
public DataSet Empleados(string sel)
{
Si el parámetro es una cadena vacía, usaremos una selección predeterminada
para extraer los datos de la tabla Employees, aunque en la cadena de selección
que pases por parámetro puedes usar la tabla de Northwind que quieras:
If sel = "" Then
sel = "SELECT LastName, FirstName, Title, BirthDate FROM Employees"
End If
if( sel == "" )
sel = "SELECT LastName, FirstName, Title, BirthDate FROM Employees";
Ahora creamos el objeto DataAdapter usando esta cadena de selección y las
instrucciones necesarias para que se conecte con el servidor de SQL Server y
la base de datos Northwind, en este ejemplo se utiliza la autenticación de
Windows:
da = New SqlDataAdapter(sel, _
"integrated security=true; data source=(local); initial catalog=Northwind")
da = new SqlDataAdapter(sel,
"integrated security=true; data source=(local); initial catalog=Northwind");
Si queremos usar un nombre de usuario y un password, también podríamos
hacerlo:
da = New SqlDataAdapter(sel, _
"user id=usuario; password=clave; data source=(local); initial catalog=Northwind")
da = new SqlDataAdapter(sel, _
"user id=usuario; password=clave; data source=(local); initial catalog=Northwind");
Nota:
En este sitio:
http://www.connectionstrings.com/ puedes encontrar ejemplos de cadenas de
conexión a distintos tipos de bases de datos, como SQL Server, Access, Oracle,
MySQL, etc., además usando diferentes "proveedores": OleDb, Odbc, etc.
Para conservar los datos, vamos a usar un DataSet, por tanto tendremos que
crear un nuevo objeto de este tipo para después usarlo con el DataAdapter:
Dim ds As New DataSet()
DataSet ds = new DataSet();
Ahora tenemos que "llenar" el DataSet con los datos solicitados, para ello
utilizamos el método Fill del DataAdapter. Como no nos fiamos de que todo vaya
a funcionar bien, y para no provocar un desastre mayor, es recomendable a la
hora de usar ese método, hacerlo dentro de un bloque Try/Catch para poder
interceptar los posibles errores que se produzcan:
Try
da.Fill(ds)
Catch ex As Exception
Throw ex
End Try
try
{
da.Fill(ds);
}
catch(Exception ex)
{
throw ex;
}
Como puedes comprobar, si se produce una excepción, se la devolvemos al
cliente que use nuestro servicio Web, así tendrá más cuidado a la hora de usar
las cadenas de selección.
Por último devolvemos el DataSet con los datos:
Return ds
End Function
End Class
return ds;
}
}
Algunas comprobaciones extras para que no nos "la cuelen"
Cuando estemos trabajando con cadenas de SQL, siempre deberíamos tener
cuidado de que no se pasen valores incorrectos e incluso "peligrosos".
En este ejemplo, sólo deberíamos admitir sentencias SELECT, por tanto haremos
algunas comprobaciones de que la cadena pasada como argumento no tenga código
malicioso.
Después del IF de que la cadena está vacía, añade el siguiente código:
' Comprobar que están indicando valores correctos (o casi)
'
' Que no sea una cadena vacía
If sel = "" Then
Throw New ArgumentException("La cadena no puede ser nula")
End If
' Comprobar que realmente se use SELECT,
If sel.ToUpper().IndexOf("SELECT") = -1 Then
Throw New ArgumentException("La cadena debe ser SELECT <campos> FROM <tabla>")
End If
' no permitir comentarios ni algunas instrucciones maliciosas
If sel.IndexOf("--") > -1 Then
Throw New ArgumentException("No se admiten comentarios de SQL en la cadena de selección")
End If
If sel.ToUpper().IndexOf("DROP") > -1 Then
Throw New ArgumentException("La cadena debe ser SELECT <campos> FROM <tabla>, no DROP y otros comandos no adecuados...")
End If
// Comprobar que están indicando valores correctos (o casi)
//
// Que no sea una cadena vacía
if( sel == "" )
throw new ArgumentException("La cadena no puede ser nula");
//
// Comprobar que realmente se use SELECT,
if( sel.ToUpper().IndexOf("SELECT") == -1 )
throw new ArgumentException("La cadena debe ser SELECT <campos> FROM <tabla>");
//
// no permitir comentarios ni algunas instrucciones maliciosas
if( sel.IndexOf("--") > -1 )
throw new ArgumentException("No se admiten comentarios de SQL en la cadena de selección");
//
if( sel.ToUpper().IndexOf("DROP") > -1 )
throw new ArgumentException("La cadena debe ser SELECT <campos> FROM <tabla>, no DROP y otros comandos no adecuados...");
Guarda el fichero, y cópialo en el directorio del servidor local (localhost)
el cual estará en C:\Inetpub\wwwroot
Ahora solo queda hacer un cliente que consuma el servicio Web, además de
crear una clase que permita a ese cliente usarlo.
Crear una clase "proxy" para acceder al servicio Web
Una vez que tenemos el servicio Web, debemos crear una clase para usarla en
la aplicación cliente. La clase la vamos a generar automáticamente con la
utilidad WSDL.exe.
Esta clase servirá para que el compilador sepa dónde está el servicio Web y
qué funciones tiene, con idea de que podamos usar la clase como una clase
normal y corriente.
Nota:
Si tienes instalado Visual Studio .NET, tendrás un acceso directo a
"Símbolo del sistema de Visual Studio .NET 2003" que te carga las variables de
entorno necesarias para usar las herramientas de .NET.
Si solo tienes el SDK de .NET, en el directorio bin del SDK habrá un fichero
.bat llamado sdkvars.bat que carga los valores adecuados para usar desde la
línea de comandos, crea un acceso directo que contenga esta instrucción:
%comspec% /k "<directorio del SDK>\sdkvars.bat"
Y úsalo para compilar y ejecutar las herramientas del SDK.
Abre una ventana de MSDOS con el path a las herramientas de .NET Framework
y escribe lo siguiente:
Para Visual Basic:
WSDL /l:vb http://localhost/Northwind.asmx
Para C#:
WSDL http://localhost/Northwind.asmx
Esto creará una clase llamada de la misma forma que el nombre de la clase
definida en el servicio Web: NorthwindSW.vb o NorthwindSW.cs, según el
lenguaje indicado.
A esta clase me referiré como la clase "proxy". Que es la que se usará para
acceder al servicio Web.
Continuará...
Bueno, vamos a dejar el tema por ahora... Al final te muestro el código de
un cliente de consola que utiliza este servicio Web y las instrucciones para
compilarlo desde la línea de comandos.
Nos vemos.
Guillermo
Código
para Visual Basic
El código completo para Visual Basic .NET, tanto del servicio Web como de
la aplicación cliente para usarlo.
El servicio Web:
<%@ WebService Language="VB" Class="NorthwindSW" %>
'------------------------------------------------------------------------------
' Servicio Web para acceder a una tabla de Northwind (15/Mar/05)
'
' ©Guillermo 'guille' Som, 2005
'
' Para crear la clase "proxy":
' WSDL /l:vb http://localhost/Northwind.asmx
'------------------------------------------------------------------------------
Imports System
Imports System.Data
Imports System.Data.SqlClient
Imports System.Web.Services
<WebService(Namespace:="http:"//elGuille/ServiciosWeb/", _
Description:="Acceso a la base de datos Northwind (local) desde un servicio Web")> _
Public Class NorthwindSW_vb
Inherits System.Web.Services.WebService
'
Private da As SqlDataAdapter
'
<WebMethod(Description:="Devuelve datos indicados en el parámetro de la base Northwind")> _
Public Function Empleados(sel As String) As DataSet
'
If sel = "" Then
sel = "SELECT LastName, FirstName, Title, BirthDate FROM Employees"
End If
'
' Comprobar que están indicando valores correctos (o casi)
'
' Que no sea una cadena vacía
If sel = "" Then
Throw New ArgumentException("La cadena no puede ser nula")
End If
' Comprobar que realmente se use SELECT,
If sel.ToUpper().IndexOf("SELECT") = -1 Then
Throw New ArgumentException("La cadena debe ser SELECT <campos> FROM <tabla>")
End If
' no permitir comentarios ni algunas instrucciones maliciosas
If sel.IndexOf("--") > -1 Then
Throw New ArgumentException("No se admiten comentarios de SQL en la cadena de selección")
End If
If sel.ToUpper().IndexOf("DROP") > -1 Then
Throw New ArgumentException("La cadena debe ser SELECT <campos> FROM <tabla>, no DROP y otros comandos no adecuados...")
End If
'
da = New SqlDataAdapter(sel, _
"integrated security=true; data source=(local); initial catalog=Northwind")
'
Dim ds As New DataSet()
'
Try
da.Fill(ds)
Catch ex As Exception
Throw ex
End Try
'
Return ds
End Function
End Class
La aplicación cliente (de consola)
Para compilar esta
aplicación, necesitamos la clase proxy generada con WSDL.
La línea de comando para compilar es:
vbc ClienteNorthwindSW.vb NorthwindSW.vb /r:System.dll /r:System.Data.dll /r:System.Web.Services.dll /r:System.Xml.dll
'------------------------------------------------------------------------------
' ClienteNorthwindSW (05/Abr/05)
'
' ©Guillermo 'guille' Som, 2005
'
' Para crear el EXE:
' vbc ClienteNorthwindSW.vb NorthwindSW.vb /r:System.dll /r:System.Data.dll /r:System.Web.Services.dll /r:System.Xml.dll
'------------------------------------------------------------------------------
Option Strict On
Option Explicit On
Imports Microsoft.VisualBasic
Imports System
Imports System.Data
Public Class ClienteNorthwindSW
'
<STAThread> _
Shared Sub Main(args As String())
' punto de entrada del ejecutable
Dim sw As New NorthwindSW
Dim sel As String = "SELECT LastName, FirstName, Title, BirthDate FROM Employees"
'
' Si se ha indicado algún parámetro en la línea de comandos
' será la cadena de selección
If args.Length = 1 Then
' Si hay uno, se habrá indicado entre comillas dobles
sel = args(0)
ElseIf args.Length > 1 Then
' Se habrá indicado sin usar comillas dobles
' unir todas las cadenas en una sola
sel = String.Join(" ", args).Trim()
End If
'
Dim ds As DataSet
Try
ds = sw.Empleados(sel)
Catch ex As Exception
Console.WriteLine("ERROR: " & ex.Message)
Return
End Try
'
' Para ajustar el texto mostrado
Const mostrar As Integer = 21
Dim sb As New System.Text.StringBuilder
'
' Mostrar las cabeceras
For Each columna As DataColumn In ds.Tables(0).Columns
If columna.DataType.ToString = "System.DateTime" Then
Console.Write("{0} ", ajustar(columna.ColumnName, 10))
sb.AppendFormat("{0} ", New String("-"c, 10) )
Else
Console.Write("{0} ", ajustar(columna.ColumnName, mostrar))
sb.AppendFormat("{0} ", New String("-"c, mostrar) )
End If
Next
Console.WriteLine()
Console.WriteLine(sb.ToString)
' Mostrar los datos
For Each fila As DataRow In ds.Tables(0).Rows
For Each columna As DataColumn In ds.Tables(0).Columns
' Si es una fecha, usar un formato especial
If TypeOf fila(columna) Is DateTime Then
Console.Write("{0} ", CType(fila(columna), DateTime).ToString("dd/MM/yyyy"))
Else
Console.Write("{0} ", ajustar(fila(columna).ToString, mostrar))
End If
Next
Console.WriteLine()
Next
End Sub
' Ajustar la cadena al ancho indicado
Private Shared Function ajustar(cadena As String, ancho As Integer) As String
Return ( cadena & New String(" "c, ancho) ).Substring(0, ancho)
End Function
End Class
Código
para C#
El código completo para C#, tanto del servicio Web como de la aplicación
cliente para usarlo.
El servicio Web:
<%@ WebService Language="C#" Class="NorthwindSW" %>
//-----------------------------------------------------------------------------
// Servicio Web para acceder a una tabla de Northwind (15/Mar/05)
//
// (c)Guillermo 'guille' Som, 2005
//
//-----------------------------------------------------------------------------
using System;
using System.Data;
using System.Data.SqlClient;
using System.Web.Services;
[WebService(Namespace="http:"//elGuille/ServiciosWeb/",
Description="Acceso a la base de datos Northwind (local) desde un servicio Web")]
public class NorthwindSW : System.Web.Services.WebService
{
//
private SqlDataAdapter da;
//
[WebMethod(Description="Devuelve datos indicados en el parámetro de la base Northwind")]
public DataSet Empleados(string sel)
{
//
if( sel == "" )
sel = "SELECT LastName, FirstName, Title, BirthDate FROM Employees";
//
// Comprobar que están indicando valores correctos (o casi)
//
// Que no sea una cadena vacía
if( sel == "" )
throw new ArgumentException("La cadena no puede ser nula");
//
// Comprobar que realmente se use SELECT,
if( sel.ToUpper().IndexOf("SELECT") == -1 )
throw new ArgumentException("La cadena debe ser SELECT <campos> FROM <tabla>");
//
// no permitir comentarios ni algunas instrucciones maliciosas
if( sel.IndexOf("--") > -1 )
throw new ArgumentException("No se admiten comentarios de SQL en la cadena de selección");
//
if( sel.ToUpper().IndexOf("DROP") > -1 )
throw new ArgumentException("La cadena debe ser SELECT <campos> FROM <tabla>, no DROP y otros comandos no adecuados...");
//
da = new SqlDataAdapter(sel,
"integrated security=true; data source=(local); initial catalog=Northwind");
//
DataSet ds = new DataSet();
try
{
da.Fill(ds);
}
catch(Exception ex)
{
throw ex;
}
return ds;
}
}
La aplicación cliente (de consola)
Para compilar esta
aplicación, necesitamos la clase proxy generada con WSDL.
La línea de comando para compilar es:
csc ClienteNorthwindSW.cs NorthwindSW.cs /r:System.dll /r:System.Data.dll /r:System.Web.Services.dll /r:System.Xml.dll
//------------------------------------------------------------------------------
// ClienteNorthwindSW (05/Abr/05)
//
// ©Guillermo 'guille' Som, 2005
//
// Para crear el EXE:
// csc ClienteNorthwindSW.cs NorthwindSW.cs /r:System.dll /r:System.Data.dll /r:System.Web.Services.dll /r:System.Xml.dll
//------------------------------------------------------------------------------
using System;
using System.Data;
public class ClienteNorthwindSW{
//
[STAThread]
static void Main(string[] args) {
// punto de entrada del ejecutable
NorthwindSW sw = new NorthwindSW();
string sel = "SELECT LastName, FirstName, Title, BirthDate FROM Employees";
//
// Si se ha indicado algún parámetro en la línea de comandos
// será la cadena de selección
if( args.Length == 1 ){
// Si hay uno, se habrá indicado entre comillas dobles
sel = args[0];
}else if( args.Length > 1 ){
// Se habrá indicado sin usar comillas dobles
// unir todas las cadenas en una sola
sel = String.Join(" ", args).Trim();
}
//
DataSet ds;
try{
ds = sw.Empleados(sel);
}catch(Exception ex){
Console.WriteLine("ERROR: " + ex.Message);
return;
}
//
// Para ajustar el texto mostrado
const int mostrar = 21;
System.Text.StringBuilder sb = new System.Text.StringBuilder();
//
// Mostrar las cabeceras
foreach(DataColumn columna in ds.Tables[0].Columns){
if( columna.DataType.ToString() == "System.DateTime" ){
Console.Write("{0} ", ajustar(columna.ColumnName, 10));
sb.AppendFormat("{0} ", new string('-', 10) );
}else{
Console.Write("{0} ", ajustar(columna.ColumnName, mostrar));
sb.AppendFormat("{0} ", new string('-', mostrar) );
}
}
Console.WriteLine();
Console.WriteLine(sb.ToString());
// Mostrar los datos
foreach(DataRow fila in ds.Tables[0].Rows){
foreach(DataColumn columna in ds.Tables[0].Columns){
// Si es una fecha, usar un formato especial
if( fila[columna] is DateTime ){
Console.Write("{0} ", ( (DateTime)fila[columna] ).ToString("dd/MM/yyyy"));
}else{
Console.Write("{0} ", ajustar(fila[columna].ToString(), mostrar));
}
}
Console.WriteLine();
}
}
// Ajustar la cadena al ancho indicado
private static string ajustar(string cadena, int ancho) {
return ( cadena + new string(' ', ancho) ).Substring(0, ancho);
}
}