BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Artigos MicroORM, um ORM de tipos dinâmicos para VB e C# em mais ou menos 160 linhas

MicroORM, um ORM de tipos dinâmicos para VB e C# em mais ou menos 160 linhas

ORMs estão na moda nos dias de hoje por uma boa razão: eles podem fazer o desenvolvimento de aplicações baseadas em banco de dados rápido e sem dor. Mas frameworks de ORM são bem restritos, eles esperam que os desenvolvedores sigam certas regras e são as vezes bem difíceis de se usar quando isso não é feito. Uma regra comum é que uma stored procedure pode retornar apenas 1 conjunto de resultados e este tem que ter uma lista consistente de colunas. Infelizmente existem várias stored procedures onde seus retornos variam de acordo com suas lógicas internas. Por exemplo, uma stored procedure pode ter um parâmetro que define quais colunas são retornadas e outro indicando se devem ser retornadas todas as linhas ou somente parte delas. Ou talvez este resultado varia de acordo com algum parâmetro interno e a aplicação precisa examinar a saída para determinar a estrutura a ser usada em tempo de execução.

Quando um desenvolvedor .NET encontra um conjunto de stored procedures que simplesmente não foi projetado para trabalhar com ORMs, normalmente ele utiliza DataTables. Com o novo suporte a tipagem dinâmica encontrado no .NET 4.0, outra solução se apresenta. E se tudo, incluindo o nome da stored procedure, parâmetros SQL e os objetos resultantes fossem avaliados em tempo de execução?

Aqui vai um exemplo de código em VB e C#. Você vai notar que VB requerer Option Strict para funcionar enquanto c# utiliza diretamente sua nova palavra reservada "dynamic".

VB

Using con As New SqlClient.SqlConnection(connectionString)

    Dim customer = con.CallSingleProc.CustomerSelect(AccountKey:=12345)

    Console.WriteLine(customer.FirstName & " " & customer.LastName)

 

    Dim orders As IList = con.CallListProc.OrderSearch(AccountKey:=12345, MinCreatedDate:=Now.AddDays(-7), MaxCreatedDate:=Now)

    Dim totalValue = Aggregate order In orders Into Sum(CDec(order.TotalOrderValue))

    Console.WriteLine("This customer ordered a total of $" & totalValue & " last week")

    For Each order In orders

        Console.WriteLine(vbTab & "Order Key: " & order.OrderKey & " Value: $" & order.TotalOrderValue)

    Next

 

End Using

 

C#

using (var con = new SqlConnection(connectionString))

{

    var customer = con.CallSingleProc().CustomerSelect(AccountKey: 12345);

    Console.WriteLine(customer.FirstName + " " + customer.LastName);

 

    IList<dynamic> orders = con.CallListProc().OrderSearch(AccountKey: 12345, MinCreatedDate: DateTime.Now.AddDays(-7), MaxCreatedDate: DateTime.Now);

    var totalValue = orders.Sum(order => (decimal)order.TotalOrderValue);

 

    Console.WriteLine("This customer ordered a total of $" + totalValue + " last week");

    foreach (var order in orders)

    {

        Console.WriteLine("\tOrder Key: " + order.OrderKey + " Value: $" + order.TotalOrderValue);

    }

}

Isto parece código .NET normal, mas muitos desses métodos e propriedades simplesmente não existem. Aqui vai o mesmo código com membros inexistentes grifados.

VB

Using con As New SqlClient.SqlConnection(connectionString)

    Dim customer = con.CallSingleProc.CustomerSelect(AccountKey:=12345)

    Console.WriteLine(customer.FirstName & " " & customer.LastName)

 

    Dim orders As IList = con.CallListProc.OrderSearch(AccountKey:=12345, MinCreatedDate:=Now.AddDays(-7), MaxCreatedDate:=Now)

    Dim totalValue = Aggregate order In orders Into Sum(CDec(order.TotalOrderValue))

    Console.WriteLine("This customer ordered a total of $" & totalValue & " last week")

    For Each order In orders

        Console.WriteLine(vbTab & "Order Key: " & order.OrderKey & " Value: $" & order.TotalOrderValue)

    Next

 

End Using

C#

using (var con = new SqlConnection(connectionString))

{

    var customer = con.CallSingleProc().CustomerSelect(AccountKey: 12345);

    Console.WriteLine(customer.FirstName + " " + customer.LastName);

 

    IList<dynamic> orders = con.CallListProc().OrderSearch(AccountKey: 12345, MinCreatedDate: DateTime.Now.AddDays(-7), MaxCreatedDate: DateTime.Now);

    var totalValue = orders.Sum(order => (decimal)order.TotalOrderValue);

 

    Console.WriteLine("This customer ordered a total of $" + totalValue + " last week");

    foreach (var order in orders)

    {

        Console.WriteLine("\tOrder Key: " + order.OrderKey + " Value: $" + order.TotalOrderValue);

    }

} 

Neste ponto alguns tradicionalistas vão começar a falar sobre os riscos que conexões tardias trazem, como por exemplo erros de sintaxe que só vão aparecer em tempo de execução. Enquanto isso é certamente uma possibilidade, nós não estamos muito pior que a situação anterior. Quando deixamos nomes de stored procedures e colunas em texto, nós vamos enfrentar o mesmo problema de erros aparecerem somente em tempo de execução.

Para isto funcionar, nós precisamos de algumas coisas, como por exemplo uma forma de mudar de um contexto tipado para um contexto dinâmico. Para isso nós tempos que escolher um conjunto de métodos de extensão que retornam "System.Object". Em VB isto é suficiente para começar a conexão tardia, mas não em C#. Para o C# mudar de modo, você também tem que decorar o valor de retorno com um atributo dinâmico.

Public Module MicroOrm

    ''' <summary>

    ''' Chama a stored procedure e retorna um valor escalar.

    ''' </summary>

    ''' <returns>Nulo ou um único valor</returns>

    ''' <remarks>Somente a primeira coluna da primeira linha do primeiro resultset é retornada. Todo o resto é ignorado. Nulos da base de dados são convertidos para nulos CLR.</remarks>

    <Extension()>

    Public Function CallScalarProc(ByVal connection As SqlConnection) As <Dynamic()> Object

        Return New MicroProcCaller(connection, Scalar)

    End Function

 

    ''' <summary>

    ''' Chama uma stored procedure que retorna um único objeto.

    ''' </summary>

    ''' <returns>Nulo ou um MicroDataObject</returns>

    ''' <remarks>Somente a primeira linha de um resultset é retornada. Todo o resto é ignorado. Nulos da base de dados são convertidos para nulos CLR.</remarks>

    <Extension()>

    Public Function CallSingleProc(ByVal connection As SqlConnection) As <Dynamic()> Object

        Return New MicroProcCaller(connection, [Single])

    End Function

 

    ''' <summary>

    ''' Chama uma stored procedure que retorna uma lista de objetos.

    ''' </summary>

    ''' <returns>Uma lista de MicroDataObject. Há um MicroDataObject por linha.</returns>

    ''' <remarks>Somente o primeiro resultset é retornado. Nulos da base de dados são convertidos para nulos CLR.</remarks>

    <Extension()>

    Public Function CallListProc(ByVal connection As SqlConnection) As <Dynamic()> Object

        Return New MicroProcCaller(connection, List)

    End Function

 

    ''' <summary>

    ''' Chama uma stored procedure que retorna uma lista com listas de objetos.

    ''' </summary>

    ''' <returns>Uma lista contendo listas de MicroDataObject. Há uma lista por resultset e um MicroDataObject por linha de cada resultset.</returns>

    ''' <remarks>Nulos da base de dados são convertidos para nulos CLR.</remarks>

    <Extension()>

    Public Function CallMultipleListProc(ByVal connection As SqlConnection) As <Dynamic()> Object

        Return New MicroProcCaller(connection, MultipleLists)

    End Function

 

End Module

Para mostrar a diferença, aqui vai uma função usando C#.

public static class MicroOrm

{

    public static dynamic CallSingleProc(this SqlConnection connection)

    {

       return new MicroProcCaller(connection, CallingOptions.Single);

    }

}

Para completar a história, aqui vai o construtor da classe MicroProcCaller. Note que a classe é marcada como Friend (C# internal). Isto é feito porque ninguém deve nunca declarar uma variável deste tipo; ela somente funciona em um contexto dinâmico. Esta classe é também transitória; consumidores não devem segurar uma referência para ela.

Friend Class MicroProcCaller

    Inherits Dynamic.DynamicObject

 

    Private m_Connection As SqlConnection

    Private m_Options As CallingOptions

 

    Public Sub New(ByVal connection As SqlConnection, ByVal options As CallingOptions)

        m_Connection = connection

        m_Options = options

    End Sub

End Class

 

Public Enum CallingOptions

    Scalar = 0

    [Single] = 1

    List = 2

    MultipleLists = 3

End Enum

Agora que estamos em um contexto dinâmico, nós precisamos de uma forma de traduzir a chamada deste método de conexão tardia em uma stored procedure. Existem várias formas de se fazer isso, mas a mais fácil é extender a classe de DynamicObject e sobrescrever o método TryInvokeMember. Seguem numeradas abaixo as etapas a serem realizadas:

  1. Decidir se esta função é responsável por gerenciar o ciclo de vida do objeto de conexão.
  2. Criar um SqlCommand usando um método com o mesmo nome da stored procedure. O nome do método que foi chamado pode ser achado no "conector".
  3. Já que chamadas para stored procedures utilizando o Data.SqlClient não suporta parâmetros sem nome, ter certeza que todos os parâmetros foram nomeados.
  4. Iterar sobre a lista de argumentos, criando objetos SqlParameter.
  5. Criar os resultados e armazená-los em um parâmetro de resultado. (Detalhes sobre isso serão mostrados depois).
  6. Retornar verdadeiro, indicando que o método foi processado com sucesso.

Public Overrides Function TryInvokeMember(

    ByVal binder As System.Dynamic.InvokeMemberBinder,

    ByVal args() As Object,

    ByRef result As Object) As Boolean

 

    Dim manageConnectionLifespan = (m_Connection.State = ConnectionState.Closed)

    If manageConnectionLifespan Then m_Connection.Open()

 

    Try

        Using cmd As New SqlClient.SqlCommand(binder.Name, m_Connection)

            cmd.CommandType = CommandType.StoredProcedure

 

            If binder.CallInfo.ArgumentNames.Count <> binder.CallInfo.ArgumentCount Then

                Throw New ArgumentException("All parameters must be named")

            End If

 

            For i = 0 To binder.CallInfo.ArgumentCount - 1

                Dim param As New SqlClient.SqlParameter

                param.ParameterName = "@" & binder.CallInfo.ArgumentNames(i)

                param.Value = If(args(i) Is Nothing, DBNull.Value, args(i))

                cmd.Parameters.Add(param)

            Next

 

            Select Case m_Options

                Case CallingOptions.Scalar

                    result = ExecuteScalar(cmd)

                Case CallingOptions.Single

                    result = ExecuteSingle(cmd)

                Case CallingOptions.List

                    result = ExecuteList(cmd)

                Case CallingOptions.MultipleLists

                    result = ExecuteMultpleLists(cmd)

                Case Else

                    Throw New ArgumentOutOfRangeException("options")

            End Select

        End Using

    Finally

        If manageConnectionLifespan Then m_Connection.Close()

    End Try

 

    Return True

End Function

ExecuteScalar é bem simples, o único motivo dele ter seu próprio método é por consistência.

Private Function ExecuteScalar(ByVal command As SqlCommand) As Object

    Dim temp = command.ExecuteScalar

    If temp Is DBNull.Value Then Return Nothing Else Return temp

End Function

Para o resto dos variantes, consumidores estão esperando propriedades reais ou pelo menos algo que pareça uma propriedade. Uma opção seria gerar o código das classes baseando-se no conteúdo gerado em tempo de execução pelos conjuntos de resultados. Mas gerar código em tempo de execução é caro e não vamos ganhar muito já que nossos consumidores não podem acessar as classes pelo nome. Então ao invés disso, vamos manter o tema de código dinâmico e usar um objeto dinâmico prototípico.

Friend Class MicroDataObject

    Inherits Dynamic.DynamicObject

    Private m_Values As New Dictionary(Of String, Object)(StringComparer.OrdinalIgnoreCase)

 

    Public Overrides Function TryGetMember(ByVal binder As System.Dynamic.GetMemberBinder, ByRef result As Object) As Boolean

        If m_Values.ContainsKey(binder.Name) Then result = m_Values(binder.Name) Else Throw New System.MissingMemberException("The property " & binder.Name & " does not exist")

        Return True

    End Function

 

    Public Overrides Function TrySetMember(ByVal binder As System.Dynamic.SetMemberBinder, ByVal value As Object) As Boolean

        SetMember(binder.Name, value)

        Return True

    End Function

 

    Public Overrides Function GetDynamicMemberNames() As System.Collections.Generic.IEnumerable(Of String)

        Return m_Values.Keys

    End Function

 

    Friend Sub SetMember(ByVal propertyName As String, ByVal value As Object)

        If value Is DBNull.Value Then m_Values(propertyName) = Nothing Else m_Values(propertyName) = value

    End Sub

 

End Class

Novamente, já que nenhuma classe deve ter dependência com este objeto nós o marcamos como Friend (C# internal). Assim precisamos sobrescrever 3 métodos: um para setar as propriedades, um para retorna-lase uma para listar seus nomes. Também há uma porta dos fundos para inicializar a classe usando código estático.

Private Function ExecuteSingle(ByVal command As SqlCommand) As Object

    Using reader = command.ExecuteReader

        If reader.Read Then

            Dim dataObject As New MicroDataObject

            For i = 0 To reader.FieldCount - 1

                dataObject.SetMember(reader.GetName(i), reader.GetValue(i))

            Next

            Return dataObject

        Else

            Return Nothing

        End If

    End Using

End Function

 

Private Function ExecuteList(ByVal command As SqlCommand) As List(Of MicroDataObject)

    Dim resultList = New List(Of MicroDataObject)

    Using reader = command.ExecuteReader

        Do While reader.Read

            Dim dataObject As New MicroDataObject

            For i = 0 To reader.FieldCount - 1

                dataObject.SetMember(reader.GetName(i), reader.GetValue(i))

            Next

            resultList.Add(dataObject)

        Loop

    End Using

    Return resultList

End Function

 

Private Function ExecuteMultpleLists(ByVal command As SqlCommand) As List(Of List(Of MicroDataObject))

    Dim resultSet As New List(Of List(Of MicroDataObject))

 

    Using reader = command.ExecuteReader

        Do

 

            Dim resultList = New List(Of MicroDataObject)

            Do While reader.Read

                Dim dataObject As New MicroDataObject

                For i = 0 To reader.FieldCount - 1

                    dataObject.SetMember(reader.GetName(i), reader.GetValue(i))

                Next

                resultList.Add(dataObject)

            Loop

            resultSet.Add(resultList)

 

        Loop While reader.NextResult

    End Using

 

    Return resultSet

End Function

Há bastante espaço para mexer em nosso "Micro ORM". Funcionalidades possíveis incluem suporte para parâmetros de saída, uma opção para enviar consultas parametrizadas ao invés de stored procedures e suporte à outras bases de dados.

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT