Partilhar via


Usando o Entity Framework 4.0 e o controle ObjectDataSource, parte 2: adicionando uma camada lógica de negócios e testes de unidade

por Tom Dykstra

Esta série de tutoriais se baseia no aplicativo Web da Contoso University criado pelo Introdução com a série de tutoriais do Entity Framework 4.0. Se você não concluiu os tutoriais anteriores, como ponto de partida para este tutorial, poderá baixar o aplicativo que teria criado. Você também pode baixar o aplicativo criado pela série de tutoriais completa. Se você tiver dúvidas sobre os tutoriais, poderá postá-los no fórum ASP.NET Entity Framework.

No tutorial anterior, você criou um aplicativo Web de n camadas usando o Entity Framework e o ObjectDataSource controle . Este tutorial mostra como adicionar lógica de negócios mantendo a BLL (camada de lógica de negócios) e a DAL (camada de acesso a dados) separadas e mostra como criar testes de unidade automatizados para a BLL.

Neste tutorial, você concluirá as seguintes tarefas:

  • Crie uma interface de repositório que declare os métodos de acesso a dados necessários.
  • Implemente a interface do repositório na classe de repositório.
  • Crie uma classe de lógica de negócios que chame a classe de repositório para executar funções de acesso a dados.
  • Conecte o ObjectDataSource controle à classe de lógica de negócios em vez de à classe de repositório.
  • Crie um projeto de teste de unidade e uma classe de repositório que usa coleções na memória para seu armazenamento de dados.
  • Crie um teste de unidade para a lógica de negócios que você deseja adicionar à classe de lógica de negócios e, em seguida, execute o teste e veja-o falhar.
  • Implemente a lógica de negócios na classe de lógica de negócios e execute novamente o teste de unidade e veja-o passar.

Você trabalhará com as páginas Departments.aspx e DepartmentsAdd.aspx criadas no tutorial anterior.

Criando uma interface do repositório

Você começará criando a interface do repositório.

Imagem08

Na pasta DAL , crie um novo arquivo de classe, nomeie-o ISchoolRepository.cs e substitua o código existente pelo seguinte código:

using System;
using System.Collections.Generic;

namespace ContosoUniversity.DAL
{
    public interface ISchoolRepository : IDisposable
    {
        IEnumerable<Department> GetDepartments();
        void InsertDepartment(Department department);
        void DeleteDepartment(Department department);
        void UpdateDepartment(Department department, Department origDepartment);
        IEnumerable<InstructorName> GetInstructorNames();
    }
}

A interface define um método para cada um dos métodos CRUD (criar, ler, atualizar, excluir) que você criou na classe de repositório.

SchoolRepository Na classe em SchoolRepository.cs, indique que essa classe implementa a ISchoolRepository interface:

public class SchoolRepository : IDisposable, ISchoolRepository

Criando uma classe Business-Logic

Em seguida, você criará a classe de lógica de negócios. Faça isso para que você possa adicionar a lógica de negócios que será executada pelo ObjectDataSource controle, embora ainda não faça isso. Por enquanto, a nova classe de lógica de negócios executará apenas as mesmas operações CRUD que o repositório faz.

Imagem09

Crie uma nova pasta e nomeie-a como BLL. (Em um aplicativo do mundo real, a camada de lógica de negócios normalmente seria implementada como uma biblioteca de classes — um projeto separado — mas, para manter este tutorial simples, as classes BLL serão mantidas em uma pasta de projeto.)

Na pasta BLL , crie um novo arquivo de classe, nomeie-o SchoolBL.cs e substitua o código existente pelo seguinte código:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using ContosoUniversity.DAL;

namespace ContosoUniversity.BLL
{
    public class SchoolBL : IDisposable
    {
        private ISchoolRepository schoolRepository;

        public SchoolBL()
        {
            this.schoolRepository = new SchoolRepository();
        }

        public SchoolBL(ISchoolRepository schoolRepository)
        {
            this.schoolRepository = schoolRepository;
        }

        public IEnumerable<Department> GetDepartments()
        {
            return schoolRepository.GetDepartments();
        }

        public void InsertDepartment(Department department)
        {
            try
            {
                schoolRepository.InsertDepartment(department);
            }
            catch (Exception ex)
            {
                //Include catch blocks for specific exceptions first,
                //and handle or log the error as appropriate in each.
                //Include a generic catch block like this one last.
                throw ex;
            }
        }

        public void DeleteDepartment(Department department)
        {
            try
            {
                schoolRepository.DeleteDepartment(department);
            }
            catch (Exception ex)
            {
                //Include catch blocks for specific exceptions first,
                //and handle or log the error as appropriate in each.
                //Include a generic catch block like this one last.
                throw ex;
            }
        }

        public void UpdateDepartment(Department department, Department origDepartment)
        {
            try
            {
                schoolRepository.UpdateDepartment(department, origDepartment);
            }
            catch (Exception ex)
            {
                //Include catch blocks for specific exceptions first,
                //and handle or log the error as appropriate in each.
                //Include a generic catch block like this one last.
                throw ex;
            }

        }

        public IEnumerable<InstructorName> GetInstructorNames()
        {
            return schoolRepository.GetInstructorNames();
        }

        private bool disposedValue = false;

        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposedValue)
            {
                if (disposing)
                {
                    schoolRepository.Dispose();
                }
            }
            this.disposedValue = true;
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

    }
}

Esse código cria os mesmos métodos CRUD que você viu anteriormente na classe de repositório, mas em vez de acessar os métodos do Entity Framework diretamente, ele chama os métodos de classe do repositório.

A variável de classe que contém uma referência à classe de repositório é definida como um tipo de interface e o código que cria uma instância da classe de repositório está contido em dois construtores. O construtor sem parâmetros será usado pelo ObjectDataSource controle . Ele cria uma instância da SchoolRepository classe que você criou anteriormente. O outro construtor permite que qualquer código que instancie a classe de lógica de negócios passe qualquer objeto que implemente a interface do repositório.

Os métodos CRUD que chamam a classe de repositório e os dois construtores possibilitam usar a classe de lógica de negócios com qualquer armazenamento de dados de back-end escolhido. A classe de lógica de negócios não precisa estar ciente de como a classe que está chamando persiste os dados. (Isso geralmente é chamado de ignorância de persistência.) Isso facilita o teste de unidade, pois você pode conectar a classe de lógica de negócios a uma implementação de repositório que usa algo tão simples quanto coleções na memória List para armazenar dados.

Observação

Tecnicamente, os objetos de entidade ainda não ignoram persistência, pois são instanciados de classes que herdam da classe do EntityObject Entity Framework. Para a ignorância de persistência completa, você pode usar objetos CLR antigos simples, ou POCOs, no lugar de objetos que herdam da EntityObject classe . O uso de POCOs está além do escopo deste tutorial. Para obter mais informações, consulte Testability e Entity Framework 4.0 no site do MSDN.)

Agora você pode conectar os ObjectDataSource controles à classe de lógica de negócios em vez de ao repositório e verificar se tudo funciona como antes.

Em Departments.aspx e DepartmentsAdd.aspx, altere cada ocorrência de TypeName="ContosoUniversity.DAL.SchoolRepository" para TypeName="ContosoUniversity.BLL.SchoolBL". (Há quatro instâncias no total.)

Execute as páginas Departments.aspx e DepartmentsAdd.aspx para verificar se elas ainda funcionam como antes.

Imagem01

Imagem02

Criando um projeto Unit-Test e implementação de repositório

Adicione um novo projeto à solução usando o modelo projeto de teste e nomeie-o ContosoUniversity.Testscomo .

No projeto de teste, adicione uma referência a System.Data.Entity e adicione uma referência de projeto ao ContosoUniversity projeto.

Agora você pode criar a classe de repositório que usará com testes de unidade. O armazenamento de dados para esse repositório estará dentro da classe .

Imagem12

No projeto de teste, crie um novo arquivo de classe, nomeie-o como MockSchoolRepository.cs e substitua o código existente pelo seguinte código:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ContosoUniversity.DAL;
using ContosoUniversity.BLL;

namespace ContosoUniversity.Tests
{
    class MockSchoolRepository : ISchoolRepository, IDisposable
    {
        List<Department> departments = new List<Department>();
        List<InstructorName> instructors = new List<InstructorName>();

        public IEnumerable<Department> GetDepartments()
        {
            return departments;
        }

        public void InsertDepartment(Department department)
        {
            departments.Add(department);
        }

        public void DeleteDepartment(Department department)
        {
            departments.Remove(department);
        }

        public void UpdateDepartment(Department department, Department origDepartment)
        {
            departments.Remove(origDepartment);
            departments.Add(department);
        }

        public IEnumerable<InstructorName> GetInstructorNames()
        {
            return instructors;
        }

        public void Dispose()
        {
            
        }
    }
}

Essa classe de repositório tem os mesmos métodos CRUD que aquele que acessa diretamente o Entity Framework, mas eles funcionam com List coleções na memória em vez de com um banco de dados. Isso torna mais fácil para uma classe de teste configurar e validar testes de unidade para a classe de lógica de negócios.

Criando testes de unidade

O modelo de projeto de teste criou uma classe de teste de unidade stub para você e sua próxima tarefa é modificar essa classe adicionando métodos de teste de unidade a ela para a lógica de negócios que você deseja adicionar à classe de lógica de negócios.

Imagem13

Na Contoso University, qualquer instrutor individual só pode ser o administrador de um único departamento e você precisa adicionar lógica de negócios para impor essa regra. Você começará adicionando testes e executando os testes para vê-los falhar. Em seguida, você adicionará o código e executará novamente os testes, esperando vê-los passar.

Abra o arquivo UnitTest1.cs e adicione using instruções para a lógica de negócios e as camadas de acesso a dados que você criou no projeto ContosoUniversity:

using ContosoUniversity.BLL;
using ContosoUniversity.DAL;

Substitua o TestMethod1 método pelos seguintes métodos:

private SchoolBL CreateSchoolBL()
{
    var schoolRepository = new MockSchoolRepository();
    var schoolBL = new SchoolBL(schoolRepository);
    schoolBL.InsertDepartment(new Department() { Name = "First Department", DepartmentID = 0, Administrator = 1, Person = new Instructor () { FirstMidName = "Admin", LastName = "One" } });
    schoolBL.InsertDepartment(new Department() { Name = "Second Department", DepartmentID = 1, Administrator = 2, Person = new Instructor() { FirstMidName = "Admin", LastName = "Two" } });
    schoolBL.InsertDepartment(new Department() { Name = "Third Department", DepartmentID = 2, Administrator = 3, Person = new Instructor() { FirstMidName = "Admin", LastName = "Three" } });
    return schoolBL;
}

[TestMethod]
[ExpectedException(typeof(DuplicateAdministratorException))]
public void AdministratorAssignmentRestrictionOnInsert()
{
    var schoolBL = CreateSchoolBL();
    schoolBL.InsertDepartment(new Department() { Name = "Fourth Department", DepartmentID = 3, Administrator = 2, Person = new Instructor() { FirstMidName = "Admin", LastName = "Two" } });
}

[TestMethod]
[ExpectedException(typeof(DuplicateAdministratorException))]
public void AdministratorAssignmentRestrictionOnUpdate()
{
    var schoolBL = CreateSchoolBL();
    var origDepartment = (from d in schoolBL.GetDepartments()
                          where d.Name == "Second Department"
                          select d).First();
    var department = (from d in schoolBL.GetDepartments()
                          where d.Name == "Second Department"
                          select d).First();
    department.Administrator = 1;
    schoolBL.UpdateDepartment(department, origDepartment);
}

O CreateSchoolBL método cria uma instância da classe de repositório que você criou para o projeto de teste de unidade, que, em seguida, passa para uma nova instância da classe de lógica de negócios. Em seguida, o método usa a classe de lógica de negócios para inserir três departamentos que você pode usar em métodos de teste.

Os métodos de teste verificam se a classe de lógica empresarial gera uma exceção se alguém tentar inserir um novo departamento com o mesmo administrador que um departamento existente ou se alguém tentar atualizar o administrador de um departamento definindo-o para a ID de uma pessoa que já é administrador de outro departamento.

Você ainda não criou a classe de exceção, portanto, esse código não será compilado. Para que ele seja compilado, clique com o botão DuplicateAdministratorException direito do mouse e selecione Gerar e, em seguida, Classe.

Captura de tela que mostra Gerar selecionado no submenu Classe.

Isso cria uma classe no projeto de teste que você pode excluir depois de criar a classe de exceção no projeto main. e implementou a lógica de negócios.

Execute o projeto de teste. Conforme o esperado, os testes falham.

Imagem03

Adicionando lógica de negócios para fazer uma aprovação de teste

Em seguida, você implementará a lógica de negócios que torna impossível definir como administrador de um departamento alguém que já é administrador de outro departamento. Você lançará uma exceção da camada de lógica de negócios e a capturará na camada de apresentação se um usuário editar um departamento e clicar em Atualizar depois de selecionar alguém que já seja um administrador. (Você também pode remover instrutores da lista suspensa que já são administradores antes de renderizar a página, mas a finalidade aqui é trabalhar com a camada de lógica de negócios.)

Comece criando a classe de exceção que você lançará quando um usuário tentar tornar um instrutor o administrador de mais de um departamento. No projeto main, crie um novo arquivo de classe na pasta BLL, nomeie-o duplicateAdministratorException.cs e substitua o código existente pelo seguinte código:

using System;

namespace ContosoUniversity.BLL
{
    public class DuplicateAdministratorException : Exception
    {
        public DuplicateAdministratorException(string message)
            : base(message)
        {
        }
    }
}

Agora, exclua o arquivo DuplicateAdministratorException.cs temporário que você criou no projeto de teste anteriormente para poder compilar.

No projeto main, abra o arquivo SchoolBL.cs e adicione o método a seguir que contém a lógica de validação. (O código refere-se a um método que você criará posteriormente.)

private void ValidateOneAdministratorAssignmentPerInstructor(Department department)
{
    if (department.Administrator != null)
    {
        var duplicateDepartment = schoolRepository.GetDepartmentsByAdministrator(department.Administrator.GetValueOrDefault()).FirstOrDefault();
        if (duplicateDepartment != null && duplicateDepartment.DepartmentID != department.DepartmentID)
        {
            throw new DuplicateAdministratorException(String.Format(
                "Instructor {0} {1} is already administrator of the {2} department.", 
                duplicateDepartment.Person.FirstMidName, 
                duplicateDepartment.Person.LastName, 
                duplicateDepartment.Name));
        }
    }
}

Você chamará esse método quando estiver inserindo ou atualizando Department entidades para marcar se outro departamento já tem o mesmo administrador.

O código chama um método para pesquisar no banco de dados uma Department entidade que tenha o mesmo Administrator valor de propriedade que a entidade que está sendo inserida ou atualizada. Se um for encontrado, o código gerará uma exceção. Nenhuma marcar de validação será necessária se a entidade que está sendo inserida ou atualizada não tiver nenhum Administrator valor e nenhuma exceção for gerada se o método for chamado durante uma atualização e a Department entidade encontrada corresponder à Department entidade que está sendo atualizada.

Chame o novo método dos Insert métodos e Update :

public void InsertDepartment(Department department)
{
    ValidateOneAdministratorAssignmentPerInstructor(department);
    try
    ...

public void UpdateDepartment(Department department, Department origDepartment)
{
    ValidateOneAdministratorAssignmentPerInstructor(department);
    try
    ...

Em ISchoolRepository.cs, adicione a seguinte declaração para o novo método de acesso a dados:

IEnumerable<Department> GetDepartmentsByAdministrator(Int32 administrator);

Em SchoolRepository.cs, adicione a seguinte using instrução:

using System.Data.Objects;

Em SchoolRepository.cs, adicione o seguinte novo método de acesso a dados:

public IEnumerable<Department> GetDepartmentsByAdministrator(Int32 administrator)
{
    return new ObjectQuery<Department>("SELECT VALUE d FROM Departments as d", context, MergeOption.NoTracking).Include("Person").Where(d => d.Administrator == administrator).ToList();
}

Esse código recupera entidades Department que têm um administrador especificado. Somente um departamento deve ser encontrado (se houver). No entanto, como nenhuma restrição é incorporada ao banco de dados, o tipo de retorno é uma coleção no caso de vários departamentos serem encontrados.

Por padrão, quando o contexto do objeto recupera entidades do banco de dados, ele as controla no gerenciador de estado do objeto. O MergeOption.NoTracking parâmetro especifica que esse acompanhamento não será feito para essa consulta. Isso é necessário porque a consulta pode retornar a entidade exata que você está tentando atualizar e, em seguida, você não seria capaz de anexar essa entidade. Por exemplo, se você editar o departamento de Histórico na página Departments.aspx e deixar o administrador inalterado, essa consulta retornará o departamento de Histórico. Se NoTracking não estiver definido, o contexto do objeto já terá a entidade do departamento de Histórico em seu gerenciador de estado de objeto. Em seguida, quando você anexa a entidade do departamento de Histórico recriada do estado de exibição, o contexto do objeto gera uma exceção que diz "An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key".

(Como alternativa à especificação MergeOption.NoTrackingde , você pode criar um novo contexto de objeto apenas para essa consulta. Como o novo contexto de objeto teria seu próprio gerenciador de estado de objeto, não haveria conflito quando você chama o Attach método . O novo contexto de objeto compartilharia metadados e conexão de banco de dados com o contexto do objeto original, portanto, a penalidade de desempenho dessa abordagem alternativa seria mínima. A abordagem mostrada aqui, no entanto, apresenta a opção NoTracking , que você achará útil em outros contextos. A NoTracking opção é discutida mais adiante em um tutorial posterior nesta série.)

No projeto de teste, adicione o novo método de acesso a dados a MockSchoolRepository.cs:

public IEnumerable<Department> GetDepartmentsByAdministrator(Int32 administrator)
{
    return (from d in departments
            where d.Administrator == administrator
            select d);
}

Esse código usa LINQ para executar a mesma seleção de dados para a qual o ContosoUniversity repositório de projeto usa LINQ to Entities.

Execute o projeto de teste novamente. Dessa vez os testes são aprovados.

Imagem04

Manipulando exceções objectDataSource

ContosoUniversity No projeto, execute a página Departments.aspx e tente alterar o administrador de um departamento para alguém que já é administrador de outro departamento. (Lembre-se de que você só pode editar departamentos adicionados durante este tutorial, pois o banco de dados vem pré-carregado com dados inválidos.) Você obtém a seguinte página de erro do servidor:

Imagem05

Você não quer que os usuários vejam esse tipo de página de erro, portanto, você precisa adicionar código de tratamento de erros. Abra Departments.aspx e especifique um manipulador para o OnUpdated evento do DepartmentsObjectDataSource. A ObjectDataSource marca de abertura agora é semelhante ao exemplo a seguir.

<asp:ObjectDataSource ID="DepartmentsObjectDataSource" runat="server" 
        TypeName="ContosoUniversity.BLL.SchoolBL"
        DataObjectTypeName="ContosoUniversity.DAL.Department" 
        SelectMethod="GetDepartments" 
        DeleteMethod="DeleteDepartment" 
        UpdateMethod="UpdateDepartment"
        ConflictDetection="CompareAllValues"
        OldValuesParameterFormatString="orig{0}" 
        OnUpdated="DepartmentsObjectDataSource_Updated" >

Em Departments.aspx.cs, adicione a seguinte using instrução:

using ContosoUniversity.BLL;

Adicione o seguinte manipulador para o Updated evento:

protected void DepartmentsObjectDataSource_Updated(object sender, ObjectDataSourceStatusEventArgs e)
{
    if (e.Exception != null)
    {
        if (e.Exception.InnerException is DuplicateAdministratorException)
        {
            var duplicateAdministratorValidator = new CustomValidator();
            duplicateAdministratorValidator.IsValid = false;
            duplicateAdministratorValidator.ErrorMessage = "Update failed: " + e.Exception.InnerException.Message;
            Page.Validators.Add(duplicateAdministratorValidator);
            e.ExceptionHandled = true;
        }
    }
}

Se o ObjectDataSource controle capturar uma exceção quando tentar executar a atualização, ele passará a exceção no argumento event (e) para esse manipulador. O código no manipulador verifica se a exceção é a exceção de administrador duplicado. Se for, o código criará um controle validador que contém uma mensagem de erro para o ValidationSummary controle exibir.

Execute a página e tente tornar alguém o administrador de dois departamentos novamente. Desta vez, o ValidationSummary controle exibe uma mensagem de erro.

Image06

Faça alterações semelhantes na página DepartmentsAdd.aspx . Em DepartmentsAdd.aspx, especifique um manipulador para o OnInserted evento do DepartmentsObjectDataSource. A marcação resultante será semelhante ao exemplo a seguir.

<asp:ObjectDataSource ID="DepartmentsObjectDataSource" runat="server" 
        TypeName="ContosoUniversity.BLL.SchoolBL" DataObjectTypeName="ContosoUniversity.DAL.Department" 
        InsertMethod="InsertDepartment"  
        OnInserted="DepartmentsObjectDataSource_Inserted">

Em DepartmentsAdd.aspx.cs, adicione a mesma using instrução:

using ContosoUniversity.BLL;

Adicione o seguinte manipulador de eventos:

protected void DepartmentsObjectDataSource_Inserted(object sender, ObjectDataSourceStatusEventArgs e)
{
    if (e.Exception != null)
    {
        if (e.Exception.InnerException is DuplicateAdministratorException)
        {
            var duplicateAdministratorValidator = new CustomValidator();
            duplicateAdministratorValidator.IsValid = false;
            duplicateAdministratorValidator.ErrorMessage = "Insert failed: " + e.Exception.InnerException.Message;
            Page.Validators.Add(duplicateAdministratorValidator);
            e.ExceptionHandled = true;
        }
    }
}

Agora você pode testar a página DepartmentsAdd.aspx.cs para verificar se ela também lida corretamente com as tentativas de tornar uma pessoa a administradora de mais de um departamento.

Isso conclui a introdução à implementação do padrão de repositório para usar o ObjectDataSource controle com o Entity Framework. Para obter mais informações sobre o padrão de repositório e a capacidade de teste, consulte o white paper Testability do MSDN e o Entity Framework 4.0.

No tutorial a seguir, você verá como adicionar a funcionalidade de classificação e filtragem ao aplicativo.