WCF: compartición de tipos entre referencias a servicios

En una aplicación basada en capas, una situación que se puede dar de forma frecuente es que nuestra capa de servicios exponga dos o más servicios que hacen uso de un mismo tipo. Es decir, por poner un ejemplo simple, en una aplicación WCF, tendríamos dos contratos definidos con [OperationContract] en los que algunos métodos harían uso de un mismo tipo expuesto mediante [DataContract]. En principio, esto no constituye en sí mismo ningún problema, hasta que estemos codificando un cliente que referencie a los dos servicios; en ese caso en el cliente acabaremos teniendo dos referencias a servicio que contienen el mismo tipo. El problema es que aunque nosotros sepamos que es el mismo tipo, en el cliente tenemos dos tipos distintos y no podemos trabajar como si fuesen el mismo, hacer asignaciones de instancias, etc. Esto puede ser un mal menor, o puede suponernos grandes quebraderos de cabeza, dependiendo de cómo tengamos que utilizar esos tipos en el cliente, pero en cualquier caso hace que nuestro modelo en los clientes sea conceptualmente incorrecto y resulte confuso a la hora de utilizarlo desde el código. En este artículo voy a comentar cómo podemos tratar con este problema.


Antes de empezar quisiera destacar que en ocasiones el esfuerzo empleado en evitar la multiplicidad de tipos en las referencias, será mayor que el beneficio obtenido. Si el manejo de los tipos en cliente no es demasiado complejo, seguramente merecerá la pena quedarnos como estamos, que añadir más código y tareas de mantenimiento a nuestro proyecto. Probablemente éste sea el caso si tenemos clientes ligeros que acceden a la lógica de negocio a través de la capa de servicios, pero lo más recomendable es valorar cada caso antes de tomar una decisión.


Unificando los tipos en el cliente: librería de tipos


La forma de evitar la multiplicidad de tipos en las referencias del cliente, es tener en el proyecto del cliente una referencia a una librería que contenga dichos tipos, y configurar la referencia para que utilice dicha librería en lugar de usar los tipos expuestos en el servicio. En un momento hablaré de las alternativas que tenemos para construir esta librería, pero suponiendo que ya dispongamos de ella, sólo nos quedaría añadir una referencia al ensamblado desde nuestro cliente, y a la hora de añadir la referencia a cada servicio, indicar que de reutilicen los tipos presentes en un ensamblado. Para ello se usa la opción que aparece al pulsar el botón “Advanced” en la pantalla de adición de referencia a servicio:


 


referenciaServicio


 referenciaServicioConfig


Como se puede apreciar en la captura de pantalla, estamos indicando que para la referencia al servicio se reutilice el tipo “Person” que está contenido en un ensamblado referenciado por el proyecto. De este modo en el código del proxy no aparecerá la definición de este tipo, y en el código del cliente podremos utilizar un sólo tipo “Person” (el contenido en el ensamblado que tenemos referenciado).


 


Generación de la librería de tipos


Para generar la librería de tipos tenemos principalmente dos alternativas: codificarla manualmente o generarla usando svcutil. En ambos casos se suele partir de unos tipos ya presentes en el modelo de la aplicación, de los cuales vamos a crear una versión distribuible a los clientes.


 


Codificación manual de la librería de tipos: La opción más inmediata consiste en codificar una librería de tipos en la que tengamos los tipos utilizados por los servicios. Esta libréría se podría referenciar tanto en los servicios como en los clientes, teniendo en cuenta que para hacerlo en los servicios tendremos que decorarla con los atributos [DataContract] y [DataMember] en los lugares necesarios. Es necesario hacer algunas puntualizaciones para esta opción:



  • Tenemos que tener presente al codificar los tipos que no deberíamos incluir código correspondiente a nuestra lógica de negocio, ya que la librería va a ser referenciada en los clientes. Es decir, deberíamos ceñirnos al “tenet” de SOA que indica que debemos compartir contratos en lugar de código. Si nuestros tipos originales contienen métodos correspondientes a lógica de negocio, podemos apoyarnos por ejemplo en interfaces y/o en el patrón proxy [GoF] para la codificación de estos tipos destinados a ser distribuidos.

  • Si por lo explicado en el punto precedente, nos vemos obligados a mantener manualmente dos “copias” de parte del código de nuestros tipos, estaremos perjudicando la mantenibilidad del proyecto (por eso es necesario valorar si merece la pena el esfuerzo de solucionar la repetición de tipos en las referencias a servicios). Hay ocasiones en las que no es tan fácil mantener actualizado el tipo distribuible a partir del tipo original, un ejemplo podría ser si los tipos a distribuir están contenidos en un modelo EDM de Entity Framework, el cual ya tiene dificultades de mantenimiento propias (por otra parte además las entidades del EDM ya contendrían los atributos correspondientes a los contratos de datos).

  • Sin embargo, si los tipos originales no contienen demasiada lógica, y los servicios y clientes son desarrollados por un mismo equipo, ésta puede ser una forma muy cómoda de distribuir nuestros tipos y de que en todo momento (a través del control de código fuente) todos los miembros de nuestro equipo tengan la última versión de los contratos de datos, sin necesidad de actualizar las referencias a los servicios.

 


Generación de la librería de tipos usando svcutil: mediante la herramienta svcutil podemos generar fácilmente el código de los tipos, sin necesidad de escribirlo a mano. Para esta opción se aplica por lo tanto todo lo dicho unas líneas más arriba, con algunos matices:



  • En principio no tendremos que preocuparnos de que el código generado no contenga lógica de negocio, ya que svcutil sólo generará lo que tengamos marcado con [DataContract] y [DataMember]

  • La mantenibilidad del proyecto se complica igualmente, aunque en este caso, si cambiamos los tipos originales, en lugar de tocar a mano los tipos distribuibles, los vamos a regenerar con svcutil (lo cual en principio es menos trabajo)

Para que svcutil nos genere el código de los tipos, tenemos que invocarlo con la opción /dconly. Un detalle que hace que esta opción se complique un poco es que /dconly sólo funciona si lo usamos contra los metadatos del servicio, es decir, contra los ficheros .wsdl y .xsd (a pesar de que la ayuda de la herramienta muestra lo contrario en uno de los ejemplos 😉 ). Por esta razón, cada vez que tengamos que actualizar los tipos a distribuir, tendremos que pasar por dos invocaciones a svcutil, una para regenerar los metadatos y otra para obtener el código de los tipos.


Para obtener los metadatos del servicio invocaremos svcutil de la siguiente forma, sustituyendo la uri mostrada por la correspondiente a nuestro servicio:


svcutil /t:metadata http://localhost:8000


Esto generará una serie de ficheros .wsdl y .xsd que podemos utilizar para generar los tipos de la siguiente forma:


svcutil /o:types.cs /dconly *.wsdl *.xsd


En este punto tendremos un fichero types.cs que contendrá los tipos de nuestro servicio marcados con [DataContract], el cual podemos utilizar en los clientes de la forma explicada al comienzo del artículo. Para finalizar incluyo el código de la clase “Person” de ejemplo y el código del fichero types.cs generado por la herramienta. Como podéis comprobar, como es lógico en el tipo generado no se incluye el método isMinor correspondiente a la lógica de nuestro tipo “Person”:


 


Person.cs



   1: [DataContract]
   2: public class Person
   3: {
   4:     [DataMember]
   5:     public string Name { get; set; }
   6:     [DataMember]
   7:     public int Age { get; set; }
   8:  
   9:     protected bool isMinor()
  10:     {
  11:         return (Age < 18);
  12:     }
  13: }

 


types.cs



   1: //——————————————————————————
   2: // <auto-generated>
   3: //     This code was generated by a tool.
   4: //     Runtime Version:2.0.50727.1434
   5: //
   6: //     Changes to this file may cause incorrect behavior and will be lost if
   7: //     the code is regenerated.
   8: // </auto-generated>
   9: //——————————————————————————
  10:  
  11: [assembly: System.Runtime.Serialization.ContractNamespaceAttribute(“http://tempuri.org/”, ClrNamespace=“tempuri.org”)]
  12:  
  13: namespace DemoTypeLibrary
  14: {
  15:     using System.Runtime.Serialization;
  16:     
  17:     
  18:     [System.Diagnostics.DebuggerStepThroughAttribute()]
  19:     [System.CodeDom.Compiler.GeneratedCodeAttribute(“System.Runtime.Serialization”, “3.0.0.0”)]
  20:     [System.Runtime.Serialization.DataContractAttribute(Name=“Person”, Namespace=“http://schemas.datacontract.org/2004/07/DemoTypeLibrary”)]
  21:     public partial class Person : object, System.Runtime.Serialization.IExtensibleDataObject
  22:     {
  23:         
  24:         private System.Runtime.Serialization.ExtensionDataObject extensionDataField;
  25:         
  26:         private int AgeField;
  27:         
  28:         private string NameField;
  29:         
  30:         public System.Runtime.Serialization.ExtensionDataObject ExtensionData
  31:         {
  32:             get
  33:             {
  34:                 return this.extensionDataField;
  35:             }
  36:             set
  37:             {
  38:                 this.extensionDataField = value;
  39:             }
  40:         }
  41:         
  42:         [System.Runtime.Serialization.DataMemberAttribute()]
  43:         public int Age
  44:         {
  45:             get
  46:             {
  47:                 return this.AgeField;
  48:             }
  49:             set
  50:             {
  51:                 this.AgeField = value;
  52:             }
  53:         }
  54:         
  55:         [System.Runtime.Serialization.DataMemberAttribute()]
  56:         public string Name
  57:         {
  58:             get
  59:             {
  60:                 return this.NameField;
  61:             }
  62:             set
  63:             {
  64:                 this.NameField = value;
  65:             }
  66:         }
  67:     }
  68: }
  69: namespace tempuri.org
  70: {
  71:     using System.Runtime.Serialization;
  72:     
  73:     
  74:     [System.Diagnostics.DebuggerStepThroughAttribute()]
  75:     [System.CodeDom.Compiler.GeneratedCodeAttribute(“System.Runtime.Serialization”, “3.0.0.0”)]
  76:     [System.Runtime.Serialization.DataContractAttribute(Name=“greet”, Namespace=“http://tempuri.org/”)]
  77:     public partial class greet : object, System.Runtime.Serialization.IExtensibleDataObject
  78:     {
  79:         
  80:         private System.Runtime.Serialization.ExtensionDataObject extensionDataField;
  81:         
  82:         private DemoTypeLibrary.Person pField;
  83:         
  84:         public System.Runtime.Serialization.ExtensionDataObject ExtensionData
  85:         {
  86:             get
  87:             {
  88:                 return this.extensionDataField;
  89:             }
  90:             set
  91:             {
  92:                 this.extensionDataField = value;
  93:             }
  94:         }
  95:         
  96:         [System.Runtime.Serialization.DataMemberAttribute()]
  97:         public DemoTypeLibrary.Person p
  98:         {
  99:             get
 100:             {
 101:                 return this.pField;
 102:             }
 103:             set
 104:             {
 105:                 this.pField = value;
 106:             }
 107:         }
 108:     }
 109:     
 110:     [System.Diagnostics.DebuggerStepThroughAttribute()]
 111:     [System.CodeDom.Compiler.GeneratedCodeAttribute(“System.Runtime.Serialization”, “3.0.0.0”)]
 112:     [System.Runtime.Serialization.DataContractAttribute(Name=“greetResponse”, Namespace=“http://tempuri.org/”)]
 113:     public partial class greetResponse : object, System.Runtime.Serialization.IExtensibleDataObject
 114:     {
 115:         
 116:         private System.Runtime.Serialization.ExtensionDataObject extensionDataField;
 117:         
 118:         private string greetResultField;
 119:         
 120:         public System.Runtime.Serialization.ExtensionDataObject ExtensionData
 121:         {
 122:             get
 123:             {
 124:                 return this.extensionDataField;
 125:             }
 126:             set
 127:             {
 128:                 this.extensionDataField = value;
 129:             }
 130:         }
 131:         
 132:         [System.Runtime.Serialization.DataMemberAttribute()]
 133:         public string greetResult
 134:         {
 135:             get
 136:             {
 137:                 return this.greetResultField;
 138:             }
 139:             set
 140:             {
 141:                 this.greetResultField = value;
 142:             }
 143:         }
 144:     }
 145: }

 


Un saludo !!!

5 comentarios en “WCF: compartición de tipos entre referencias a servicios”

  1. Yo utilizo el svcutil, en el archivo generado hago unas modificaciones (en realidad a traves de los modificadores de la herramienta) para incluir diferentes namespaces, de esa forma no tengo problemas de confusion de los tipos desde la perspectiva del cliente.
    Esta tecnica surgio de un problema en la generación de los clientes del servicio desde Visual Basic, como se ve aqui: http://www.juanpelaez.com/Blog/2007/07/14/ProblemasEnWCFConVBnetYDataContract.aspx
    Saludos,
    Juan.

  2. @jkpelaez: tener que usar svcutil es un lio…
    Sal del VS, incluye los archivos en el proyecto, si tienes que regenerar los proxies otra vez vuelta a empezar… yo prefiero no usar svcutil salvo que necesite alguna de sus capacidades que no están en VS… cosa que cada vez ocurre con menos frecuencia…

    Un saludo!

  3. @Rodrigo

    Totalmente de acuerdo, solo lo uso cuando se presenta este problema especifico de dos tipos iguales (o peor aparentemente iguales), (con todas las problematicas que mencionas de la regeneración, la adicion manual de los archivos, etc), pero en esos casos añadir el namespace facilita todo y pues ya que el amigo Jose Luis menciona el SVUtil pues bueno es recordar que esa generación del namespace lo hace directamente el tool con uno de sus parametros de invocación. Pero en los casos “normales” siempre uso (y recomiendo) VS2008.

    Saludos,

    Juan.

  4. podrian poner un ejemplo de como uan funcion devuelve del cliente devuelve ENT.Person vs WS.Person?

    ami me sale que no se puede convertir

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *