Použití Entity Framework 4.0 a ObjectDataSource Ovládací prvek, Část 2: Přidání vrstvy obchodní logiky a testů jednotek
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ě.
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ě.
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.
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 .
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.
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.
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.
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 Department
Administrator
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.NoTracking
můž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.
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:
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.
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í.