Sdílet prostřednictvím


Použití Entity Framework 4.0 a ObjectDataSource Ovládací prvek, Část 2: Přidání vrstvy obchodní logiky a testů jednotek

Tom Dykstra

Tato série kurzů vychází z webové aplikace Contoso University vytvořené Začínáme s řadou kurzů Entity Framework 4.0. Pokud jste nedokončí předchozí kurzy, můžete si jako výchozí bod pro tento kurz stáhnout aplikaci , kterou jste vytvořili. Můžete si také stáhnout aplikaci vytvořenou kompletní řadou kurzů. Pokud máte dotazy k kurzům, můžete je publikovat na fóru ASP.NET Entity Framework.

V předchozím kurzu jste vytvořili n-vrstvou webovou aplikaci pomocí Entity Frameworku ObjectDataSource a ovládacího prvku. V tomto kurzu se dozvíte, jak přidat obchodní logiku a zároveň zachovat oddělené vrstvy obchodní logiky (BLL) a vrstvu pro přístup k datům (DAL), a ukazuje, jak vytvořit automatizované testy jednotek pro BLL.

V tomto kurzu dokončíte následující úlohy:

  • Vytvořte rozhraní úložiště, které deklaruje metody přístupu k datům, které potřebujete.
  • Implementujte rozhraní úložiště ve třídě úložiště.
  • Vytvořte třídu obchodní logiky, která volá třídu úložiště pro provádění funkcí přístupu k datům.
  • ObjectDataSource Připojte ovládací prvek k třídě obchodní logiky místo k třídě úložiště.
  • Vytvořte projekt testování jednotek a třídu úložiště, která pro své úložiště dat používá kolekce v paměti.
  • Vytvořte test jednotek pro obchodní logiku, který chcete přidat do třídy obchodní logiky, a pak test spusťte a uvidíte, že selže.
  • Implementujte obchodní logiku ve třídě obchodní logiky, pak znovu spusťte test jednotek a podívejte se, jak projde.

Budete pracovat se stránkami Departments.aspx a DepartmentsAdd.aspx , které jste vytvořili v předchozím kurzu.

Vytvoření rozhraní úložiště

Začnete vytvořením rozhraní úložiště.

Obrázek08

Ve složce DAL vytvořte nový soubor třídy, pojmenujte ho ISchoolRepository.cs a nahraďte stávající kód následujícím kódem:

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();
    }
}

Rozhraní definuje jednu metodu pro každou z metod CRUD (vytvoření, čtení, aktualizace, odstranění), které jste vytvořili ve třídě úložiště.

SchoolRepository Ve třídě v souboru SchoolRepository.cs uveďte, že tato třída implementuje ISchoolRepository rozhraní:

public class SchoolRepository : IDisposable, ISchoolRepository

Vytvoření třídy Business-Logic

Dále vytvoříte třídu obchodní logiky. Uděláte to tak, abyste mohli přidat obchodní logiku ObjectDataSource , kterou ovládací prvek spustí, i když to ještě neuděláte. V tuto chvíli bude nová třída obchodní logiky provádět pouze stejné operace CRUD jako úložiště.

Obrázek09

Vytvořte novou složku a pojmenujte ji BLL. (V reálné aplikaci se vrstva obchodní logiky obvykle implementuje jako knihovna tříd – samostatný projekt , ale aby byl tento kurz jednoduchý, budou třídy BLL uloženy ve složce projektu.)

Ve složce BLL vytvořte nový soubor třídy, pojmenujte ho SchoolBL.cs a nahraďte stávající kód následujícím kódem:

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);
        }

    }
}

Tento kód vytvoří stejné metody CRUD, které jste viděli dříve ve třídě úložiště, ale místo přímého přístupu k metodám Entity Framework volá metody třídy úložiště.

Proměnná třídy, která obsahuje odkaz na třídu úložiště, je definována jako typ rozhraní a kód, který vytváří instanci třídy úložiště, je obsažen ve dvou konstruktorech. Konstruktor bez parametrů bude použit ovládacím ObjectDataSource prvku. Vytvoří instanci SchoolRepository třídy, kterou jste vytvořili dříve. Druhý konstruktor umožňuje, aby jakýkoli kód, který vytváří instanci třídy obchodní logiky, předal jakýkoli objekt, který implementuje rozhraní úložiště.

Metody CRUD, které volají třídu úložiště a dva konstruktory, umožňují použití třídy obchodní logiky s jakýmkoli back-endovým úložištěm dat, které zvolíte. Třída obchodní logiky nemusí vědět, jak třída, kterou volá, udržuje data. (Tomu se často říká trvalost nevědomost.) To usnadňuje testování jednotek, protože můžete připojit třídu obchodní logiky k implementaci úložiště, která k ukládání dat používá něco tak jednoduchého, jako jsou kolekce v paměti List .

Poznámka

Technicky vzato nejsou objekty entit stále trvalé, protože se vytvářejí z tříd, které dědí z třídy Entity Framework EntityObject . Pro úplnou trvalost nevědomosti můžete místo objektů, které dědí z EntityObject třídy, použít prosté staré objekty CLR neboli POCO. Použití objektů POCO je nad rámec tohoto kurzu. Další informace najdete v tématech Testovatelnost a Entity Framework 4.0 na webu MSDN.)

Teď můžete ovládací prvky připojit ObjectDataSource k třídě obchodní logiky místo k úložišti a ověřit, že všechno funguje jako předtím.

V části Departments.aspx a DepartmentsAdd.aspx změňte jednotlivé výskyty TypeName="ContosoUniversity.DAL.SchoolRepository" na TypeName="ContosoUniversity.BLL.SchoolBL". (Ve všech jsou čtyři instance.)

Spusťte stránky Departments.aspx a DepartmentsAdd.aspx a ověřte, že fungují stejně jako předtím.

Obrázek01

Obrázek02

Vytvoření implementace projektu a úložiště Unit-Test

Přidejte do řešení nový projekt pomocí šablony Testovací projekt a pojmenujte ho ContosoUniversity.Tests.

V testovacím projektu přidejte odkaz na System.Data.Entity projekt a přidejte do ContosoUniversity projektu odkaz na projekt.

Teď můžete vytvořit třídu úložiště, kterou budete používat s testy jednotek. Úložiště dat pro toto úložiště bude v rámci třídy .

Obrázek 12

V testovacím projektu vytvořte nový soubor třídy, pojmenujte ho MockSchoolRepository.cs a nahraďte existující kód následujícím kódem:

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()
        {
            
        }
    }
}

Tato třída úložiště má stejné metody CRUD jako ta, která přistupuje k Entity Frameworku přímo, ale pracují s kolekcemi List v paměti místo s databází. To usnadňuje testovací třídě nastavení a ověření testů jednotek pro třídu obchodní logiky.

Vytváření testů jednotek

Šablona projektu Test pro vás vytvořila testovací třídu jednotek se zástupným inzerováním a vaším dalším úkolem je upravit tuto třídu přidáním metod testování jednotek pro obchodní logiku, kterou chcete přidat do třídy obchodní logiky.

Obrázek 13

Na univerzitě Contoso může být každý jednotlivý instruktor správcem pouze jednoho oddělení a k vynucení tohoto pravidla je potřeba přidat obchodní logiku. Začnete přidáním testů a spuštěním testů, abyste viděli, že se nezdaří. Pak přidáte kód a znovu spustíte testy s očekáváním, že je projdou.

Otevřete soubor UnitTest1.cs a přidejte using příkazy pro vrstvy obchodní logiky a přístupu k datům, které jste vytvořili v projektu ContosoUniversity:

using ContosoUniversity.BLL;
using ContosoUniversity.DAL;

Nahraďte metodu TestMethod1 následujícími metodami:

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);
}

Metoda CreateSchoolBL vytvoří instanci třídy úložiště, kterou jste vytvořili pro projekt testování jednotek, kterou pak předá nové instanci třídy obchodní logiky. Metoda pak použije třídu obchodní logiky k vložení tří oddělení, která můžete použít v testovacích metodách.

Testovací metody ověřují, že třída obchodní logiky vyvolá výjimku, pokud se někdo pokusí vložit nové oddělení se stejným správcem jako stávající oddělení, nebo pokud se někdo pokusí aktualizovat správce oddělení tak, že ho nastaví na ID osoby, která už je správcem jiného oddělení.

Ještě jste nevytvořili třídu výjimky, takže tento kód nebude kompilován. Chcete-li jej zkompilovat, klikněte pravým tlačítkem myši DuplicateAdministratorException a vyberte Generovat a pak třída.

Snímek obrazovky znázorňující vybranou možnost Generovat v podnabídce Třída

Tím se v testovacím projektu vytvoří třída, kterou můžete odstranit po vytvoření třídy výjimky v hlavním projektu. a implementovali obchodní logiku.

Spusťte testovací projekt. Podle očekávání testy selžou.

Obrázek03

Přidání obchodní logiky pro úspěšné provedení testu

Dále implementujete obchodní logiku, která znemožňuje nastavit jako správce oddělení někoho, kdo už je správcem jiného oddělení. Vyvoláte výjimku z vrstvy obchodní logiky a pak ji zachytíte v prezentační vrstvě, pokud uživatel upraví oddělení a klikne na Aktualizovat poté, co vybere někoho, kdo už je správcem. (Z rozevíracího seznamu můžete také odebrat instruktory, kteří už jsou před vykreslením stránky správci, ale účelem je pracovat s vrstvou obchodní logiky.)

Začněte vytvořením třídy výjimky, kterou vyvoláte, když se uživatel pokusí nastavit instruktora jako správce více než jednoho oddělení. V hlavním projektu vytvořte ve složce BLL nový soubor třídy, pojmenujte ho DuplicateAdministratorException.cs a nahraďte existující kód následujícím kódem:

using System;

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

Teď odstraňte dočasný soubor DuplicateAdministratorException.cs , který jste dříve vytvořili v testovacím projektu, aby bylo možné kompilovat.

V hlavním projektu otevřete soubor SchoolBL.cs a přidejte následující metodu, která obsahuje logiku ověřování. (Kód odkazuje na metodu, kterou vytvoříte později.)

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));
        }
    }
}

Tuto metodu zavoláte při vkládání nebo aktualizaci Department entit, abyste zkontrolovali, jestli už jiné oddělení má stejného správce.

Kód volá metodu pro vyhledání entity, která má stejnou DepartmentAdministrator hodnotu vlastnosti jako vložená nebo aktualizovaná entita. Pokud se najde, kód vyvolá výjimku. Pokud vložená nebo aktualizovaná entita nemá žádnou Administrator hodnotu a není vyvolána žádná výjimka, pokud je metoda volána během aktualizace a Department nalezená entita odpovídá Department aktualizované entitě, nevyžaduje se žádná ověřovací kontrola.

Volejte novou metodu Insert z metod a Update :

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

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

V souboru ISchoolRepository.cs přidejte následující deklaraci pro novou metodu přístupu k datům:

IEnumerable<Department> GetDepartmentsByAdministrator(Int32 administrator);

V souboru SchoolRepository.cs přidejte následující using příkaz:

using System.Data.Objects;

V souboru SchoolRepository.cs přidejte následující novou metodu přístupu k datům:

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();
}

Tento kód načte Department entity, které mají zadaného správce. Mělo by být nalezeno pouze jedno oddělení (pokud existuje). Vzhledem k tomu, že do databáze není zabudována žádná omezení, je návratovým typem kolekce pro případ, že se najde více oddělení.

Když kontext objektu ve výchozím nastavení načte entity z databáze, sleduje je ve správci stavu objektů. Parametr MergeOption.NoTracking určuje, že toto sledování se pro tento dotaz neprovedou. To je nezbytné, protože dotaz může vrátit přesnou entitu, kterou se pokoušíte aktualizovat, a pak nebudete moct tuto entitu připojit. Pokud například upravíte oddělení historie na stránce Departments.aspx a ponecháte správce beze změny, vrátí tento dotaz oddělení historie. Pokud NoTracking není nastavená, kontext objektu by již měl entitu Oddělení historie ve správci stavu objektu. Když pak připojíte entitu Oddělení historie, která se znovu vytvořila ze stavu zobrazení, kontext objektu vyvolá výjimku s textem "An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key".

(Jako alternativu k zadání MergeOption.NoTrackingmůžete vytvořit nový kontext objektu jen pro tento dotaz. Vzhledem k tomu, že nový kontext objektu by měl vlastní správce stavu objektu, při volání metody nedojde ke Attach konfliktu. Nový kontext objektu by sdílel metadata a připojení k databázi s původním kontextem objektu, takže výkon tohoto alternativního přístupu by byl minimální. Zde uvedený přístup však zavádí NoTracking možnost, která se vám bude hodit v jiných kontextech. Možnost NoTracking je podrobněji popsána v pozdějším kurzu v této sérii.)

V testovacím projektu přidejte novou metodu přístupu k datům do souboru MockSchoolRepository.cs:

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

Tento kód používá LINQ k provedení stejného výběru dat, pro který ContosoUniversity úložiště projektu používá LINQ to Entities.

Spusťte testovací projekt znovu. Tentokrát testy projdou.

Obrázek04

Zpracování výjimek ObjectDataSource

ContosoUniversity V projektu spusťte stránku Departments.aspx a zkuste změnit správce oddělení na někoho, kdo už je správcem jiného oddělení. (Nezapomeňte, že můžete upravovat jenom oddělení, která jste přidali během tohoto kurzu, protože databáze je předinstalovaná s neplatnými daty.) Zobrazí se následující chybová stránka serveru:

Obrázek05

Nechcete, aby se uživatelům zobrazoval tento druh chybové stránky, takže musíte přidat kód pro zpracování chyb. Otevřete Departments.aspx a zadejte obslužnou rutinu OnUpdated pro událost DepartmentsObjectDataSource. Levá ObjectDataSource značka se teď podobá následujícímu příkladu.

<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" >

V souboru Departments.aspx.cs přidejte následující using příkaz:

using ContosoUniversity.BLL;

Přidejte následující obslužnou rutinu Updated události:

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;
        }
    }
}

Pokud ovládací prvek při pokusu ObjectDataSource o provedení aktualizace zachytí výjimku, předá výjimku v argumentu události (e) této obslužné rutině. Kód v obslužné rutině zkontroluje, jestli je výjimka duplicitní výjimkou správce. Pokud ano, kód vytvoří ovládací prvek validátoru, který obsahuje chybovou ValidationSummary zprávu pro ovládací prvek, který se má zobrazit.

Spusťte stránku a zkuste někoho znovu nastavit jako správce dvou oddělení. Tentokrát ovládací ValidationSummary prvek zobrazí chybovou zprávu.

Obrázek06

Proveďte podobné změny na stránce DepartmentsAdd.aspx . V souboru DepartmentsAdd.aspx zadejte obslužnou rutinu OnInserted pro událost DepartmentsObjectDataSource. Výsledná značka bude vypadat podobně jako v následujícím příkladu.

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

V části DepartmentsAdd.aspx.cs přidejte stejný using příkaz:

using ContosoUniversity.BLL;

Přidejte následující obslužnou rutinu události:

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;
        }
    }
}

Teď můžete otestovat stránku DepartmentsAdd.aspx.cs a ověřit, že také správně zpracovává pokusy o to, aby se jedna osoba byla správcem více než jednoho oddělení.

Tím se dokončí úvod k implementaci vzoru úložiště pro použití ObjectDataSource ovládacího prvku v Entity Frameworku. Další informace o vzoru úložiště a testovatelnosti najdete v dokumentu o možnostech testování a Entity Frameworku 4.0 v dokumentu white paper MSDN.

V následujícím kurzu se dozvíte, jak do aplikace přidat funkci řazení a filtrování.