Ícone do arquivo ZipOlá meu querido! Faz teeeeeeempo que não posto aqui no NM Tech, ainda mais posts técnicos, e para dar uma sacudida na poeira, vamos pensar na seguinte situação: Você tem uma aplicação em multi camadas composta de vários clientes Windows Forms, espalhadas em diversos locais fora da sua empresa (ou de seu datacenter) que se conectam a um servidor central pela Web a um Serviço WCF (ou até mesmo um WebService em ASP.NET), pois você não quer que esses clientes Windows Forms se conectem direto ao banco de dados.

Só que essa camada WCF precisa transmitir o resultado de uma query imeeeeeeeeensa. Como você faz?

Primeiramente, não podemos passar um DataSet como retorno de um método WCF, pois não temos como serializar uma classe complexa como um DataSet para que possa ser transmitida via XML (que é a forma como o Cliente Windows Forms e o Serviço WCF se conversam nas entranhas).

Então, para simplificar as coisas, que tal transmitir somente o XML do esquema e dos dados do DataSet para que possamos montar um DataSet igualzinho lá do outro lado? Sim, decisão corretíssima.

E para isso criaremos uma classe que tem como membros dois campos públicos do tipo String, sendo um deles para receber o XML do esquema do DataSet e o outro para receber o XML dos dados do mesmo. Para que ela possa ser entendida como serializável pelo WCF, devemos colocar o atributo [DataContract] nesta classe. Veja o código:

[DataContract]
public class DataRet
{
	public string DataXML {get;set;}
	public string SchemaXML {get;set;}
}

Após fazer a query desejada no Banco de Dados, popularemos os atributos da classe criada acima chamando os métodos GetXmlSchema() e GetXml() do DataSet que será remontado no cliente da seguinte forma:

public DataRet MinhaQuery()
{
	DataRet ret = new DataRet();
	DataSet ds = MinhaClasse.ExecutaQuery("select * from mulheres_edificantes where Nome like 'D%'")
	ret.DataXML = ds.GetXml();
	ret.SchemaXML = ds.GetXmlSchema();
	return ret;
}

Essa classe “contêiner” será transmitida via XML pelo nosso Serviço WCF, recebida no nosso cliente Windows Forms que reconstruirá o nosso DataSet para deixá-lo igualzinho quando foi transmitido e que no futuro poderá popular um DataGridView ou outro controle qualquer:

public DataSet MinhaQuery()
{
	DataSet ds = new DataSet();
	using(MeuWCFClient cliente = new MewWCFClient())
	{
		DataRet ret = cliente.MinhaQuery();
		ds.ReadXmlSchema(new MemoryStream(Encoding.BigEndianUnicode.GetBytes(ret.SchemaXML)));
		ds.ReadXml(new MemoryStream(Encoding.UTF8.GetBytes(ret.DataXML));
	}
	return ds;
}

Note aí que os métodos ReadXmlSchema() e ReadXml() não recebem uma string diretamente. Por isso que utilizamos os MemoryStream e convertemos as strings do nosso DataRet em array de bytes para poder popular esses Streams. Importante prestar atenção na codificação de caracteres para popular os Streams!!!

Beleza, cumprimos com o nosso objetivo de transmitir um DataSet através de um Serviço WCF, mas temos um pequeno probleminha aí: Estamos transmitindo esses dados através da rede pública da Internet, certo? E como sabemos, por mais que contratemos megabits de conexão nunca recebemos essa quantia, e de vez em sempre acontece desse link ficar lento, correto?

Imagine agora que a string resultante do XML dos dados tenha um tamanho, digamos assim, de 2 megabytes. Isso demoraria uma ETERNIDADE para ser transmitida!

Agora, vamos pensar uma coisa: Qual é a taxa de compressão de um arquivo de texto com o algorítmo Zip? Em quantos KB se transformariam os temíveis 2 MB do nosso XML se o transformarmos em um arquivo TXT e zipá-lo?

Garanto a você que são poucos KB, menos de 200 KB até, muito mais suaves para serem transmitidos pela Internet.

Junte esses pensamentos num liquidificador e você já tem a idéia central deste artigo: Vamos zipar o XML resultante da query, colocá-lo na nossa classe “contêiner”, transmití-lo, descompactá-lo no consumidor do Serviço WCF e finalmente remontar o DataSet.

Chega de firulas, e vamos construir o “motor” da compactação. Veja a classe abaixo:

public class EngineCompactacaoDataSet
{
	public static byte[] CompactaXML(string XMLACompactar)
	{
		byte[] ret = new byte[0];
		using (Ionic.Zip.ZipFile zip = new ZipFile(Encoding.UTF8))
		{
			zip.CompressionLevel = CompressionLevel.DEFAULT;
			MemoryStream sXMLBytes = new MemoryStream(Encoding.UTF8.GetBytes(XMLACompactar));
			zip.AddFileStream("data.xml", "xml", sXMLBytes);
			MemoryStream sZip = new MemoryStream();
			zip.Save(sZip);
			sZip.Position = 0;
			ret = new byte[sZip.Length];
			sZip.Read(ret, 0, ret.Length);
			sZip.Close();
			sZip.Dispose();
			sXMLBytes.Close();
			sXMLBytes.Dispose();
		}
		return ret;
	}

	public static string DescompactaXML(byte[] XmlCompressed)
	{
		string ret = String.Empty;
		using (ZipFile unZip = ZipFile.Read(XmlCompressed))
		{

			MemoryStream ms2 = new MemoryStream();
			unZip.Extract("xml/data.xml", ms2);
			byte[] zipE = new byte[ms2.Length];
			ms2.Position = 0;
			ms2.Read(zipE, 0, zipE.Length);
			ret = Encoding.UTF8.GetString(zipE);
			ms2.Close();
			ms2.Dispose();
		}
		return ret;
	}
}

O método CompactaXML() recebe uma string contendo o XML do DataSet que queremos compactar. Para realizar tal tarefa, utilizaremos o componente Ionic.Zip (ou DotNetZip Library). Primeiramente, criamos um arquivo Zip com a classe ZipFile do componente Ionic.Zip, e em seguida empacotaremos o XML recebido dentro de um MemoryStream. Importante: Esse MemoryStream DEVE ser criado com a codificação UTF-8, por isso utilizamos o método Encoding.UTF8.GetBytes() para transformar o nosso XML em um array de bytes que preencherá o nosso MemoryStream (variável sXMLBytes).

Em seguida, iremos adicionar esse MemoryStream no arquivo Zip, através do método AddFileStream() do objeto ZipFile. Ele pede como parâmetros o nome do arquivo, o diretório e por fim o stream a ser colocado lá. Colocaremos como padrão os nomes “data.xml” para o arquivo e “xml” para o diretório. Nem preciso dizer que vamos utilizar esses nomes depois, certo?

Depois criaremos o MemoryStream que receberá o arquivo Zip (variável sZip). Preenchemos esse MemoryStream passando-o como parâmetro do método Save() da classe ZipFile.

Depois, retornaremos esse MemoryStream correspondente ao arquivo Zip em um array de bytes, que será por fim o retorno do nosso método.

O método complementar a esse é o que faz a “descompressão” do XML, ou seja, recebe um arquivo Zip, extrai o XML dele e retorna a string correspondente.

Novamente criamos um objeto do tipo ZipFile, e para isso utilizamos o método estático Read() da classe ZipFile, que requer como parâmetro um array de bytes correspondentes ao arquivo zip a ser aberto. Logo em seguida criamos um MemoryStream que receberá o arquivo a ser extraído, que no nosso caso fica no caminho “xml/data.xml” do arquivo comprimido. Para isso utilizamos o método Extract() do objeto ZipFile, informando como parâmetros o caminho do arquivo a extrair (caminho DENTRO do arquivo .zip) e o MemoryStream para conter o arquivo descomprimido (variável ms2).

Logo em seguida, convertemos este Stream para um array de bytes, e como sabemos que se trata de uma string (o conteúdo do nosso arquivo que estava zipado é um arquivo texto, lembra?), utilizaremos o método Encoding.UTF8.GetString() informando como parâmetro esse array de bytes, e por fim retornamos a nossa string XML para poder reconstruir o DataSet.

Beleza, agora já sei como comprimir e descomprimir o XML, e como faz para transportar esse DataSet comprimido via WebService?

Do “lado servidor”, vamos voltar no método onde colocamos o XML do DataSet na nossa classe contênier (DataRet). Para colocar o nosso DataSet compactado alí, veja o código abaixo:

public DataRet MinhaQuery()
{
	DataRet ret = new DataRet();
	DataSet ds = MinhaClasse.ExecutaQuery("select * from mulheres_edificantes where Nome like 'D%'");
	ret.DataXML = Convert.ToBase64String(EngineCompactacaoDataSet.CompactaXML(ds.GetXml()));
	ret.SchemaXML = ds.GetXmlSchema();
	return ret;
}

Note que como a propriedade DataXML é do tipo string, precisamos de alguma forma “transformar” esse array de bytes retornados pelo método de compactação em uma string para poder fazer o transporte. Para isso, convertemos esse array de bytes retornados pelo método de compressão para uma string codificada em Base64. Você pode até não saber, mas cada vez que anexa um arquivo a um email ele é empacotado na mensagem como uma string Base64, pois o protocolo SMTP transporta texto, e não dados binários (na mensagem de email em si).

Para fazer a operação inversa, utilize o seguinte código:

public DataSet MinhaQuery()
{
	DataSet ds = new DataSet();
	using(MeuWCFClient cliente = new MewWCFClient())
	{
		DataRet ret = cliente.MinhaQuery();
		ds.ReadXmlSchema(new MemoryStream(Encoding.BigEndianUnicode.GetBytes(ret.SchemaXML));
		ds.ReadXML(new MemoryStream( Encoding.UTF8.GetBytes(EngineCompactacaoDataSet.DescompactaXML( Convert.FromBase64String(ret.DataXML))));
	}
	return ds;
}

Para descomprimir, o método ReadXml() do DataSet requer um MemoryStream. Mas para popular esse MemoryStream com o nosso XML compactado, necessitamos restaurar no array de bytes correspondente ao arquivo zip a string codificada em Base64 da classe contêiner, passá-lo pelo método de descompressão de XML, e transformar a string XML em um array de bytes. É isso o que fizemos com a sequencia mostrada acima! Importante é sempre utilizar UTF-8 para a transformação de strings em array de bytes e vice-versa.

E note que não necessitamos de compactar o schema do XML do DataSet, somente a parte de dados.

Tudo simples, mas tenho uma consideração a fazer: A codificação em Base64 aumenta o tamanho dos dados convertidos em cerca de 30%. Por exemplo, se o nosso arquivo zip possui 100 KB, o tamanho da string Base64 será em torno de 130 KB. Se a quantidade de dados retornada pelo DataSet for pequena, não há tanto ganho nessa operação, pode até ser o contrário. Mas se fomos partir para DataSets grandes o ganho será astronômico. E como não temos I/O de disco, ou seja, fazemos toda a manipulação do arquivo zip em memória, o overhead nessa operação não será grande se fomos comparar com o tempo que os dados levarão ou levaram para chegar no cliente.

Como basta copiar e colar os códigos e testar, no caso da biblioteca de compressão, não é necessário um projeto exemplo ;)

Um abraço e bora comprimir DataSets grandes se estes forem transmitidos por WebService ;)