Korzystanie z programu Entity Framework 4.0 i kontrolki ObjectDataSource, część 2: Dodawanie warstwy logiki biznesowej i testów jednostkowych
Autor : Tom Dykstra
Ta seria samouczków opiera się na aplikacji internetowej Contoso University utworzonej przez Wprowadzenie z serii samouczków Entity Framework 4.0. Jeśli nie ukończysz wcześniejszych samouczków, jako punkt wyjścia dla tego samouczka możesz pobrać utworzoną aplikację . Możesz również pobrać aplikację utworzoną przez kompletną serię samouczków. Jeśli masz pytania dotyczące samouczków, możesz opublikować je na forum ASP.NET Entity Framework.
W poprzednim samouczku utworzono aplikację internetową n-warstwową przy użyciu struktury Entity Framework i kontrolki ObjectDataSource
. W tym samouczku pokazano, jak dodać logikę biznesową przy zachowaniu oddzielnej warstwy logiki biznesowej (BLL) i warstwy dostępu do danych (DAL), a także pokazano, jak utworzyć zautomatyzowane testy jednostkowe dla usługi BLL.
W tym samouczku wykonasz następujące zadania:
- Utwórz interfejs repozytorium, który deklaruje potrzebne metody dostępu do danych.
- Zaimplementuj interfejs repozytorium w klasie repozytorium.
- Utwórz klasę logiki biznesowej, która wywołuje klasę repozytorium w celu wykonywania funkcji dostępu do danych.
- Połącz kontrolkę
ObjectDataSource
z klasą logiki biznesowej zamiast z klasą repozytorium. - Utwórz projekt testowy jednostkowy i klasę repozytorium, która używa kolekcji w pamięci dla swojego magazynu danych.
- Utwórz test jednostkowy dla logiki biznesowej, który chcesz dodać do klasy logiki biznesowej, a następnie uruchom test i sprawdź, czy zakończy się niepowodzeniem.
- Zaimplementuj logikę biznesową w klasie logiki biznesowej, a następnie uruchom ponownie test jednostkowy i sprawdź, czy został przekazany.
Będziesz pracować z stronami Departments.aspx i DepartmentsAdd.aspx utworzonymi w poprzednim samouczku.
Tworzenie interfejsu repozytorium
Zaczniesz od utworzenia interfejsu repozytorium.
W folderze DAL utwórz nowy plik klasy, nadaj mu nazwę ISchoolRepository.cs i zastąp istniejący kod następującym kodem:
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();
}
}
Interfejs definiuje jedną metodę dla każdej metody CRUD (create, read, update, delete), które zostały utworzone w klasie repozytorium.
SchoolRepository
W klasie w pliku SchoolRepository.cs wskaż, że ta klasa implementuje ISchoolRepository
interfejs:
public class SchoolRepository : IDisposable, ISchoolRepository
Tworzenie klasy Business-Logic
Następnie utworzysz klasę logiki biznesowej. Można to zrobić, aby dodać logikę biznesową, która będzie wykonywana przez kontrolkę ObjectDataSource
, chociaż jeszcze tego nie zrobisz. Na razie nowa klasa logiki biznesowej będzie wykonywać tylko te same operacje CRUD, które wykonuje repozytorium.
Utwórz nowy folder i nadaj mu nazwę BLL. (W rzeczywistej aplikacji warstwa logiki biznesowej jest zwykle implementowana jako biblioteka klas — oddzielny projekt — ale aby zachować ten samouczek proste, klasy BLL będą przechowywane w folderze projektu).
W folderze BLL utwórz nowy plik klasy, nadaj mu nazwę SchoolBL.cs i zastąp istniejący kod następującym kodem:
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);
}
}
}
Ten kod tworzy te same metody CRUD, które widzieliśmy wcześniej w klasie repozytorium, ale zamiast uzyskiwać bezpośredni dostęp do metod programu Entity Framework, wywołuje metody klas repozytorium.
Zmienna klasy, która zawiera odwołanie do klasy repozytorium, jest definiowana jako typ interfejsu, a kod, który tworzy wystąpienie klasy repozytorium, znajduje się w dwóch konstruktorach. Konstruktor bez parametrów będzie używany przez kontrolkę ObjectDataSource
. Tworzy wystąpienie utworzonej SchoolRepository
wcześniej klasy. Drugi konstruktor umożliwia dowolny kod, który tworzy wystąpienie klasy logiki biznesowej, aby przekazać dowolny obiekt implementujący interfejs repozytorium.
Metody CRUD wywołujące klasę repozytorium i dwa konstruktory umożliwiają używanie klasy logiki biznesowej z wybranym magazynem danych zaplecza. Klasa logiki biznesowej nie musi mieć świadomości, w jaki sposób klasa, którą wywołuje, utrwala dane. (Często nazywa się to ignorancją trwałości). Ułatwia to testowanie jednostkowe, ponieważ można połączyć klasę logiki biznesowej z implementacją repozytorium, która używa czegoś tak prostego, jak kolekcje w pamięci List
do przechowywania danych.
Uwaga
Technicznie obiekty jednostki nadal nie są ignorowane przez trwałość, ponieważ są tworzone wystąpienia klas dziedziczące z klasy entity Framework EntityObject
. W przypadku całkowitej niewiedzy trwałości można użyć zwykłych starych obiektów CLR lub obiektów POC, zamiast obiektów dziedziczynych z EntityObject
klasy. Korzystanie z obiektów POC wykracza poza zakres tego samouczka. Aby uzyskać więcej informacji, zobacz Testability and Entity Framework 4.0 (Testowanie i platforma Entity Framework 4.0 ) w witrynie sieci Web MSDN).
Teraz możesz połączyć kontrolki ObjectDataSource
z klasą logiki biznesowej zamiast z repozytorium i sprawdzić, czy wszystko działa tak jak wcześniej.
W obszarze Departments.aspx i DepartmentsAdd.aspx zmień każde wystąpienie na TypeName="ContosoUniversity.DAL.SchoolRepository"
TypeName="ContosoUniversity.BLL.SchoolBL
". (W sumie istnieją cztery wystąpienia).
Uruchom strony Departments.aspx i DepartmentsAdd.aspx , aby sprawdzić, czy nadal działają tak, jak wcześniej.
Tworzenie implementacji projektu i repozytorium Unit-Test
Dodaj nowy projekt do rozwiązania przy użyciu szablonu Projekt testowy i nadaj mu ContosoUniversity.Tests
nazwę .
W projekcie testowym dodaj odwołanie do System.Data.Entity
projektu i dodaj odwołanie do ContosoUniversity
projektu.
Teraz możesz utworzyć klasę repozytorium, której będziesz używać z testami jednostkowymi. Magazyn danych dla tego repozytorium będzie znajdować się w klasie .
W projekcie testowym utwórz nowy plik klasy, nadaj mu nazwę MockSchoolRepository.cs i zastąp istniejący kod następującym kodem:
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()
{
}
}
}
Ta klasa repozytorium ma te same metody CRUD co ta, która uzyskuje bezpośredni dostęp do programu Entity Framework, ale współpracują z kolekcjami List
w pamięci zamiast z bazą danych. Ułatwia to klasę testową konfigurowania i weryfikowania testów jednostkowych dla klasy logiki biznesowej.
Tworzenie testów jednostkowych
Szablon projektu Test utworzył klasę testów jednostkowych, a następnym zadaniem jest zmodyfikowanie tej klasy przez dodanie do niej metod testu jednostkowego dla logiki biznesowej, którą chcesz dodać do klasy logiki biznesowej.
Na Uniwersytecie Contoso każdy indywidualny instruktor może być tylko administratorem jednego działu i musisz dodać logikę biznesową, aby wymusić tę regułę. Zaczniesz od dodania testów i uruchomienia testów, aby zobaczyć ich niepowodzenie. Następnie dodasz kod i ponownie uruchomisz testy, spodziewając się ich przekazania.
Otwórz plik UnitTest1.cs i dodaj using
instrukcje dla warstw logiki biznesowej i dostępu do danych utworzonych w projekcie ContosoUniversity:
using ContosoUniversity.BLL;
using ContosoUniversity.DAL;
Zastąp metodę TestMethod1
następującymi 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
tworzy wystąpienie klasy repozytorium utworzonej dla projektu testu jednostkowego, które następnie przekazuje do nowego wystąpienia klasy logiki biznesowej. Następnie metoda używa klasy logiki biznesowej do wstawiania trzech działów, których można użyć w metodach testowych.
Metody testowe sprawdzają, czy klasa logiki biznesowej zgłasza wyjątek, jeśli ktoś próbuje wstawić nowy dział z tym samym administratorem co istniejący dział lub jeśli ktoś próbuje zaktualizować administratora działu, ustawiając go na identyfikator osoby, która jest już administratorem innego działu.
Nie utworzono jeszcze klasy wyjątków, więc ten kod nie zostanie skompilowany. Aby go skompilować, kliknij prawym przyciskiem myszy DuplicateAdministratorException
i wybierz polecenie Generuj, a następnie pozycję Klasa.
Spowoduje to utworzenie klasy w projekcie testowym, który można usunąć po utworzeniu klasy wyjątków w projekcie głównym. i zaimplementowano logikę biznesową.
Uruchom projekt testowy. Zgodnie z oczekiwaniami testy kończą się niepowodzeniem.
Dodawanie logiki biznesowej do testowania
Następnie wdrożysz logikę biznesową, która uniemożliwia ustawienie jako administrator działu, który jest już administratorem innego działu. Zgłosisz wyjątek z warstwy logiki biznesowej, a następnie przechwycisz go w warstwie prezentacji, jeśli użytkownik edytuje dział i kliknie pozycję Aktualizuj po wybraniu osoby, która jest już administratorem. (Możesz również usunąć instruktorów z listy rozwijanej, którzy są już administratorami przed renderowaniem strony, ale celem jest praca z warstwą logiki biznesowej).
Zacznij od utworzenia klasy wyjątków, którą zgłosisz, gdy użytkownik próbuje utworzyć instruktora administratora więcej niż jednego działu. W projekcie głównym utwórz nowy plik klasy w folderze BLL , nadaj mu nazwę DuplicateAdministratorException.cs i zastąp istniejący kod następującym kodem:
using System;
namespace ContosoUniversity.BLL
{
public class DuplicateAdministratorException : Exception
{
public DuplicateAdministratorException(string message)
: base(message)
{
}
}
}
Teraz usuń tymczasowy plik DuplicateAdministratorException.cs utworzony wcześniej w projekcie testowym, aby móc go skompilować.
W projekcie głównym otwórz plik SchoolBL.cs i dodaj następującą metodę zawierającą logikę walidacji. (Kod odwołuje się do metody, którą utworzysz później).
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));
}
}
}
Wywołasz tę metodę podczas wstawiania lub aktualizowania Department
jednostek, aby sprawdzić, czy inny dział ma już tego samego administratora.
Kod wywołuje metodę wyszukiwania bazy danych dla Department
jednostki, która ma tę samą Administrator
wartość właściwości co wstawiona lub zaktualizowana jednostka. Jeśli zostanie znaleziony, kod zgłasza wyjątek. Nie jest wymagane sprawdzenie poprawności, jeśli wstawiona lub zaktualizowana jednostka nie Administrator
ma wartości i nie jest zgłaszany wyjątek, jeśli metoda jest wywoływana podczas aktualizacji, a Department
znaleziona jednostka jest zgodna ze zaktualizowaną jednostką Department
.
Wywołaj nową metodę z Insert
metod i Update
:
public void InsertDepartment(Department department)
{
ValidateOneAdministratorAssignmentPerInstructor(department);
try
...
public void UpdateDepartment(Department department, Department origDepartment)
{
ValidateOneAdministratorAssignmentPerInstructor(department);
try
...
W pliku ISchoolRepository.cs dodaj następującą deklarację dla nowej metody dostępu do danych:
IEnumerable<Department> GetDepartmentsByAdministrator(Int32 administrator);
W pliku SchoolRepository.cs dodaj następującą using
instrukcję:
using System.Data.Objects;
W pliku SchoolRepository.cs dodaj następującą nową metodę dostępu do danych:
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();
}
Ten kod pobiera Department
jednostki, które mają określonego administratora. Należy znaleźć tylko jeden dział (jeśli istnieje). Jednak ponieważ żadne ograniczenie nie jest wbudowane w bazę danych, zwracany typ jest kolekcją na wypadek znalezienia wielu działów.
Domyślnie gdy kontekst obiektu pobiera jednostki z bazy danych, śledzi je w menedżerze stanu obiektu. Parametr MergeOption.NoTracking
określa, że to śledzenie nie zostanie wykonane dla tego zapytania. Jest to konieczne, ponieważ zapytanie może zwrócić dokładną jednostkę, którą próbujesz zaktualizować, a następnie nie będzie można dołączyć tej jednostki. Jeśli na przykład edytujesz dział historii na stronie Departments.aspx i pozostawisz administratora bez zmian, to zapytanie zwróci dział historii. Jeśli NoTracking
nie zostanie ustawiona, kontekst obiektu będzie miał już jednostkę Dział historii w menedżerze stanu obiektu. Następnie po dołączeniu jednostki Działu historii, która zostanie ponownie utworzona na podstawie stanu widoku, kontekst obiektu zgłosi wyjątek z informacją "An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key"
.
(Alternatywą dla określenia MergeOption.NoTracking
parametru jest utworzenie nowego kontekstu obiektu tylko dla tego zapytania. Ponieważ nowy kontekst obiektu będzie miał własnego menedżera stanu obiektu, podczas wywoływania Attach
metody nie występuje konflikt. Nowy kontekst obiektu współużytkuje metadane i połączenie z bazą danych z oryginalnym kontekstem obiektu, więc kary za wydajność tego alternatywnego podejścia byłyby minimalne. Przedstawione tutaj podejście wprowadza NoTracking
jednak opcję, która będzie przydatna w innych kontekstach. Ta NoTracking
opcja została omówiona w dalszej części tego samouczka w tej serii).
W projekcie testowym dodaj nową metodę dostępu do danych do pliku MockSchoolRepository.cs:
public IEnumerable<Department> GetDepartmentsByAdministrator(Int32 administrator)
{
return (from d in departments
where d.Administrator == administrator
select d);
}
Ten kod używa linQ do wykonania tego samego wyboru danych, do którego ContosoUniversity
repozytorium projektu używa LINQ to Entities.
Ponownie uruchom projekt testowy. Tym razem testy kończą się powodzeniem.
Obsługa wyjątków ObjectDataSource
W projekcie ContosoUniversity
uruchom stronę Departments.aspx i spróbuj zmienić administratora działu na kogoś, kto jest już administratorem innego działu. (Pamiętaj, że możesz edytować tylko działy dodane podczas tego samouczka, ponieważ baza danych jest wstępnie ładowana z nieprawidłowymi danymi). Zostanie wyświetlona następująca strona błędu serwera:
Nie chcesz, aby użytkownicy widzieli tego rodzaju stronę błędu, dlatego należy dodać kod obsługi błędów. Otwórz plik Departments.aspx i określ procedurę obsługi dla OnUpdated
zdarzenia .DepartmentsObjectDataSource
Tag ObjectDataSource
otwierający jest teraz podobny do poniższego przykładu.
<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" >
W pliku Departments.aspx.cs dodaj następującą using
instrukcję:
using ContosoUniversity.BLL;
Dodaj następującą procedurę obsługi dla zdarzenia Updated
:
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;
}
}
}
Jeśli kontrolka ObjectDataSource
przechwytuje wyjątek podczas próby przeprowadzenia aktualizacji, przekazuje wyjątek w argumencie zdarzenia (e
) do tej procedury obsługi. Kod w procedurze obsługi sprawdza, czy wyjątek jest duplikatem wyjątku administratora. Jeśli tak jest, kod tworzy kontrolkę modułu sprawdzania poprawności, która zawiera komunikat o błędzie dla kontrolki ValidationSummary
do wyświetlenia.
Uruchom stronę i spróbuj ponownie utworzyć administratora dwóch działów. Tym razem kontrolka ValidationSummary
wyświetla komunikat o błędzie.
Wprowadź podobne zmiany na stronie DepartmentsAdd.aspx . W obszarze DepartmentsAdd.aspx określ procedurę obsługi dla OnInserted
zdarzenia .DepartmentsObjectDataSource
Wynikowa adiustacja będzie wyglądać podobnie do poniższego przykładu.
<asp:ObjectDataSource ID="DepartmentsObjectDataSource" runat="server"
TypeName="ContosoUniversity.BLL.SchoolBL" DataObjectTypeName="ContosoUniversity.DAL.Department"
InsertMethod="InsertDepartment"
OnInserted="DepartmentsObjectDataSource_Inserted">
W pliku DepartmentsAdd.aspx.cs dodaj tę samą using
instrukcję:
using ContosoUniversity.BLL;
Dodaj następującą procedurę obsługi zdarzeń:
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;
}
}
}
Teraz możesz przetestować stronę DepartmentsAdd.aspx.cs , aby sprawdzić, czy poprawnie obsługuje próby wykonania jednej osoby przez administratora więcej niż jednego działu.
Spowoduje to ukończenie wprowadzenia do implementacji wzorca repozytorium na potrzeby używania kontrolki ObjectDataSource
z programem Entity Framework. Aby uzyskać więcej informacji na temat wzorca i możliwości testowania repozytorium, zobacz oficjalny dokument MSDN Testability and Entity Framework 4.0 (Testability i Entity Framework 4.0).
W poniższym samouczku dowiesz się, jak dodać funkcje sortowania i filtrowania do aplikacji.