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.

Listagem 1. Modelagem de um cliente no projeto WSWithMyOwnTypes.Model.

            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; }

        }

    }

}
        
Solution Explorer
Figura 1. Solution Explorer
Listagem 2. Serviço WEB simples para usar um cliente no projeto WSWithMyOwnTypes.CustomerService.

            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

Solution Explorer
Figura 2. 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.

Listagem 3. Fazendo a chamada do serviço WEB (com erro de compilação)

            {

                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.

Listagem 4. Fragmento do código criado pelo Xsd.exe (reference.cs) para permitir a chamada do serviço WEB.

            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.

Listagem 5. Fazendo a chamada do serviço WEB (agora vai pelo menos compilar)

            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));
}
        
Cliente CustomerService em ação
Figura 3. Cliente CustomerService em ação.

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).

Listagem 6. Modificações na classe Customer.

            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.

Listagem 7. Modificações no serviço WEB para utilização das modificações feitas na classe Customer.

            public class CustomerService : System.Web.Services.WebService

  {

   [WebMethod]

   public string MakeSomethingWithCustomer(Customer customer)

   {

     //Uso o estado do objeto

     return customer.MakeAnyThingWasExecuted.ToString();

   }

  }
        
Listagem 8. Mudando o código que chama o serviço WEB.

            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).

Listagem 9. Classe para ‘ajudar’ o Xsd.exe a entender nossos tipos ao criar o Proxy para acessar o serviço WEB.

            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).

Listagem 10. Tornando nossos tipos conhecidos.

            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:

  1. 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.
  2. 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:
  3. 
                    sn -p MyStrongNameKey.snk MyPublicKey.txt
    
                    sn -t MyPublicKey.txt.
                
  4. 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.
  5. Modifique o arquivo de configuração machine.config da forma indicada pela Listagem 11 (dentro de configuration):
Listagem 11 . Alteração no machine.config.

            <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>
        
Note que 2876b2fe162cb809 é a parte pública da chave utilizada nos exemplos deste artigo.

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.

Listagem 12. Fragmento do código criado pelo Xsd.exe para permitir a chamada do serviço WEB (desta vez usando os nossos tipos).

            public void MakeSomethingWithCustomer(Palladino.Articles.WSWithMyOwnTypes.Customer customer)
        
Listagem 13. Agora podemos utilizar nosso tipo Customer sem problemas.

            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.