Antes de mais nada, é importante que eu diga os pré-requisitos para que você possa ter um melhor aproveitamento deste artigo: Saber o básico de Linq To SQL (Criar o arquivo *.dbml, fazer Linq Expressions, etc).

1. Clientes Gordos X Clientes Magros

É muito importante também entender-mos em qual ambiente vamos trabalhar. Quando digo "ambiente", me refiro à aplicações de Clientes Gordos ou Clientes Magros, isto eu posso explicar:

Clientes GordosClientes Magros
A maioria do processamento dos dados e das regras de negócio são processadas no(s) Cliente(s) (Normalmente exemplificado por aplicações Desktop)A maioria do processamento dos dados e das regras de negócio são processadas no(s) Servidor(es) (Normalmente exemplificado por aplicações Web)

Por que é importante entender este princípio? A resposta é simples: Para definir-mos e apontar-mos quando faremos cache. Com relação à Clientes Gordos, trabalhare-mos com cache quase que todo o tempo, diferente dos Clientes Magros.

2. Business Objects

Se você pensou que este cache estaria relacionado aos objetos do Linq To SQL, você acertou! A idéia principal de fazer ou não fazer cache está fortemente ligada aos objetos do Linq To SQL em combinação com os conceitos de Business Objects. Sendo assim, nada mais natural que compreender os conceitos básicos de Business Objects:
  • Existe uma persistência de tuplas (registros de uma determinada tabela) em objetos instanciados;
  • Existe uma persistência de cada objeto instanciado em coleções (Listas) também instanciadas;
  • Existe o uso de sub-coleções de objetos dentro de um objeto para simular um relacionamento 1 X N;
  • Existe o uso de objetos dentro de outro objeto para simular um relacionamento N X 1 (Conhecido como "Parent" de um objeto);
  • As regras de negócio são encapsuladas nos objetos persistidos;
  • Em C# (nosso caso), criamos na maioria das vezes Propriedades que encapsulam estas regras;
  • É comum a criação de métodos que ancapsulam genericamente uma regra de negócio;
Entendo que para você que não conhece ou nunca trabalhou com Busines Objects, o conceito pode parecer um pouco distante, ou mesmo não muito didático da forma como foi apresentada acima. Veja as figuras abaixo mostrando como seria uma arquitetura de persistência e implementação com Business Objects utilizando os conceitos básicos:

Primeiro, veja o seguinte DER com uma simples composição de dados, onde uma Categoria pode estar relacionada com vários Produtos.

DER de Exemplo

Com base nisto, criei uma estrutura de classes onde procurei persistir exatamente este modelo entre os objetos, inclusive implementando algumas regras de negócio no comportamento dos objetos.

DIAGRAMA DE CLASSES - PERSISTÊNCIA COM BUSINESS OBJECTS

2.1. Explicando as Classes:
  • Classes: BusinessObjects.Categoria / BusinessObjects.Produto
Representam integralmente cada registro das tabelas Categoria ou Produto em tempo de instância, onde existem alguns Campos ou Propriedades públicas que representam cada coluna da tabela ou regras de negócio encapsuladas.
  • Classes: BusinessObjects.CategoriaList / BusinessObjects.ProdutoList
Representam integralmente uma coleção de objetos (tabela de registros) das tabelas Categoria ou Produto em tempo de instância.
  • Classe: System.Guid
Representa exatamente a classe (ou struct, se não me engano) System.Guid do .NET Framework.

2.2. Exemplicando as Propriedades e Métodos da classe Categoria:
  • + Guid : Guid / + Nome : string
São Propriedades padrões da classe, que representam exatamente as colunas da tabela Categoria.
  • + Produtos : ProdutoList
É uma propriedade que possui uma coleção de objetos da classe Produto onde as referências deles estão diretamente ligadas aos Produtos que pertencem à Categoria em questão, obedecendo o relacionamento (Categoria 1 X N Produto)
  • + SomaPreco : decimal
É uma propriedade que encapsula um somatório do Preco de cada Produto da lista interna de Produtos (Propriedade "Produtos").
  • + GetSomaPrecoProdutosByCategoria(In categoria : Categoria) : decimal
É um método estático que recebe como parâmetro um objeto da classe Categoria, com o objetivo de somar o Preco de cada Produto que compartilha o relacionamento com a Categoria em questão (parâmetro).

2.3. Exemplicando as Propriedades da classe Produto:
  • + Guid : Guid / + GuidCategoria : Guid / + Nome : string / + Preco : decimal
São Propriedades padrões da classe, que representam exatamente as colunas da tabela Produto.
  • + Categoria : Categoria
É uma propriedade que representa exatamente o "Parent" deste objeto. Lembrando que este "Parent" pode ser qualquer objeto que no modelo relacional, obedece a seguinte relação: Produto N X 1 Categoria.
  • + SomaPrecoProdutosMesmaCategoria : decimal
É uma propriedade que encapsula todo o processo de somar cada Preco de cada objeto da classe Produto que pertença à mesma Categoria que o produto em questão.

3. Juntando as peças

Agora que você já possui esta base de conhecimento, vamos juntar as peças e demonstrar o uso de Business Objects na prática com Linq To SQL.

Seguindo o mesmo DER passado acima, eu criei uma nova solução no Visual Studio 2008 com 3 projetos, veja:

Solution Explorer

É importante que as referências dos projetos sejam bem assimiladas. O que fiz para que estas referências estivessem desta forma foi:
  • BusinessObjects: Adicionei a referência para a DLL do .NET Framework 3.5 System.Data.Linq
  • WebAPP: O mesmo acima mais uma referência para o projeto BusinessObjects
  • DesktopAPP: O mesmo acima
3.1. Criando o arquivo Linq To SQL (*.dbml)

Com isto, já podemos criar nossos primeiros arquivos de código para descrever a persistência. No caso, criei um novo arquivo Linq To SQL (*.dbml) no projeto BusinessObjects com o nome do meu Banco de Dados (DBDevMedia):

Criando o arquivo DBML

Além disto, como nosso campo da tabela é do tipo Uniqueidentifier no SGBD (SQL Server 200X) e Guid (Genuine Unique Identifier) em C#, é preciso modificar uma propriedade deste campo que o Linq persistiu:

Setando o Auto Generate

Faça a mesma coisa para a tabela (agora classe persistida) Categoria. Este movimento é para que o Linq gere o valor do Guid automaticamente (isto é, se você colocou no banco de dados o Default Value desta coluna como sendo "(NEWID())" sem as aspas).

3.2. Criando a classe ContextManager para Clientes Gordos

Agora, eu criei um arquivo na raiz do projeto BusinessObjects com o nome de "ContextManagerClienteGordo" (o nome é somente para efeito didático do artigo, normalmente dou o nome de "ContextManager"), criando uma classe pública de mesmo nome:

Código da classe ContextManagerClienteGordo

Esta classe está encapsulando operações básicas para manipulação de transações com Linq To SQL, além de garantir uma única instância (cache) do DBDevMediaDataContext, que contém tudo o que é preciso para se trabalhar com os objetos do Linq To SQL na aplicação.

Na aplicação Desktop que mostrarei daqui a pouco, verá que não faremos instância desta classe, e sim usaremos sua propriedade DB ou seus métodos para manipulação de transação.

3.3. Criando a classe ContextManager para Clientes Magros

Seguindo os passos do tópico acima, eu criei mais um arquivo na raiz do projeto BusinessObjects com o nome de "ContextManagerClienteMagro", criando uma classe pública de mesmo nome:

Código da classe ContextManagerClienteMagro

Assim como a classe criada para Clientes Gordos, esta também encapsula operações básicas para manipulação de transações com Linq To SQL, porém, existe uma diferença notável: Não é feito um cache global do DBDevMediaDataContext, ou seja, faremos instância desta classe para utilizar seus recursos.

Na aplicação Web que mostrarei daqui a pouco, veremos com mais detalhes o uso desta classe.

3.4. Implementando as regras de negócio nos objetos

Localização do arquivo
Com as classes que gerenciam nosso DBDevMediaDataContext criadas para atender ambos os ambientes, Clientes Gordos e Clientes Magros, nosso objetivo passa a ser implementar as regras de negócio nos objetos.

Para isto, recomendo que você abra o arquivo "DBDevMedia.designer.cs" e explore seu conteúdo, principalmente na declaração das classes que representam as tabelas no SGBD.

Note que muitas classes e métodos possuem o modificador partial. Isto por que o Linq To SQL foi desenvolvido para ser altamente customizado às necessidades do desenvolvedor. Aí é que entra o conceito aplicado de Business Objects.

Quem nunca ouviu falar no modificador partial, agora é uma boa hora para aprender. Sendo curto e grosso, entende-se que você divide o conteúdo de uma classe em várias classes partial's.

Veja as classes que eu criei:

Código da calsse Categoria (partial)

Código da calsse Produto (partial)

Veja que estas são classes com o mesmo nome das classes persistidas pelo Linq To SQL, usando o modificador partial. Sendo assim, é possível fazer qualquer modificação ou implementação na classe sem afetar a versão originalmente persistida, possibilitando inclusive alterações na estrutura das tebelas no SGBD e atualização da persistência no arquivo DBML sem que as regras de negócio incluídas nas partial-classes sofram graves concequências.

Note também que em todas as expressões Linq que utilizei, a fonte de dados é exatamente a propriedade estática DB da classe ContextManagerClienteGordo que encapsulou exatamente o acesso ao DBDevMediaDataContext.

3.5. Populando o banco de dados

Neste momento, temos toda a persistência dos BusinessObjects montada, ou seja, objetos que representam muito bem o modelo relacional do SGBD em orientação a objeto e inclusive, a implementação de algumas regras de negócio nestes objetos.

Agora vamos popular nossas tabelas do banco, segue um script. Adapte-o se necessário:

USE [DBDevMedia];

DELETE FROM [Produto]
DELETE FROM [Categoria]

DECLARE @GUID_LIVRO AS UNIQUEIDENTIFIER
DECLARE @GUID_GAMES AS UNIQUEIDENTIFIER
DECLARE @GUID_DVD AS UNIQUEIDENTIFIER
DECLARE @GUID_CELULAR AS UNIQUEIDENTIFIER
DECLARE @GUID_ELETRODOMESTICO AS UNIQUEIDENTIFIER
DECLARE @GUID_INFORMATICA AS UNIQUEIDENTIFIER

SET @GUID_LIVRO = NEWID()
SET @GUID_GAMES = NEWID()
SET @GUID_DVD = NEWID()
SET @GUID_CELULAR = NEWID()
SET @GUID_ELETRODOMESTICO = NEWID()
SET @GUID_INFORMATICA = NEWID()

INSERT INTO [Categoria] VALUES ( @GUID_LIVRO, 'Livros' )
INSERT INTO [Categoria] VALUES ( @GUID_GAMES, 'Games' )
INSERT INTO [Categoria] VALUES ( @GUID_DVD, 'DVDs' )
INSERT INTO [Categoria] VALUES ( @GUID_CELULAR, 'Celulares' )
INSERT INTO [Categoria] VALUES ( @GUID_ELETRODOMESTICO, 'Eletrodomésticos' )
INSERT INTO [Categoria] VALUES ( @GUID_INFORMATICA, 'Informática' )

INSERT INTO [Produto] VALUES ( NEWID(), @GUID_LIVRO, 'Watchman - Edição Definitiva', 59 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_LIVRO, 'As Crônicas de Narnia - Volume Único', 75.5 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_LIVRO, 'A Cabeça de Steve Jobs', 12.9 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_LIVRO, 'Formaturas Infernais', 28 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_LIVRO, 'Harry Potter e o Enigma do Príncipe', 29 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_LIVRO, 'O código da inteigência', 15 )

INSERT INTO [Produto] VALUES ( NEWID(), @GUID_GAMES, 'Street Fighter IV', 249 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_GAMES, 'Call of Duty 5: World at War', 269.9 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_GAMES, 'Pro Evolution Soccer 2009', 229.9 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_GAMES, 'Prince of Percia', 199.9 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_GAMES, 'Far Cary 2', 239 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_GAMES, 'Gears of War 2', 179 )

INSERT INTO [Produto] VALUES ( NEWID(), @GUID_DVD, 'Piratas do Vale do Silício', 29.9 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_DVD, 'Coleção Batman', 59.9 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_DVD, 'O Diago Veste Prada', 26.9 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_DVD, '24 Horas - A Redenção', 39.9 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_DVD, 'Ensaio Sobre Cegueira', 39.9 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_DVD, 'Babel', 39.9 )

INSERT INTO [Produto] VALUES ( NEWID(), @GUID_CELULAR, 'Nokia N78', 799 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_CELULAR, 'Motorola K1', 349 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_CELULAR, 'Sony Ericsson T280i', 199 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_CELULAR, 'PalmTreo Pro GSM', 2599 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_CELULAR, 'iPhone 3G', 1989 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_CELULAR, 'Smartphone Q11 GSM', 1099 )

INSERT INTO [Produto] VALUES ( NEWID(), @GUID_ELETRODOMESTICO, 'Refrigerador Compacto 76,5L CRC08 Consul', 639 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_ELETRODOMESTICO, 'Lavadora de Roupas 9 Kg Total BWL09 Brastemp', 1097 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_ELETRODOMESTICO, 'Refrigerador Duplex Frost Free 403Lts KDN43A - Bosch', 1699 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_ELETRODOMESTICO, 'Forno Microondas 30L Ative BMS35B Branco Brastemp', 348 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_ELETRODOMESTICO, 'Adega Climatizada para 30 Garrafas GWC04B GE', 999 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_ELETRODOMESTICO, 'Ar Condicionado Split 12000 Btus Frio BBF12 220V Brastemp', 1399 )

INSERT INTO [Produto] VALUES ( NEWID(), @GUID_INFORMATICA, 'Macbook Core 2 Duo 2.4GHz 2GB 160GB DVD-RW 13.3´´ White - MB403LL/A', 3499 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_INFORMATICA, 'Notebook VAIO NS150 Core 2 Duo T5800 2.0GHz 3GB 250GB DVD-RW Sony', 3499 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_INFORMATICA, 'CPU I730 Core 2 Duo 4GB 500GB + LCD 22" Wide - AOC', 1599 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_INFORMATICA, 'Monitor LCD 22" Widescreen 212VA', 599 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_INFORMATICA, 'Multifuncional Jato de Tinta Deskjet F4280', 299 )
INSERT INTO [Produto] VALUES ( NEWID(), @GUID_INFORMATICA, 'MicroVault Click 4 GB USM4GL - Sony', 89 )


3.6. Implementando o DesktopApp

Com as tabelas populadas, vou criar uma aplicação desktop simples, fazendo uma leitura estilo "mestre detalhe" com dois DataGridView, onde no primeiro terei as categorias e no segundo os produtos da categoria selecionada no primeiro.

Sendo assim, primeiro eu vou criar um formulário de nome FRMTeste:



Depois vou adicionar um novo DataSource do tipo Object, apontando para a classe Categoria gerado pelo Linq To SQL:





Completando a sequência, teremos a estrutura dos Data Sources (Shift + ALt + D para abrir) conforme figura ao lado.

Agora basta arrastar o DataGridView (note pelo símbolo que aparece) Categoria e o DataGridView Produtos (note que ele está dentro de Categoria, por isto terei o efeito de "mestre detalhe":



Agora vou precisar preencher a propriedade DataSource do objeto "categoriaBindingSource" via código. Para isto, criei o evento Load do FRMTeste e coloquei o seguinte código:



Basta rodar a aplicação (F5) e ver o resultado:



Atente-se para a propriedade "SomaPreco" criada de forma customizada. Ela apareceu com o resultado da regra de negócio implementada no tópico 3.4.

3.7. Implementando o WebApp

Assim como no DesktopApp, vou criar um aplicativo web seguindo as mesmas idéias de "mestre detalhe", usando dois GridView com a ajuda de um UpdatePanel.

Vale lembrar que antes de continuar, vou precisar modificar alguns pontos das classes customizadas (partial) no projeto BusinessObjects. A alteração será no uso da classe ContextManagerClienteGordo para ContextManagerClienteMagro, lembrando que neste segundo, faremos uma instância:





Sendo assim, criei um formulário web de nome WFTeste:



Depois vou adicionar alguns componentes e configurá-los, segue o código ASPX:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="WFTeste.aspx.cs" Inherits="WFTeste" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:ScriptManager ID="ScriptManager1" runat="server">
        </asp:ScriptManager>
        <asp:UpdatePanel ID="UpdatePanel1" runat="server">
            <ContentTemplate>
                <asp:LinqDataSource ID="ldsCategoria" runat="server"
                    ContextTypeName="BusinessObjects.DBDevMediaDataContext" EnableDelete="True"
                    EnableInsert="True" EnableUpdate="True" TableName="Categorias">
                </asp:LinqDataSource>
                <asp:GridView ID="GridViewCategoria" runat="server" AllowSorting="True"
                    AutoGenerateColumns="False" DataKeyNames="Guid" DataSourceID="ldsCategoria"
                    ondatabound="GridViewCategoria_DataBound"
                    onselectedindexchanged="GridViewCategoria_SelectedIndexChanged">
                    <Columns>
                        <asp:CommandField ShowSelectButton="True" />
                        <asp:BoundField DataField="Guid" HeaderText="Guid" InsertVisible="False"
                            ReadOnly="True" SortExpression="Guid" />
                        <asp:BoundField DataField="Nome" HeaderText="Nome" SortExpression="Nome" />
                        <asp:BoundField DataField="SomaPreco" HeaderText="SomaPreco" ReadOnly="True"
                            SortExpression="SomaPreco" />
                    </Columns>
                </asp:GridView>
                <asp:LinqDataSource ID="ldsProduto" runat="server"
                    ContextTypeName="BusinessObjects.DBDevMediaDataContext"
                    onselecting="ldsProduto_Selecting" TableName="Produtos">
                    <WhereParameters>
                        <asp:Parameter Name="GuidCategoria" />
                    </WhereParameters>
                </asp:LinqDataSource>
                <asp:GridView ID="GridViewProduto" runat="server" AutoGenerateColumns="False"
                    DataKeyNames="Guid" DataSourceID="ldsProduto">
                    <Columns>
                        <asp:BoundField DataField="SomaPrecoProdutosMesmaCategoria"
                            HeaderText="SomaPrecoProdutosMesmaCategoria" ReadOnly="True"
                            SortExpression="SomaPrecoProdutosMesmaCategoria" />
                        <asp:BoundField DataField="Guid" HeaderText="Guid" InsertVisible="False"
                            ReadOnly="True" SortExpression="Guid" />
                        <asp:BoundField DataField="GuidCategoria" HeaderText="GuidCategoria"
                            SortExpression="GuidCategoria" />
                        <asp:BoundField DataField="Nome" HeaderText="Nome" SortExpression="Nome" />
                        <asp:BoundField DataField="Preco" HeaderText="Preco" SortExpression="Preco" />
                    </Columns>
                </asp:GridView>
            </ContentTemplate>
        </asp:UpdatePanel>
    </div>
    </form>
</body>
</html>

Abaixo o código C#:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using BusinessObjects;

public partial class WFTeste : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {

    }
    protected void ldsProduto_Selecting(object sender, LinqDataSourceSelectEventArgs e)
    {
        if (e.WhereParameters["GuidCategoria"] != null)
        {
            ContextManagerClienteMagro cm = new ContextManagerClienteMagro();

            e.Result = from p in cm.DB.Produtos
                       where p.GuidCategoria == new Guid(e.WhereParameters["GuidCategoria"].ToString())
                       select p;
        }
    }
    protected void GridViewCategoria_SelectedIndexChanged(object sender, EventArgs e)
    {
        this.DefineParameters();
    }
    protected void GridViewCategoria_DataBound(object sender, EventArgs e)
    {
        this.GridViewCategoria.SelectedIndex = 0;
        this.DefineParameters();
    }

    private void DefineParameters()
    {
        if (this.ldsProduto.WhereParameters != null && this.ldsProduto.WhereParameters.Count > 0)
        {
            this.ldsProduto.WhereParameters["GuidCategoria"].DefaultValue = this.GridViewCategoria.SelectedDataKey.Value.ToString();
            this.GridViewProduto.DataBind();
        }
    }
}

Agora é só rodar e veremos o resultado:



O resultado é bem similar ao DesktopApp, diferenciando no quesito cache, pois nosso WebApp faz uma instância dos objetos a todo momento em que precisa de ir ao banco de dados e fazer uma consulta.

É claro que podemos utilizar estes recursos de BusinessObjects para defirnir-mos várias carecterísticas na aplicação (além de um simples somatório) tanto para Web quanto para Desktop.

4. Conclusão

Neste artigo foi possível perceber que temos em nosso poder uma tecnologia completa de persistência de dados que se bem utilizada, independente dos conceitos utilizados (Business Objects ou não), teremos sucesso no projeto, principalmente no processo de desenvolvimento, que acaba se tornando divertido, rápido e abre um novo leque de possibilidades.

Pessoalmente, prefiro me concentrar e me aprofundar na criação de projetos que combinem Business Objects com Linq To SQL, principalmente por preservar o comportamento orientado a objetos dos dados (ou objetos) na aplicação.

Qualquer dúvida, sugestão ou crítica negativa (espero que esta não aconteça), entre em contato.