EFUtil, IRegraNegocio e serviços para o Entity Framework

Muito se têm falado do Entity Framework e aqui em Brasília não é diferente, onde eu tenho trabalhado com alguns clientes, discutindo sobre a melhor forma se utilizar o EF nos projetos. Dentro das restrições de tempo (algo que está mais do que apertado ultimamente) eu juntei algumas idéias que tive e gostaria de compartilhar com vocês, um pouquinho de cada vez.

Definição do problema: a empresa R2D2 vai utilizar o Entity Framework em um novo projeto (ponto para ela!) e está montando uma arquitetura de referência. Dentre os requisitos, eles querem tentar garantir ao máximo que os desenvolvedores programem de acordo com os padrões definidos e utilizem métodos de uma interface específica (para validação de negócio) junto com o Entity Framework.

Pensando nisso, podemos criar diversas maneiras para tentar garantir ao máximo que essas regras serão seguidas, como utilização de templates, geradores de código, modificação do gerador do Entity Framework, etc. Eu resolvi usar uma abordagem mais direta, com o uso de interfaces e, alguns subterfúgios, para que sejam feitas algumas validações das entidades.

Quer ver no que deu? Continue lendo o artigo e baixe o projeto de referência, anexado a esta postagem.

Solução 01 do brainstorm (feedbacks são mais do que bem vindos!!)

Devemos criar uma biblioteca de classes “EFUtil” com classes utilitárias que ofereçam serviços interessantes para o desenvolvedor. Dessa forma ele ficará compelido a usar os recursos disponibilizados e nós poderemos fazer alguns controles através desses métodos.

Detalhamento do projeto EFUtil

Classe PoliticasEF - Oferece métodos responsáveis pela verificação das políticas de desenvolvimento definidas pelo time de arquitetos, como por exemplo: toda entidade deve implementar a interface IRegraNegocio.

Interface IRegraNegocio - Interface que faz parte do EFUtil e define o método ValidaRegraNegocio, que deve ser implementado por toda entidade definida no modelo conceitual. Na versão 1.0 do EF precisamos usar uma interface, pois por padrão toda entidade já deriva de EntityObject. Quem sabe no futuro vamos usar uma classe abstrata e fazer com que as entidade derivem desta…

Classe ServicoContexto – Responsável por manter os métodos relacionados com os contextos do EF, como por exemplo, o método SalvarAlteracoes. Este seria responsável por chamar o método SaveChanges, fazer a validação das políticas, logging e, em caso de problemas de concorrência, aplicar a política definida para o projeto.

Partindo do que foi dito acima, para essa proposta todo desenvolvedor seria orientado para fazer o seguinte:

- Implementar a interface IRegraNegocio em uma classe parcial para todas as entidades definidas no seu modelo conceitual.
- Criar uma classe parcial para o contexto e implementar o método parcial OnContextCreated().

No exemplo que tenho, no projeto de acesso a dados “EFDataAccess”, criei um modelo edmx baseado no banco de dados SimpleDB (que somente possui a tabela funcionário) e implementei as regras definidas acima…

public partial class SimpleDBEntities
{
partial void OnContextCreated()
{
EFUtil.PoliticasEF.VerificaEntidades(this);
}
}

public partial class Funcionario: EFUtil.IRegraNegocio
{
#region IRegraNegocio Members

    public void ValidaRegraNegocio()
{
throw new Exception("Um erro qualquer de validação de negócio...");
}

    #endregion
}

Vamos ver agora como implementei as verificações e validações para tentar evitar que o desenvolvedor fugisse à regra…

O método VerificaEntidades analisa o espaço conceitual do modelo informado pesquisando todos os tipos “entidade” e, como estamos falando da entidade conceitual, não temos informação das interfaces que as classes parciais implementam, então precisamos usar reflexão. Dessa forma, para cada entidade recuperada, verificamos se o tipo recuperado pelo método GetType implementa a interface EFUtil.IRegraNegocio. Utilizo uma coleção de dicionário para verificar quais contextos já foram verificados, fazendo este controle através do hashcode do objeto.

public static void VerificaEntidades(ObjectContext contexto)
{
string nomeAssembly, nomeEntidade;
Type tipoEntidade;
Assembly AssemblyDA = Assembly.GetCallingAssembly();

// Mostra o nome do Assembly - EFDataAccess neste caso
nomeAssembly = AssemblyDA.GetName().Name;

            var entidades = from e in contexto.MetadataWorkspace.GetItems(System.Data.Metadata.Edm.DataSpace.CSpace)
where e.BuiltInTypeKind == System.Data.Metadata.Edm.BuiltInTypeKind.EntityType
select e;

            foreach (var e in entidades)
{
// Recupera o nome da entidade listada, ignorando o namespace do modelo conceitual, que normalmente é
// diferente do namespace definido no assembly
nomeEntidade = e.ToString().Split('.')[1];

                tipoEntidade = AssemblyDA.GetType(nomeAssembly + "." + nomeEntidade, true);
if (tipoEntidade.GetInterface("EFUtil.IRegraNegocio") == null)
throw new Exception("Entidade '" + nomeEntidade + "' não está implementando a interface EFUtil.IRegraNegocio");
}

            objetosVerificados.Add(contexto.GetHashCode(), DateTime.Now);
}

Obs 1: É necessário compormos o nome do tipo conforme exibido, porque o namespace do modelo conceitual é diferente do namespace primitivo do assembly, que por padrão é o nome do projeto. Por simplicidade eu estou considerando namespaces no formato namespace.entidade, mas é claro que isso pode ser diferente e deverá ser tratado de forma genérica, recuperando o último nome após o “.”.

Obs 2: Como estamos chamando o método VerificaEntidades através de uma classe parcial, é obrigatório que essa classe esteja na mesma DLL que o modelo, então eu posso assumir que o GetCallingAssembly está sempre se referindo ao módulo correto.

Para tentar aumentar a segurança e evitar que um desenvolvedor tente usufruir dos serviços disponibilizados pela classe ServicoContexto sem chamar o VerificaEntidades, no início do método SalvarAlteracoes() é feito uma validação, que pesquisa no dicionário se o objeto deste contexto já foi verificado.

internal static void JaFoiVerificado(ObjectContext contexto)
{
if (!objetosVerificados.ContainsKey(contexto.GetHashCode()))
throw new Exception("A validação do contexto não foi feita, verifique se existe uma classe parcial " +
"do contexto implementando o seguinte trecho de código: \n" +
"partial void OnContextCreated() { \n" +
"EFUtil.PoliticasEF.VerificaEntidades(this); \n" +
"}");
}

É também no SalvarAlteracoes que as regras de negócio de cada entidade são invocadas, de acordo com o código abaixo (note que por simplicidade eu somente estou verificando os itens que foram adicionados):

private static void ValidaRegras(ObjectContext contexto)
{
foreach (ObjectStateEntry item in contexto.ObjectStateManager.GetObjectStateEntries(EntityState.Added))
{
IRegraNegocio entidade = (IRegraNegocio)item.Entity;
entidade.ValidaRegraNegocio();
}
}

Conclusão e considerações

Com o exemplo acima eu não espero ter resolvido essa questão, muito pelo contrário, espero que essa postagem seja o início de uma discussão produtiva. Gostaria de fazer algumas considerações sobre o que foi codificado:

  • Se o desenvolvedor utilizar o método SalvarAlteracoes sem ter implementado corretamente o método parcial OnContextCreated, o JaFoiVerificado pega o problema.
  • Se o desenvolvedor implementar corretamente a classe parcial do contexto, mas esquecer de implementar em alguma entidade a interface IRegraNegocio, o problema será detectado pelo VerificaEntidades.
  • Se o desenvolvedor não implementar a interface e chamar o SaveChanges do contexto diretamente, não podemos fazer nada (com essa abordagem).
    • Comentando a interface e o método que chama o VerificaEntidades, vocês podem simular os cenário acima.
  • Fazer a validação da regras auxiliares (e até poder chamar outros métodos) em toda instância pode parecer um desperdício, mas talvez seja possível parametrizar algumas coisas e até criar novas sobrecargas para os construtores do contexto, que pode trazer uma flexibilidade interessante se pensarmos na configuração do contexto em subsistemas específicos.

Pessoal, notem que eu não quero com esse post entrar em detalhes qual a melhor arquitetura para sua aplicação, aonde é melhor colocar sua regra de negócio ou outra afirmação do tipo. Estou apenas mostrando algumas maneiras de como podemos utilizar o Entity Framework, que espero eu, possa ajudá-los no dia-a-dia.

Em anexo está tudo o que foi codificado, além de um outro modelo do northwind com mais entidades. Espero que vocês testem o projeto e tudo funcione corretamente, pois estou esperando o feedback de vocês. :-)
Em breve voltarei com adendos e este projeto, como controle de concorrência e logging…

[]s
Luciano Caixeta Moreira
luciano.moreira@microsoft.com
===============================================
This post is provided "AS IS" and confers no right
===============================================

EFBrainstorm.zip