Israel Aece

Software Developer

Customizando o SQLCLR

Uma das maiores novidades (e aparentemente pouco utilizada/comentada) que o SQL Server 2005 trouxe em conjunto com o .NET Framework 2.0, foi a capacidade de escrevermos Stored Procedures, Triggers e Functions em código gerenciado, ou seja, VB.NET ou C#, como dito aqui.

A idéia por trás da criação destes objetos em .NET é justamente fornecer ao SQL Server, acesso a recursos que são extremamente difícieis em uma linguagem de banco, garantindo também que outras aplicações (não gerenciadas) possam fazer o acesso as mesmas Stored Procedures sem pagar pelo preço do COM Interop. Sendo assim, dentro de uma Stored Procedure, antes de retornar os dados para o chamador, voce pode customizar o result-set, definindo os campos que achar conveniente e, além disso, pode aplicar a manipulação que desejar.

Imagine o seguinte cenário: voce tem uma coluna na sua tabela e, dentro dela, é necessário guardar um relatório. Dependendo do tamanho do relatório e da quantidade de registros, voce poderá "inchar" a sua base de dados rapidamente; sendo assim, voce poderia recorrer a compactação fornecida pelo .NET 2.0 e, antes de guardar ou antes de exibir, aplicar essa (des)compactação. A idéia do código abaixo é justamente, antes de retornar o result-set para o cliente, customizar a sua saída, ou seja, não é necessário retornar exatamente os dados provenientes da query que voce faz internamente.

[SqlProcedure]
public static void ResgatarConsultas()
{
    using (SqlConnection conn = new SqlConnection("context connection=true"))
    {
        string query = "SELECT Data, Relatorio FROM Tabela";

        using (SqlCommand cmd = new SqlCommand(query, conn))
        {
            conn.Open();
            using (SqlDataReader dr = cmd.ExecuteReader(CommandBehavior.CloseConnection))
            {
                SqlDataRecord record =
                    new SqlDataRecord(
                        new SqlMetaData("Data", SqlDbType.DateTime),
                        new SqlMetaData("Relatorio", SqlDbType.Text));

                SqlContext.Pipe.SendResultsStart(record);

                while (dr.Read())
                {
                    if (!dr.IsDBNull(0) && !dr.IsDBNull(1))
                    {
                        record.SetDateTime(0, dr.GetDateTime(0));
                        record.SetString(1,
                            Encoding.Default.GetString(IO.Decompress((byte[])dr.GetValue(1))));

                        SqlContext.Pipe.SendResultsRow(record);
                    }
                }

                SqlContext.Pipe.SendResultsEnd();
            }
        }
    }
}

Se repararmos no código acima, não há nada de novidade, pois utilizamos as mesmas classes do ADO.NET em uma aplicação tradicional. A única diferença é que não vamos expor o result-set da forma em que ele é retornado pela query interna; antes disso, será necessário passar pelo algoritmo de descompactação.

A classe SqlDataRecord representa uma linha de metadados; é através dela que vamos construir a estrutura do result-set. O construtor recebe um array de objetos do tipo SqlMetadata onde, em cada uma delas, precisamos especificar o nome e o tipo de dado do campo. Feito isso, recorremos ao método SendResultsStart passando a instancia da classe SqlDataRecord para caracterizar o ínicio do retorno para o cliente.

Uma vez que isso está pronto, utilizamos o tradicional SqlDataReader para percorrer os registros internos (retornados pelo query) e definirmos os valores fornecidos por ele para o SqlDataRecord. Note que vinculamos cada coluna do SqlDataReader a uma determinada coluna do SqlDataRecord, fazendo isso através de métodos do tipo SetXXX, onde XXX deve ser substituído pelo tipo de dado a ser carregado. O primeiro parametro de métodos SetXXX é um número inteiro que representa a coluna (do result-set) em que uma determinada coluna do SqlDataReader será carregada. Fazemos esse processo por cada iteração do SqlDataReader e, dentro deste loop, passamos o SqlDataRecord para o método SendResultsRow que, por sua vez, envia a linha para o cliente.

Finalmente, para encerrar o processamento, invocamos o método SendResultsEnd para informar ao chamador que o result-set está finalizado.

ManualResetEvent vs. AutoResetEvent

Ambas as classes que são temas do post tem basicamente a mesma finalidade, ou seja, efetuar a sinalização entre threads. Isso permite que uma determinada thread notifique as outras threads que ela finalizou ou liberou um determinado recurso e, conseqüentemente, essas outras threads poderão dar seqüencia na execução.

Leve em consideração o seguinte código:

delegate void Executor();

private static XXX _resetEvent = new XXX(false);

static void Main(string[] args)
{
    new Executor(Teste1).BeginInvoke(null, null);
    new Executor(Teste2).BeginInvoke(null, null);
    new Executor(Teste3).BeginInvoke(null, null);

    Console.WriteLine("Fim");
    Console.ReadLine();
}

static void Teste1()
{
    Console.WriteLine("Teste1 - Antes");

    Thread.Sleep(5000); //Simula um processamento pesado
    _resetEvent.Set();
   
    Console.WriteLine("Teste1 - Depois");
}

static void Teste2()
{
    Console.WriteLine("Teste2 - Antes");
    _resetEvent.WaitOne();
    Console.WriteLine("Teste2 - Depois");
}

static void Teste3()
{
    Console.WriteLine("Teste3 - Antes");
    _resetEvent.WaitOne();
    Console.WriteLine("Teste3 - Depois");
}

Resultados para quando XXX = ManualResetEvent
Fim
Teste1 - Antes
Teste2 - Antes
Teste3 - Antes
Teste1 - Depois
Teste3 - Depois
Teste2 - Depois

Resultados para quando XXX = AutoResetEvent
Fim
Teste1 - Antes
Teste2 - Antes
Teste3 - Antes
Teste1 - Depois
Teste3 - Depois

Como sabemos, o método BeginInvoke fornecido pela instancia do delegate permite a chamado para o método que ele aponta de forma assíncrona, ou seja, será criada uma worker thread para cada um deles; sendo assim, os processos acontecerão paralelamente e o reset event está aqui para garantir a sincronização das informações, ou melhor, destes métodos.

Quando utilizamos o método WaitOne do ManualResetEvent nos métodos 2 e 3, eles aguardarão um sinal que, por sua vez, será dado através do método Set, também do ManualResetEvent. Enquanto isso não acontecer, os métodos 2 e 3 não darão sequencia no processamento. Se repararmos o resultado final, as mensagens (writelines) que a aparecem depois do método WaitOne retornará, isso quer dizer que ele ficará "travado" até receber o sinal para prosseguir. Uma vez recebido, todos aqueles que estiveremos pendentes serão executados.

Já com um AutoResetEvent, o processo é muito semelhante, com a exceção que quando o método Set for chamado, apenas uma thread da fila será executada. Isso explica o motivo pelo qual a mensagem do método 2 ("Teste2 - Depois") não aparece no resultado final.

Bem, isso é uma das N possibilidades que o .NET fornece para efetuar a sincronização através de sinalização. Para aqueles que querem se aprofundar, vejam o namespace System.Threading.

TransferRequest

Há algum tempo atrás eu mencionei aqui um grande problema que existe quando utilizamos o método Transfer da classe HttpServerUtility, principalmente quando falamos de redirecionamento para páginas restritas. Isso ocorre porque este método não executa o processo total dentro do ASP.NET pipeline. Esse foi um dos motivos que levou o Paulo Morgado a criar os Page Modules.

Para sanar este problema, a versão 3.5 do ASP.NET incluiu um novo método na classe HttpServerUtitlity chamado TransferRequest. A sua utilização é identica ao método Transfer, só que possui um comportamento totalmente diferente, ou seja, permite que a requisição para o recurso solicitado seja executado passando por todo o pipeline do ASP.NET e, sendo assim, o processo de autorização será novamente executado. Além disso, é necessário que a aplicação corra no IIS 7.0 pois, do contrário, uma exceção será atirada.

Só que para aqueles que querem utilizar esse recurso e fazem o uso de variáveis de sessão, é importante ler a nota que o Luis Abreu diz aqui.

WCF - Partial Trust

Uma das configurações mais importantes que se deve realizar em uma aplicação é a segurança, mais precisamente, que tipo de permissão devemos conceder à mesma para que ela possa ser executada. O tipo de permissão que me refiro aqui não tem a ver com a identidade do usuário, mas sim as permissões que o código (Assembly) tem para acessar recursos da máquina onde o mesmo está sendo executado.

Esse tipo de configuração foi introduzida desde a primeira versão do .NET Framework e é chamada de Code Access Security - CAS. Baseada no conceito de permissões, onde cada uma delas mapeia para um recurso (Message Queue, File System, SQL Server, etc.) da máquina, são através destas permissões que dizemos ao Runtime Security Policy (de forma declarativa ou imperativa), quais direitos nosso código tem de executar, ou melhor, a quais recursos ele terá acesso.

Se você cria qualquer aplicação .NET e não altera nenhuma configuração de segurança, ele será executado em Full Trust, ou seja, terá acesso irrestrito a qualquer recurso da máquina onde está atualmente sendo executado. Apesar disso ser muito comum, não é ideal. Uma premissa básica na segurança é jamais conceder direitos desnecessários a um código para desempenhar suas funções. Quando você opta por mudar a segurança padrão imposta pelo .NET, quer dizer que você está executando a aplicação em Partial Trust, ou seja, em um ambiente em que você não terá acesso a todos os recursos, mas sim, o essencial para a aplicação poder trabalhar.

Na primeira versão do WCF (Windows Communication Foundation) - .NET Framework 3.0 - ele não era suportado em ambientes que estavam sob Partial Trust, o que obrigava muitos clientes a conceder mais direitos do que o necessário para poder executar/invocar um serviço escrito em WCF. Depois de muitas requisições, a Microsoft decidiu afrouxar essa segurança com o lançamento do .NET Framework 3.5, permitindo (com várias restrições) que serviços sejam invocados a partir de um ambiente parcialmente confiável. Isso obrigou a Microsoft a decorar os assemblies System.ServiceModel.dll e System.ServiceModel.Web.dll com o atributo AllowPartiallyTrustedCallers que, como o nome diz, permite que o assembly seja invocado por aplicações que estão sendo executadas em um ambiente mais restrito.

Entre as limitações que o WCF 3.5 impõe, temos: permite somente os bindings BasicHttpBinding, WsHttpBinding (sem segurança ou com segurança a nível de transporte) e WebHttpBinding serem invocados a partir de uma aplicação que está sendo executada em ambiente parcialmente confiável; além disso, serviços WCF que estão rodando em aplicações de middle tier também podem invocar requisições para outros serviços, desde que tenha a sua respectiva WebPermisson, que concede direito de acesso a esse recurso. Todos os outros bindings, não HTTP, demandarão full trust de seus chamadores, obrigando-nos a conceder ao cliente mais direitos do que ele deveria ter para poder trabalhar. Para uma lista completa de todas as restrições, você pode consultar este link.

Mas e quando necessitamos referenciar em nossa aplicação parcialmente confiável, um serviço WCF que expõe apenas um endpoint com o binding definido como NetTcpBinding? Uma alternativa interessante é criar um proxy, isolando toda a chamada para este serviço. E no que consiste um proxy? Basicamente trata-se de uma DLL que conterá uma classe que servirá como um wrapper para a interface do serviço a ser acessado. Quando referenciamos o serviço diretamente na aplicação, automaticamente a IDE do Visual Studio se encarrega de criar a classe (também denominada proxy) para que possamos invocar os respectivos métodos do serviço localmente e que, durante a execução, será delegado ao serviço.

Para o nosso cenário isso não será a melhor saída. O que devemos fazer é utilizar o utilitário svcutil.exe que, entre suas utilidades, é capaz de gerar uma classe baseado no WSDL extraído do mesmo. A linha abaixo ilustra como devemos passar os parâmetros para o svcutil.exe para que o mesmo possa gerar a classe correspondente (executando a partir do prompt do Visual Studio .NET):

C:\>svcutil net.tcp://localhost:9000/Mex /out:C:\Proxy.cs

Uma vez que a classe é criada, criaremos um projeto do tipo Class Library e adicionaremos a classe recém criada, Proxy.cs, dentro deste projeto. Além disso, é também necessário fazer a referência para o assembly System.ServiceModel.dll. Depois deste processo, precisamos ainda fazer algumas configurações em nível de assembly; como essa DLL deverá ser consumida por um projeto que está sendo executado em um ambiente parcialmente confiável, adicionar no arquivo AssemblyInfo.cs o atributo AllowPartiallyTrustedCallers; finalmente, será necessário definir um Strong Name para essa DLL (veremos mais tarde, ainda neste artigo o motivo disso). O trecho de código abaixo ilustra como devemos proceder para definir o atributo APTCA:

using System.Security;

[assembly: AllowPartiallyTrustedCallers]

Para finalizar, é necessário abrir o arquivo Proxy.cs e, no ínicio da classe que representa o serviço do lado do cliente (ClientBase), e especificar o atributo PermissionSetAttribute, assim como é mostrado abaixo:

using System.Security;
using System.Security.Permissions;

[PermissionSetAttribute(SecurityAction.Assert, Name = "FullTrust")]
public partial class Service1Client : System.ServiceModel.ClientBase<IService1>, IService1
{
    //Implementação
}

O atributo PermissionSetAttribute irá permitir a definição de uma determinada ação para uma permission set (conjunto de permissões) específica. No exemplo acima, estamos especificando a ação Assert (via enumerador SecurityAction) sob a permission set predefinida, que é a FullTrust. A utilização da ação/método Assert deve ser analisada com muito cuidado, pois é o mesmo que "Eu sei o que estou fazendo; confie em mim!", e isso garantirá que a permissão seja concedida ao chamador mesmo que ele não tenha privilégio para isso.

Finalmente, a DLL está pronta para ser utilizada mas se, neste momento, a mesma for referenciada na aplicação que está sendo executada em ambiente parcialmente confiável, uma exceção será atirada, dizendo que não é possível consumir um serviço via TCP nesta aplicação. Neste caso, necessitamos adicionar essa DLL no GAC, pois os componentes que lá residem já ganham Full Trust. É importante dizer que, mesmo assim, é necessário o código acima, pois o stack walk também verificará a aplicação Web que chama o componente e essa, por sua vez, não terá a permissão necessária e, consequentemente, a exceção será atirada. Esse conceito que utilizamos aqui também é conhecido como Sandboxing.

Importante: Apesar desta técnica funcionar, ela tem um ponto negativo: suprimindo a security demand, que é o processo de avaliação dos chamadores para se certificar que todos possuem tal permissão, permitirá que qualquer cliente que execute sob ambiente parcialmente confiável chame qualquer serviço WCF. Isso permitirá a um cliente que não tenha permissão de acesso a algum recurso, como acesso TCP, aplicar um bypass nesta limitação. A solução para esse problema é criar uma classe que herde diretamente da classe base ClientBase, concedendo as permissões específicas, dependendo do tipo de binding.

Conclusão: Felizmente o WCF permite na versão 3.5 do .NET Framework o consumo de serviços em ambiente parcialmente confiáveis. Alguns bindings podem ser consumidos sem a necessidade de alguma configuração adicional, pois são acessíveis em ambientes parcialmente confiáveis; já outros bindings não podem ser acessados diretamente, e necessitam de um passo adicional, que é a criação do proxy, explicado neste artigo.