Crear un conector WCF simple para DBTextFinder
WCF (Windows Communication Foundation) es un marco de trabajo para crear aplicaciones distribuidas cuyos componentes se comunican entre sí a través de servicios, utilizando una amplia gama de protocolos de red. En este artículo voy a comentar los fundamentos de la creación y configuración de uno de estos servicios a través de un conector de datos para la aplicación DBTextFinder, un programa para buscar textos en una base de datos que puede extenderse para conectar con cualquier sistema de gestión de datos.
En este artículo puedes descargar la aplicación DBTextFinder para buscar texto en bases de datos, además de encontrar información sobre su manejo básico.
En este otro artículo puedes descargar el código fuente de los ejemplos de conectores de DBTextFinder para diferentes bases de datos, SQL Server, Oracle y MySQL, así como un resumen de la estructura básica de estos conectores, que pueden ser tomados como base para desarrollar otros nuevos para acceder a datos procedentes de otros sistemas.
En este enlace puedes descargar el código fuente del proyecto DBTFWCFServiceSqlServer, escrito en CSharp con Visual Studio 2013.
En el presente artículo, utilizaré el conector para Sql Server como base para construir un servicio WCF hospedado en IIS (Internet Information Server), con el que la aplicación se podrá conectar con una base de datos Sql Server remota a través del protocolo http.
Componentes básicos de un servicio WCF
Para construir un servicio WCF, en primer lugar necesitamos un contrato de servicio, esto no es nada más que un interfaz decorado con una serie de atributos que lo hacen apto para definir el servicio. El interfaz que utiliza DBTextFinder es IDBTFConnection, definido en la biblioteca de clases DBTFCommons de la siguiente manera:
[ServiceContract(Namespace = " http://tempuri.org/IDBTFConnection",
SessionMode = SessionMode.Required,
CallbackContract = typeof(IDBTFSearchCallback))]
public interface IDBTFConnection
{
IDBTFSearchCallback Callback { get; set; }
[OperationContract(Name = "GetConnectionOptions",
Action = ".../GetConnectionOptions",
ReplyAction = ".../GetConnectionOptionsResponse")]
ConnectionOptions GetConnectionOptions();
[OperationContract(Name = "GetConnectionOptionsAsync",
Action = "…/GetConnectionOptionsAsync",
ReplyAction = ".../GetConnectionOptionsAsyncResponse")]
Task<ConnectionOptions> GetConnectionOptionsAsync();
[OperationContract(Name = "ParseConnectionString",
Action = ".../ParseConnectionString",
ReplyAction = ".../ParseConnectionStringResponse")]
ConnectionOptions ParseConnectionString(string strconn);
[OperationContract(Name = "ParseConnectionStringAsync",
Action = "…/ParseConnectionStringAsync",
ReplyAction = ".../ParseConnectionStringResponse")]
Task<ConnectionOptions> ParseConnectionStringAsync(string strconn);
[OperationContract(Name = "ValidateConnectionOptions",
Action = ".../ValidateConnectionOptions",
ReplyAction = ".../ValidateConnectionOptionsResponse")]
ConnectionOptions ValidateConnectionOptions(ConnectionOptions conntest);
[OperationContract(Name = "ValidateConnectionOptionsAsync",
Action = ".../ValidateConnectionOptionsAsync",
ReplyAction = ".../ValidateConnectionOptionsAsyncResponse")]
Task<ConnectionOptions> ValidateConnectionOptionsAsync(
ConnectionOptions conntest);
[OperationContract(Name = "GetSchemas",
Action = ".../GetSchemas",
ReplyAction = ".../GetSchemasResponse")]
string[] GetSchemas(ConnectionData connection);
[OperationContract(Name = "GetSchemasAsync",
Action = ".../GetSchemasAsync",
ReplyAction = ".../GetSchemasAsyncResponse")]
Task<string[]> GetSchemasAsync(ConnectionData connection);
[OperationContract(Name = "GetTables",
Action = ".../GetTables",
ReplyAction = ".../GetTablesResponse")]
string[] GetTables(ConnectionData connection, string schema);
[OperationContract(Name = "GetTablesAsync",
Action = ".../GetTablesAsync",
ReplyAction = ".../GetTablesAsyncResponse")]
Task<string[]> GetTablesAsync(ConnectionData connection, string schema);
[OperationContract(Name = "GetViews",
Action = ".../GetViews",
ReplyAction = ".../GetViewsResponse")]
string[] GetViews(ConnectionData connection, string schema);
[OperationContract(Name = "GetViewsAsync",
Action = ".../GetViewsAsync",
ReplyAction = ".../GetViewsAsyncResponse")]
Task<string[]> GetViewsAsync(ConnectionData connection, string schema);
[OperationContract(Name = "GetProcedures",
Action = ".../GetProcedures",
ReplyAction = ".../GetProceduresResponse")]
string[] GetProcedures(ConnectionData connection, string schema);
[OperationContract(Name = "GetProceduresAsync",
Action = ".../GetProceduresAsync",
ReplyAction = ".../GetProceduresAsyncResponse")]
Task<string[]> GetProceduresAsync(ConnectionData connection,
string schema);
[OperationContract(Name = "Search",
Action = ".../Search",
ReplyAction = ".../SearchResponse")]
ObjectResult[] Search(ConnectionData connection,
string tables,
string views,
string procedures,
string searchexpr,
bool ignorecase);
[OperationContract(Name = "SearchAsync",
Action = ".../SearchAsync",
ReplyAction = ".../SearchAsyncResponse")]
Task SearchAsync(ConnectionData connection,
string tables,
string views,
string procedures,
string searchexpr,
bool ignorecase);
[OperationContract(Name = "Replace",
Action = ".../Replace",
ReplyAction = ".../ReplaceResponse")]
[ServiceKnownType(typeof(TableResult))]
[ServiceKnownType(typeof(ProcResult))]
void Replace(ConnectionData connection,
ObjectResult result,
string searchexpr,
bool ignorecase);
[OperationContract(Name = "ReplaceAsync",
Action = ".../ReplaceAsync",
ReplyAction = ".../ReplaceAsyncResponse")]
[ServiceKnownType(typeof(TableResult))]
[ServiceKnownType(typeof(ProcResult))]
Task ReplaceAsync(ConnectionData connection,
ObjectResult result,
string searchexpr,
bool ignorecase);
[OperationContract(Name = "Delete",
Action = ".../Delete",
ReplyAction = ".../DeleteResponse")]
[ServiceKnownType(typeof(TableResult))]
[ServiceKnownType(typeof(ProcResult))]
void Delete(ConnectionData connection, ObjectResult result);
[OperationContract(Name = "DeleteAsync",
Action = ".../DeleteAsync",
ReplyAction = ".../DeleteAsyncResponse")]
[ServiceKnownType(typeof(TableResult))]
[ServiceKnownType(typeof(ProcResult))]
Task DeleteAsync(ConnectionData connection, ObjectResult result);
}
El interfaz se decora con el atributo ServiceContractAttribute, mientras que los miembros del interfaz que son operaciones del servicio, se deben decorar con el atributo OperationContractAttribute. Estos atributos no tienen ningún significado especial fuera del contexto de los servicios WCF, por lo que el interfaz puede ser utilizado como un interfaz normal en cualquier otro tipo de clases.
Podéis ver que en este contrato existen dos versiones para cada operación, una síncrona (la operación bloquea la ejecución del programa hasta que ha terminado de procesarse), y otra asíncrona (se ejecuta en paralelo con el programa, sin bloquearlo). Esto no es imprescindible hacerlo siempre, se trata de un mecanismo opcional, pero de este modo el ejemplo queda más completo.
En el atributo ServiceContract podéis observar que se configura la propiedad CallbackContract = typeof(IDBTFSearchCallback)), esto hace de este contrato un contrato dúplex. Lo normal es que sea la aplicación la que llama al servicio y este devuelve los resultados de la operación, pero con un contrato dúplex, el servicio también puede llamar a la aplicación. Esto lo utilizaré para la operación de búsqueda asíncrona SearchAsync, que es la que utiliza el programa DBTextFinder, para irle pasando al programa los resultados de la búsqueda, mientras el interfaz de usuario continúa respondiendo a los eventos.
Para completar un contrato dúplex, se debe definir un interfaz, que implementará la aplicación, para que el servicio la pueda llamar. En este caso es IDBTFSearchCallback, definido de la siguiente manera:
public interface IDBTFSearchCallback
{
bool SearchResult(ObjectResult result);
[OperationContract(Name = "SearchResultNC",
IsOneWay = true,
Action = "http://tempuri.org/IDBTFConnection/SearchResultNC")]
[ServiceKnownType(typeof(TableResult))]
[ServiceKnownType(typeof(ProcResult))]
void SearchResultNC(ObjectResult result);
}
El miembro SearcResult del interfaz no está decorado con OperationContractAttribute, esto es debido a que, en un contrato dúplex, las operaciones implicadas deben ser de una sola dirección, es decir, no deben devolver ningún valor. Sin embargo, DBTextFinder utiliza este método para permitir cancelar la búsqueda, devolviendo false. En el servicio WCF perdemos esta posibilidad, y debemos definir la operación SearchResultNC para usar en su lugar. Podéis ver que el atributo OperacionContract está configurado con la propiedad IsOneWay=true, que declara que esta llamada no espera que el servicio devuelva nada. De lo contrario, la aplicación se quedaría bloqueada esperando una respuesta que no llegará nunca.
En el código, para utilizar el callback en una determinada operación, se debe obtener de la siguiente manera:
Callback = OperationContext.Current.GetCallbackChannel<IDBTFSearchCallback>();
También es necesario decorar con atributos las clases que se utilizan para el intercambio de datos. Estas son las que utiliza este servicio, definidas en el espacio de nombres DBTFCommons.Data:
[DataContract(Name = "SearchScope",
Namespace = ".../DBTFCommons.Data")]
[Flags]
public enum SearchScope
{
[EnumMember]
Tables = 0x1,
[EnumMember]
StortedProcs = 0x2,
[EnumMember]
All = 0x3
}
[DataContract(Name = "ConnectionData",
Namespace = ".../DBTFCommons.Data")]
public class ConnectionData
{
[DataMember]
public string StringConnection { get; set; }
[DataMember]
public SearchScope Scope { get; set; }
}
[DataContract(Name = "ConnectionOptions",
Namespace = ".../DBTFCommons.Data")]
public class ConnectionOptions
{
[DataMember]
public bool SwowInitialCatalog { get; set; }
[DataMember]
public bool ShowWindowsAuthentication { get; set; }
[DataMember]
public string UserName { get; set; }
[DataMember]
public string Password { get; set; }
[DataMember]
public string DataSource { get; set; }
[DataMember]
public string InitialCatalog { get; set; }
[DataMember]
public bool WindowsAuthentication { get; set; }
[DataMember]
public string StringConnection { get; set; }
[DataMember]
public string ProviderName { get; set; }
[DataMember]
public string Error { get; set; }
[DataMember]
public string Description { get; set; }
}
[DataContract(Name = "FieldData",
Namespace = ".../DBTFCommons.Data")]
public class FieldData : IComparable<FieldData>,
IEquatable<FieldData>
{
[DataMember]
public string Name { get; set; }
[DataMember]
public string Value { get; set; }
[DataMember]
public bool Selected { get; set; }
[DataMember]
public string ReplaceEx { get; set; }
public int CompareTo(FieldData other);
public bool Equals(FieldData other);
}
[DataContract(Name = "ObjectResult",
Namespace = ".../DBTFCommons.Data")]
[KnownType(typeof(TableResult))]
[KnownType(typeof(ProcResult))]
public class ObjectResult
{
[DataMember]
public string Schema { get; set; }
[DataMember]
public string Name { get; set; }
[DataMember]
public bool Selected { get; set; }
}
[DataContract(Name = "TableResult",
Namespace = ".../DBTFCommons.Data")]
public class TableResult : ObjectResult
{
public TableResult();
public TableResult(IEnumerable<FieldData> text,
IEnumerable<FieldData> keys,
IEnumerable<FieldData> all);
[DataMember]
public FieldData[] TextFields { get; set; }
[DataMember]
public FieldData[] KeyFields { get; set; }
[DataMember]
public FieldData[] AllFields { get; set; }
public string QueryFields { get; }
public int SelectedCount { get; }
public FieldData GetField(string name);
public void SetValue(string name, string value);
public override string ToString();
}
[DataContract(Name = "ProcMatch",
Namespace = ".../DBTFCommons.Data")]
public class ProcMatch
{
[DataMember]
public int Index { get; set; }
[DataMember]
public int Length { get; set; }
[DataMember]
public bool Selected { get; set; }
[DataMember]
public string ReplaceEx { get; set; }
public override string ToString();
}
[DataContract(Name = "CodeType",
Namespace = ".../DBTFCommons.Data")]
public enum CodeType
{
[EnumMember]
View = 0x1,
[EnumMember]
Procedure = 0x2
}
[DataContract(Name = "ProcResult",
Namespace = ".../DBTFCommons.Data")]
public class ProcResult : ObjectResult
{
[DataMember]
public CodeType CodeType { get; set; }
[DataMember]
public string ProcedureCode { get; set; }
[DataMember]
public ProcMatch[] Matches { get; set; }
public int MatchCount { get; }
public int SelectedCount { get; }
public void AddMatch(ProcMatch pm);
public override string ToString();
}
Las clases, estructuras y enumeraciones se deben marcar con el atributo DataContractAttribute. Se trata de un atributo similar a SerializableAttribute, que permite que los objetos sean serializados por el canal de comunicaciones con el servicio.
Los miembros de datos que se van a serializar, se deben decorar con el atributo DataMemberAttribute. Los miembros no decorados, simplemente no se serializan. En las enumeraciones, se utiliza el atributo EnumMemberAttribute para decorar los miembros para su serialización.
Nos queda el atributo KnownTypeAttribute. Para devolver los resultados de las búsquedas existe la clase básica genérica ObjectResult, de la cual derivan TableResult y ProcResult. Para que el sistema sepa cómo debe serializar y deserializar los resultados ObjectResult, se deben listar con este atributo todas las posibles variedades de la clase.
Una vez definidos el contrato de servicio y los contratos de datos, podemos crear un servicio WCF de diferentes maneras, dependiendo de cómo tengamos pensado hospedarlo. Podemos utilizar una aplicación de consola o de Windows Forms, utilizar WAS para activarlo (Windows Activation Services), o incluso un servicio de Windows, además de Windows Azure, para construir un host para el servicio. También podemos elegir, como en este caso, hospedar el servicio como una aplicación web, usando el protocolo http o https, en un servidor como IIS. Este es el modelo elegido para este servicio, para lo cual, se puede crear un nuevo proyecto Aplicación de servicio WCF en la carpeta WCF de proyectos de Visual Studio.
Como ya tenemos definido el interfaz del contrato de servicio en la librería DBTFCommons, podemos eliminar el interfaz por defecto que se crea al generar el proyecto, de manera que solo necesitamos quedarnos con la clase donde debemos implementar el servicio, en este caso SqlServerService.svc. Para implementar esta clase, dado que ya tengo implementado este interfaz en la clase DBTFSqlServerConnection, lo único que hay que hacer es un trabajo de copiar y pegar, realizando algún cambio menor para adaptar el funcionamiento al contexto de un servicio WCF. Básicamente en este caso, implementar sus limitaciones.
Configuración del servicio
Una vez creado el servicio, es necesario configurarlo adecuadamente para poder utilizarlo. La configuración se puede hacer desde el código del servicio, pero la forma más flexible es hacerlo en el archivo Web.config de la aplicación web.
La configuración del servicio se realiza dentro del elemento system.serviceModel. El primer elemento que debemos configurar es el tipo de enlace. En este caso, necesitamos un enlace http apropiado para un contrato de tipo dúplex, por lo que seleccionamos el tipo de enlace wsDualHttpBinding. Disponemos además de otros tipos de enlace, como basicHttpBinding y wsHttpBinding, para conexiones http o https, NetTcpBinding para conexiones de red locales tcp, netNamedPipeBinding para conexiones entre procesos en el mismo equipo, o incluso podemos definir conexiones a medida con el elemento customBinding.
En el elemento bindings definimos los enlaces disponibles para nuestro servicio:
<bindings>
<wsDualHttpBinding>
<binding name="wsDuplex"
maxReceivedMessageSize="10000000"
sendTimeout="00:30:00">
<security mode="None" />
</binding>
</wsDualHttpBinding>
<mexHttpBinding>
<binding name="mexBinding" />
</mexHttpBinding>
</bindings>
Donde hemos creado una configuración para el tipo de enlace wsDualHttpBinding, llamada wsDuplex, que permite mensajes de hasta 1000000 de bytes y con un timeout de 30 minutos.
En este ejemplo, he anulado por completo la seguridad con el elemento security=”None”. Normalmente, habrá que establecer un mecanismo de seguridad apropiado para la conexión.
mexHttpBinding es el enlace utilzado para obtener los metadatos del servicio. Estos metadatos se utilizan para que las aplicaciones que permiten realizar conexiones con el servicio obtengan la información necesaria para crear los clientes.
Los servicios que contiene la aplicación se definen en la sección services:
<services>
<service name="DBTFWCFServiceSqlServer.SqlServerService">
<endpoint address="http://{Server Uri}/SqlServerService.svc"
binding="wsDualHttpBinding"
bindingConfiguration="wsDuplex"
contract="DBTFCommons.Interfaces.IDBTFConnection" />
<endpoint address="http://{Server Uri}/mex"
binding="mexHttpBinding"
bindingConfiguration="mexBinding"
contract="IMetadataExchange" />
</service>
</services>
El elemento endpoint define la dirección de conexión con el servicio. Existe un endpoint para conectar con el servicio mismo, y otro para obtener los metadatos que definen el servicio. En el elemento endpoint se define el enlace y su configuración que se va a utilizar para la conexión y el contrato de servicio. También se define la url con la que se realiza el enlace con el servicio. En este ejemplo, se debe sustituir {Server Uri} por la dirección apropiada para acceder al servidor IIS en tu caso concreto.
La instalación del servicio se realiza con el administrador de IIS. Crea un directorio en tu servidor y copia en él los archivos SqlServerService.svc, Web.config y el directorio bin creados después de generar la solución. Luego puedes crear una aplicación web en IIS como un sitio independiente o un directorio virtual de un sitio preexistente, como el Default Website.
Configuración del cliente
Para que DBTextFinder se conecte con el servicio, es necesario configurar la conexión en el archivo DBTextFinder.exe.Config. Esta configuración es muy similar a la del servidor.
<system.serviceModel>
<bindings>
<wsDualHttpBinding>
<binding name="Duplex">
<security mode="None" />
</binding>
</wsDualHttpBinding>
</bindings>
<client>
<endpoint address="http://{ServerUri}/SqlServerService.svc"
binding="wsDualHttpBinding"
bindingConfiguration="Duplex"
contract="DBTFCommons.Interfaces.IDBTFConnection"
name="{Data Source name}" />
</client>
</system.serviceModel>
En la sección client se definen los enlaces de cliente con los servidores. El atributo name del elemento endpoint es importante, y debe ser igual que el nombre de la instancia de base de datos. Puede ser la IP del servidor o el nombre de la instancia de Sql Server.
Para configurar una conexión desde la aplicación, se debe seleccionar el tipo de conector Generic WCF connector, y poner en el cuadro de texto Servidor el nombre del endpoint de nuestro servicio, que debe coincidir con el nombre del servidor de base de datos.
Y con esto ya deberías poder conectar con la aplicación DBTextFinder con una base de datos remota a través de un servicio WCF utilizando el protocolo http.