Neste artigo vamos ver como utilizar tipos próprios em serviços WEB. Utilizar tipos próprios em serviços WEB pode ser considerado por alguns, algo paradoxal. De certa maneira é mesmo. Veja a questão da distribuição. Um dos objetivos do WEB service é tornar o cliente mais fácil de distribuir. Usar tipos próprios em serviços WEB pode obrigá-lo a distribuir seus assemblies tanto no cliente quanto no servidor em um aplicativo Windows, por exemplo.
De fato, em todo tempo que trabalho com a plataforma .Net houve apenas dois casos em que precisei abrir mão deste recurso. Em ambos os casos já havia uma ampla camada de modelos baseada em tipos próprios. Tipos estes que precisavam ser utilizados nos dois extremos dos aplicativos.
Mão na massa
Para começar vamos criar dois projetos. Um para o serviço WEB, que vamos chamar de CustomerService e outro para nossa biblioteca de classes que conterá nossos modelos. Vamos chamar este último de WSWithMyOwnTypes.Model. A sua solution Explorer deve estar como a da Figura 1.
using System;
using System.Collections.Generic;
using System.Text;
namespace Palladino.Articles.WSWithMyOwnTypes
{
public class Customer
{
private string name;
//Apenas estado, por enquanto
public string Name
{
get { return this.name; }
set { this.name = value; }
}
}
}
using System;
using System.Data;
using System.Web;
using System.Collections;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.ComponentModel;
namespace Palladino.Articles.WSWithMyOwnTypes
{
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ToolboxItem(false)]
public class CustomerService : System.Web.Services.WebService
{
[WebMethod]
public string MakeSomethingWithCustomer(Customer customer)
{
//Apenas utiliza o estado de 'customer' para o
//retorno do método
return customer.Name;
}
}
}
Observação
Este fragmento de código utiliza o namespace http://tempuri.org/ para o serviço. A técnica mostrada neste artigo depende desta informação, de forma que você deve dar bastante atenção a isto quando for implementar os seus próprios serviços. O código fonte usado como exemplo para este artigo utiliza o seguinte namespace.
Vamos criar mais um projeto, desta vez uma aplicação Windows, para consumir o serviço CustomerService. Vamos chamar este novo projeto de .WSWithMyOwnTypes.CustomerServiceConsumer. Este projeto conterá uma referência para o serviço (Web Reference) e outra para a biblioteca de modelos. Observe a Figura 2 que representa o seu Solution Explorer
No formulário deste aplicativo, depois que a referência WEB tiver sido adicionada adequadamente, vamos criar um botão e manipular o seu evento Click conforme indicado na Listagem 3.
{
MyServices.CustomerService service = new
MyServices.CustomerService();
Customer anyCustomer = new Customer();
anyCustomer.Name = "Um cliente qualquer";
MessageBox.Show(service.MakeSomethingWithCustomer(anyCustomer));
}
Ao tentar compilar este código vamos receber a seguinte mensagem de erro:
The best overloaded method match for Palladino.Articles.WSWithMyOwnTypes
.MyServices.CustomerService.MakeSomethingWithCustomer(Palladino.Articles
.WSWithMyOwnTypes.
MyServices.Customer)' has some invalid arguments.
Note que ele está esperando um tipo Customer contido em MyServices (nome da referência WEB) ao invés do Customer que criamos em nossa biblioteca de classes. Podemos ver isto mais de perto, analisando o código gerado pelo Xsd.exe (Listagem 4) no arquivo reference.cs. Se você não encontrar este arquivo logo de cara, não se preocupe. Selecione a referência WEB e utilize a opção Show All Files do Solution Explorer. Depois de abri-lo, note que este arquivo contém a classe Proxy que será utilizada pelo cliente para acessar o serviço. Procure o fragmento de código contido na Listagem 4 para ver o que está acontecendo.
public partial class Customer
{
private string nameField;
public string Name
{
get { return this.nameField; }
set { this.nameField = value; }
}
}
Vamos alterar nosso código cliente para o código descrito na Listagem 5 para que ele possa compilar adequadamente. Não vamos fazer nada maravilhoso neste código. Note que estamos apenas utilizando a classe Customer contida em MyServices ao invés de utilizarmos aquela definida por nós. Vamos executar o programa e clicar no botão presente no formulário. Você deve ter uma resposta como indicado pela Figura 3.
private void button1_Click(object sender, EventArgs e)
{
MyServices.CustomerService service = new
MyServices.CustomerService();
MyServices.Customer anyCustomer = new MyServices.Customer();
anyCustomer.Name = "Um cliente qualquer";
MessageBox.Show(service.MakeSomethingWithCustomer(anyCustomer));
}
O xsd.exe (programa que gera a classe Proxy para acesso ao serviço WEB) fez um excelente trabalho tornando transparente para o cliente a utilização da classe Customer contida em Palladino.Articles.WSWithMyOwnTypes.
Vamos fazer algumas modificações na nossa classe ‘Customer’, que está no projeto WSWithMyOwnTypes.Model, para explorar um pouco mais as possibilidades. Veja que no novo código tudo continua muito simples. Agora temos uma regra no set da propriedade ‘Name’, um método e mais duas propriedades (Listagem 6).
public class Customer
{
//Apenas para alterar o estado
//da classe
public void MakeAnyThing()
{
//Ajusta algumas propriedades
this.makeAnyThingWasExecuted = true;
this.makingAnyThing = true;
try
{
//Gasta um tempo aqui....
System.Threading.Thread.Sleep(3000);
}
finally
{
//Indica que não está fazendo mais nada
this.makingAnyThing = false;
}
}
private string name;
public string Name
{
get { return this.name; }
set
{
//Se o valor informado for diferente de nulo ajusta a
propriedade, caso contrário dispara uma exceção
if (value != null)
this.name = value;
else
throw new Exception("can not be null.");
}
}
//Propriedade que indica se está fazendo alguma coisa
private bool makingAnyThing;
public bool MakingAnyThing
{
get { return this.makingAnyThing; }
set { this.makingAnyThing = value; }
}
//Indica se o método 'MakeAnyThing' foi executado?
private bool makeAnyThingWasExecuted;
public bool MakeAnyThingWasExecuted
{
get { return this.makeAnyThingWasExecuted; }
set { this.makeAnyThingWasExecuted = value; }
}
}
Vamos agora alterar o nosso cliente Windows Forms (Listagem 8) e o nosso serviço WEB (Listagem 7) para que eles possam utilizar as novas características do modelo. A idéia é chamar o método ‘MakeAnyThing‘ no cliente e usar o estado do objeto criado no cliente, dentro serviço WEB. O código da Listagem 7 é o mais simples possível. Ele apenas pega o resultado do método ToString da classe Customer e devolve como retorno do método WEB. O código da listagem 8 cria uma instância do serviço WEB, cria uma referência de MyServices.Customer e tenta utilizar o método MakeAnyThing que foi criado na listagem 6.
public class CustomerService : System.Web.Services.WebService
{
[WebMethod]
public string MakeSomethingWithCustomer(Customer customer)
{
//Uso o estado do objeto
return customer.MakeAnyThingWasExecuted.ToString();
}
}
private void button1_Click(object sender, EventArgs e)
{
MyServices.CustomerService service = new
MyServices.CustomerService();
MyServices.Customer anyCustomer = new
MyServices.Customer();
anyCustomer.Name = null;
anyCustomer.MakeAnyThing();
MessageBox.Show(service.
MakeSomethingWithCustomer(anyCustomer));
}
Devemos agora atualizar a referência do Webservice, clicando com o botão direito sobre ela e escolhendo a opção ‘Update Webservice’. Ao compilar o aplicativo Windows, obtemos um erro de compilação dizendo que o método ‘MakeAnyThing‘ não existe no tipo MyServices.Customer. Olhando de perto o arquivo reference.cs (aquele em que contém a classe proxy para acessar o serviço WEB) gerado pela ferramenta, vamos perceber que apenas o código necessário para manter o estado das propriedades foi adicionado à classe. A situação agora é que estamos trabalhando com uma classe que não tem nada a ver com aquela que construímos, realmente. A ferramenta não entendeu que gostaríamos de usar o tipo Customer contido em nossa biblioteca de classes. Ela fez o melhor possível para que tudo compilasse e funcionasse bem, mas definitivamente uma classe nova não é o que queremos agora.
Para fazer funcionar na marra, poderíamos ignorar o aviso no início do arquivo reference.cs (que basicamente diz para não colocarmos a mão ali) e alterar ‘na unha’ o código, removendo a classe Customer. Isto faria o código compilar e funcionaria em tempo de execução. Não obstante, a cada atualização deste serviço (quando adicionarmos um novo método, por exemplo) teríamos de fazer o trabalho todo novamente. Ainda não está parecendo muito certo. O correto seria termos uma maneira de dizer ao xsd.exe como gerar o Proxy.
Nestas ocasiões dou um valor muito grande para as decisões de designer feitas em alguns pontos do .NET Framework. Graças a decisões acertadas de design e arquitetura temos a possibilidade de estender o Visual Studio de várias maneiras diferentes sem que haja a necessidade de alterarmos seu código fonte. Esta é uma destas ocasiões. O que vamos fazer aqui é instruir o xsd.exe para quando for criar o Proxy deste serviço, entender que queremos utilizar a classe ‘Customer’ da nossa biblioteca. Vamos dizer a ele para não preocupar-se em tentar ajudar. Para tanto, criaremos um novo projeto (uma biblioteca de classes com o nome ‘WSWithMyOwnTypes.SchemaImporterExtensions’) e nele vamos criar uma classe que herde de System.Xml.Serialization.Advanced.SchemaImporterExtension (Listagem 9).
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.Serialization;
using System.Xml.Serialization.Advanced;
using System.Xml.Schema;
using System.CodeDom;
using System.CodeDom.Compiler;
namespace Palladino.Articles.WSWithMyOwnTypes
{
public class SchemaImporterExtensions :
System.Xml.Serialization.Advanced.SchemaImporterExtension
{
}
}
Após este passo, vamos fazer um override do método ImportSchemaType da classe SchemaImporterExtension. A implementação (Listagem 10) é bem básica e faz uma espécie de tradução de nomes (note o namespace que está sendo tratado).
public override string ImportSchemaType(string name, string ns,
XmlSchemaObject context, XmlSchemas schemas, XmlSchemaImporter
importer, CodeCompileUnit compileUnit, CodeNamespace mainNamespace,
CodeGenerationOptions options, CodeDomProvider codeProvider)
{
if (string.Compare(ns, "http://palladino.com.br/", true) == 0)
{
mainNamespace.Imports.Add(new
CodeNamespaceImport(
"Palladino.Articles.WSWithMyOwnTypes"));
switch (name)
{
case "Customer":
return
"Palladino.Articles.WSWithMyOwnTypes.Customer";
}
}
return base.ImportSchemaType(name, ns, context, schemas,
importer, compileUnit, mainNamespace, options,
codeProvider);
}
O código começa fazendo uma comparação do nome do namespace utilizado pelo schema que está sendo importado. Em seguida temos a importação dos namespaces utilizados pelos nossos tipos. Isto serve para que o código gerado pela ferramenta declare os namespaces nos quais os nossos tipos estão. Para finalizar, fazemos uma comparação do nome do tipo que está sendo importado e devolvemos o nome correto. Caso o namespace não seja o que queremos tratar, a implementação padrão será executada.
Agora vamos dizer ao Visual Studio que há um tipo em uma determinada biblioteca de classes dedicado a customizar a geração do código gerado pelo xsd.exe. Há algumas coisas a serem feitas para conseguirmos fazer isto:
- Associe o assemblie WSWithMyOwnTypes.SchemaImporterExtensions a uma Strong Name Key. A criação da chave isto pode ser feita pelas propriedades do projeto ou pelo utilitário de linha de comando sn.exe, utilizando a opção –k. Por exemplo: sn -k MyStrongNameKey.snk. Caso você utilize esta opção, lembre-se de associar a chave gerada ao projeto.
- Extraia a porção pública da chave. Isto pode ser feito pelo utilitário sn.exe, utilizando as opções –p e –t, respectivamente. Por exemplo:
- Registre o assemblie WSWithMyOwnTypes.SchemaImporterExtensions no GAC. Para fazer isto na linha de comando, utilize o utilitário gacutil.exe com a opção /i. Por exemplo: gacutil /i WSWithMyOwnTypes.SchemaImporterExtensions.dll.
- Modifique o arquivo de configuração machine.config da forma indicada pela Listagem 11 (dentro de configuration):
sn -p MyStrongNameKey.snk MyPublicKey.txt
sn -t MyPublicKey.txt.
<system.xml.serialization>
<schemaImporterExtensions>
<clear />
<add name="WSWithMyOwnTypes.SchemaImporterExtensions"
type="Palladino.Articles.WSWithMyOwnTypes.SchemaImporterExtensions,
WSWithMyOwnTypes.SchemaImporterExtensions, Version=1.0.0.0, Culture=neutral,
PublicKeyToken=2876b2fe162cb809" />
</schemaImporterExtensions>;
</system.xml.serialization>
Se estiver com o Visual Studio aberto, feche-o e abra-o novamente, Exclua a referência MyServices e inclua uma nova. Note que agora a ferramenta gerou o código corretamente (Listagem 12). Do ponto de vista do cliente, agora podemos utilizar o nosso tipo sem problemas. Veja na listagem 13.
public void MakeSomethingWithCustomer(Palladino.Articles.WSWithMyOwnTypes.Customer customer)
private void button1_Click(object sender, EventArgs e)
{
MyServices.CustomerService service = new MyServices.CustomerService();
//Utilizando o ‘nosso’ tipo Customer
Customer anyCustomer = new Customer();
anyCustomer.Name = "Um cliente qualquer";
anyCustomer.MakeAnyThing();
MessageBox.Show(service.MakeSomethingWithCustomer(anyCustomer));
}
Questão de princípios
Lá pelo meio do artigo eu me referi à decisão de design, sendo que gostaria de explorar só mais um pouco aquele ponto de vista. O que vimos neste artigo é uma aplicação perfeita, em minha opinião, de um princípio de design de software conhecido como ‘Aberto-Fechado’. Este princípio tem por objetivo permitir que as classes sejam estendidas para incorporar um novo comportamento sem que haja a necessidade de alterar o código existente. Veja o Visual Studio, por exemplo, ele está quase sempre aberto para expansão e sempre fechado para alteração.
Em um software deste tipo isto é vital. Primeiro porque ele é um software cujo código não está disponível para alteração. Segundo, por ter que atender a uma gama potencialmente infinita de necessidades de desenvolvedores.
Boas e más notícias (não necessariamente nesta ordem)
As más notícias: Se você olhou bem para a última versão da classe ‘Customer’, deve ter percebido que as propriedades ‘MakingAnyThing’ e ‘MakeAnyThingWasExecuted’ estão permitindo leitura e escrita, quando o correto (e altamente desejável em um caso como este) seria que elas fossem somente de leitura. Ocorre que na versão 2.0 do framework só é possível serializar membros públicos que são de leitura e escrita. Se fizéssemos da forma correta (com as propriedades somente de leitura), o que iria acontecer é que o estado gerado no cliente não poderia ser utilizado no serviço WEB.
Como o objetivo aqui é mostrar a técnica para utilização de tipos próprios em serviços WEB isto pode até ser admitido. Apenas tenha em mente que no mundo real isto seria totalmente condenável. Na prática estas variáveis só deveriam ser mantidas pelo método MakeAnyThing e isto, por si só, pode condenar a utilização desta técnica em alguns casos.
As boas notícias: No WCF (Windows Communication Foundation) várias das limitações impostas pela serialização XML (XmlSerializer) no Famework 2.0 e anteriores foram removidas. É possível serializar campos, membros públicos, privados e somente de leitura.
Conclusão
Neste artigo vimos como utilizar nossos próprios tipos em Web Services. Mesmo que este não seja o caso para você, pense um pouco sobre como é possível que os desenvolvedores criem expansões dos mais diversos tipos para o Visual Studio, sem que haja necessidade de alterações nele. Será que há casos em que este princípio poderia ser aplicado em nossos softwares? Dá para aprender um bocado sobre como projetar softwares mais flexíveis analisando a forma como isto é feito em programas como o Visual Studio.