Budowanie modelu z walidacją reguł biznesowych

autor: Microsoft

Pobierz plik PDF

Jest to krok 3 bezpłatnego samouczka aplikacji "NerdDinner" , który zawiera instrukcje dotyczące tworzenia małej, ale kompletnej aplikacji internetowej przy użyciu ASP.NET MVC 1.

Krok 3 pokazuje, jak utworzyć model, którego możemy użyć do wykonywania zapytań i aktualizowania bazy danych dla naszej aplikacji NerdDinner.

Jeśli używasz ASP.NET MVC 3, zalecamy skorzystanie z samouczków Wprowadzenie With MVC 3 lub MVC Music Store .

NerdDinner Krok 3: Tworzenie modelu

W strukturze model-view-controller termin "model" odnosi się do obiektów reprezentujących dane aplikacji, a także odpowiednią logikę domeny, która integruje z nim reguły weryfikacji i biznesowe. Model jest na wiele sposobów "sercem" aplikacji opartej na MVC i jak zobaczymy później zasadniczo napędza jej zachowanie.

Platforma ASP.NET MVC obsługuje korzystanie z dowolnej technologii dostępu do danych, a deweloperzy mogą wybrać spośród różnych zaawansowanych opcji danych platformy .NET do implementowania modeli, takich jak: LINQ to Entities, LINQ to SQL, NHibernate, LLBLGen Pro, SubSonic, WilsonORM lub po prostu nieprzetworzone ADO.NET DataReaders lub DataSets.

W przypadku naszej aplikacji NerdDinner użyjemy LINQ to SQL do utworzenia prostego modelu, który odpowiada dość ściśle naszemu projektowi bazy danych i dodamy niestandardową logikę walidacji i reguły biznesowe. Następnie wdrożymy klasę repozytorium, która pomaga wyodrębnić implementację trwałości danych z pozostałej części aplikacji i umożliwia nam łatwe testowanie jednostkowe.

LINQ to SQL

LINQ to SQL jest maperem relacyjnym obiektu, który jest dostarczany jako część platformy .NET 3.5.

LINQ to SQL umożliwia łatwe mapowanie tabel baz danych na klasy platformy .NET, względem których możemy kodować. W naszej aplikacji NerdDinner użyjemy jej do mapowania tabel Dinners i RSVP w naszej bazie danych na klasy Dinner i RSVP. Kolumny tabel Obiady i RSVP będą odpowiadać właściwościom klas Kolacja i RSVP. Każdy obiekt Kolacja i RSVP będą reprezentować oddzielny wiersz w tabelach Dinners lub RSVP w bazie danych.

LINQ to SQL pozwala uniknąć konieczności ręcznego konstruowania instrukcji SQL w celu pobrania i zaktualizowania obiektów Dinner i RSVP przy użyciu danych bazy danych. Zamiast tego zdefiniujemy klasy Dinner i RSVP, sposób mapowania na/z bazy danych oraz relacji między nimi. LINQ to SQL następnie zajmie się generowaniem odpowiedniej logiki wykonywania SQL, która będzie używana w czasie wykonywania podczas interakcji z nimi i ich używania.

Możemy użyć obsługi języka LINQ w języku VB i C# do pisania zapytań ekspresyjnych, które pobierają obiekty Dinner i RSVP z bazy danych. Minimalizuje to ilość kodu danych, który musimy napisać i umożliwia tworzenie naprawdę czystych aplikacji.

Dodawanie klas LINQ to SQL do naszego projektu

Zaczniemy od kliknięcia prawym przyciskiem myszy folderu "Models" w naszym projekcie i wybrania polecenia menu Dodaj nowy> element :

Zrzut ekranu przedstawiający folder Models. Wyróżniono nowy element. Modele są wyróżnione i wybrane.

Spowoduje to wyświetlenie okna dialogowego "Dodaj nowy element". Filtrujemy według kategorii "Dane" i wybierzemy w nim szablon "LINQ to SQL Classes":

Zrzut ekranu przedstawiający okno dialogowe Dodawanie nowego elementu. Dane są wyróżnione. Klasy L I N Q do S Q L są zaznaczone i wyróżnione.

Nadamy nazwę elementu "NerdDinner" i klikniemy przycisk "Dodaj". Program Visual Studio doda plik NerdDinner.dbml w katalogu \Models, a następnie otworzy projektant relacyjny obiektu LINQ to SQL:

Zrzut ekranu przedstawiający okno dialogowe Nerd Dinner w programie Visual Studio. Wybrano plik Nerd Dinner dot d b m l.

Tworzenie klas modelu danych przy użyciu LINQ to SQL

LINQ to SQL umożliwia nam szybkie tworzenie klas modelu danych na podstawie istniejącego schematu bazy danych. W tym celu otworzymy bazę danych NerdDinner w Eksploratorze serwera i wybierzemy w nim tabele, które chcemy modelować:

Zrzut ekranu przedstawiający Eksploratora serwera. Tabele są rozwinięte. Kolacje i R S V P są wyróżnione.

Następnie możemy przeciągnąć tabele na powierzchnię projektanta LINQ to SQL. Gdy to zrobimy, LINQ to SQL automatycznie utworzy klasy Dinner i RSVP przy użyciu schematu tabel (z właściwościami klasy mapowania na kolumny tabeli bazy danych):

Zrzut ekranu przedstawiający okno dialogowe Kolacja z Nerdem. Zostaną wyświetlone klasy Obiad i R S V P.

Domyślnie projektant LINQ to SQL automatycznie "pluralizuje" nazwy tabel i kolumn podczas tworzenia klas na podstawie schematu bazy danych. Na przykład: tabela "Kolacje" w powyższym przykładzie spowodowała klasę "Kolacja". To nazewnictwo klas pomaga zapewnić spójność naszych modeli z konwencjami nazewnictwa platformy .NET i zwykle uważam, że projektant naprawi to wygodnie (zwłaszcza podczas dodawania wielu tabel). Jeśli nie lubisz nazwy klasy lub właściwości wygenerowanej przez projektanta, zawsze możesz ją zastąpić i zmienić na dowolną nazwę. Można to zrobić, edytując nazwę jednostki/właściwości w wierszu w projektancie lub modyfikując ją za pomocą siatki właściwości.

Domyślnie projektant LINQ to SQL sprawdza również relacje klucza podstawowego/klucza obcego tabel i na podstawie nich automatycznie tworzy domyślne "skojarzenia relacji" między tworzonymi przez nią klasami modeli. Na przykład po przeciągnięciu tabel Dinners i RSVP do projektanta LINQ to SQL skojarzenie relacji jeden do wielu między nimi zostało wywnioskowane na podstawie faktu, że tabela RSVP miała klucz obcy do tabeli Dinners (jest to wskazane przez strzałkę w projektancie):

Zrzut ekranu przedstawiający tabele Obiad i R S V P. Strzałka jest wyróżniona i wskazywana z drzewa właściwości Kolacja i drzewa właściwości języka R S V P.

Powyższe skojarzenie spowoduje, że LINQ to SQL dodać silnie typizowanej właściwości "Kolacja" do klasy RSVP, której deweloperzy mogą używać do uzyskiwania dostępu do kolacji skojarzonej z danym RSVP. Spowoduje to również, że klasa Dinner ma właściwość kolekcji "RSVPs", która umożliwia deweloperom pobieranie i aktualizowanie obiektów RSVP skojarzonych z konkretną kolacją.

Poniżej przedstawiono przykład funkcji intellisense w programie Visual Studio podczas tworzenia nowego obiektu RSVP i dodawania go do kolekcji RSVPs kolacji. Zwróć uwagę, jak LINQ to SQL automatycznie dodał kolekcję "RSVPs" w obiekcie Kolacja:

Zrzut ekranu przedstawiający funkcję intellisense w programie Visual Studio. R S V Ps jest wyróżniony.

Dodając obiekt RSVP do kolekcji RSVPs kolacji, informujemy LINQ to SQL, aby skojarzyć relację klucza obcego między kolacją a wierszem RSVP w naszej bazie danych:

Zrzut ekranu przedstawiający obiekt R S V P i kolekcję R S V P w kolacji.

Jeśli nie podoba Ci się sposób modelowania lub nazwania skojarzenia tabeli przez projektanta, możesz go zastąpić. Wystarczy kliknąć strzałkę skojarzenia w projektancie i uzyskać dostęp do jego właściwości za pośrednictwem siatki właściwości, aby zmienić nazwę, usunąć lub zmodyfikować ją. Jednak w przypadku naszej aplikacji NerdDinner domyślne reguły skojarzeń działają dobrze dla klas modelu danych, które tworzymy i możemy po prostu użyć domyślnego zachowania.

Klasa NerdDinnerDataContext

Program Visual Studio automatycznie utworzy klasy platformy .NET reprezentujące modele i relacje bazy danych zdefiniowane przy użyciu projektanta LINQ to SQL. Klasa LINQ to SQL DataContext jest również generowana dla każdego pliku projektanta LINQ to SQL dodanego do rozwiązania. Ponieważ nazwaliśmy nasz element klasy LINQ to SQL "NerdDinner", utworzona klasa DataContext będzie nazywana "NerdDinnerDataContext". Ta klasa NerdDinnerDataContext jest podstawowym sposobem interakcji z bazą danych.

Nasza klasa NerdDinnerDataContext uwidacznia dwie właściwości — "Kolacje" i "RSVPs" — reprezentujące dwie tabele, które modelowaliśmy w bazie danych. Za pomocą języka C# można pisać zapytania LINQ względem tych właściwości w celu wykonywania zapytań i pobierania obiektów Dinner i RSVP z bazy danych.

Poniższy kod pokazuje, jak utworzyć wystąpienie obiektu NerdDinnerDataContext i wykonać zapytanie LINQ względem niego w celu uzyskania sekwencji kolacji, które występują w przyszłości. Program Visual Studio zapewnia pełną funkcję IntelliSense podczas pisania zapytania LINQ, a obiekty zwracane z niego są silnie typizowane, a także obsługują funkcję IntelliSense:

Zrzut ekranu przedstawiający program Visual Studio. Opis jest wyróżniony.

Oprócz umożliwienia nam wykonywania zapytań dotyczących obiektów Dinner i RSVP, obiekt NerdDinnerDataContext również automatycznie śledzi wszelkie zmiany wprowadzane do obiektów Dinner i RSVP pobieranych przez nas. Za pomocą tej funkcji można łatwo zapisywać zmiany z powrotem w bazie danych — bez konieczności pisania jawnego kodu aktualizacji SQL.

Na przykład poniższy kod pokazuje, jak za pomocą zapytania LINQ pobrać pojedynczy obiekt Obiad z bazy danych, zaktualizować dwie właściwości kolacji, a następnie zapisać zmiany z powrotem w bazie danych:

NerdDinnerDataContext db = new NerdDinnerDataContext();

// Retrieve Dinner object that reprents row with DinnerID of 1
Dinner dinner = db.Dinners.Single(d => d.DinnerID == 1);

// Update two properties on Dinner 
dinner.Title = "Changed Title";
dinner.Description = "This dinner will be fun";

// Persist changes to database
db.SubmitChanges();

Obiekt NerdDinnerDataContext w powyższym kodzie automatycznie śledził zmiany właściwości wprowadzone w obiekcie Dinner pobranym z niego. Po wywołaniu metody "SubmitChanges()" do bazy danych zostanie wykonana odpowiednia instrukcja SQL "UPDATE", aby zachować zaktualizowane wartości.

Tworzenie klasy DinnerRepository

W przypadku małych aplikacji czasami kontrolery działają bezpośrednio względem klasy LINQ to SQL DataContext i osadzania zapytań LINQ w kontrolerach. Jednak w miarę jak aplikacje stają się większe, takie podejście staje się uciążliwe do utrzymania i testowania. Może to również prowadzić do duplikowania tych samych zapytań LINQ w wielu miejscach.

Jedną z metod ułatwiających konserwację i testowanie aplikacji jest użycie wzorca "repozytorium". Klasa repozytorium pomaga hermetyzować logikę wykonywania zapytań o dane i trwałość oraz abstrakcję szczegółów implementacji trwałości danych z aplikacji. Oprócz czyszczenia kodu aplikacji użycie wzorca repozytorium może ułatwić zmianę implementacji magazynu danych w przyszłości i ułatwić testowanie jednostkowe aplikacji bez konieczności posiadania prawdziwej bazy danych.

W przypadku naszej aplikacji NerdDinner zdefiniujemy klasę DinnerRepository z poniższym podpisem:

public class DinnerRepository {

    // Query Methods
    public IQueryable<Dinner> FindAllDinners();
    public IQueryable<Dinner> FindUpcomingDinners();
    public Dinner             GetDinner(int id);

    // Insert/Delete
    public void Add(Dinner dinner);
    public void Delete(Dinner dinner);

    // Persistence
    public void Save();
}

Uwaga: W dalszej części tego rozdziału wyodrębnimy interfejs IDinnerRepository z tej klasy i włączymy iniekcję zależności na kontrolerach. Na początek zaczniemy jednak od prostego i po prostu będziemy pracować bezpośrednio z klasą DinnerRepository.

Aby zaimplementować tę klasę, kliknij prawym przyciskiem myszy folder "Modele" i wybierz polecenie menu Dodaj nowy> element . W oknie dialogowym "Dodaj nowy element" wybierzemy szablon "Klasa" i nadamy plikowi nazwę "DinnerRepository.cs":

Zrzut ekranu przedstawiający folder Models. Wyróżniono pozycję Dodaj nowy element.

Następnie możemy zaimplementować klasę DinnerRepository przy użyciu poniższego kodu:

public class DinnerRepository {
 
    private NerdDinnerDataContext db = new NerdDinnerDataContext();

    //
    // Query Methods

    public IQueryable<Dinner> FindAllDinners() {
        return db.Dinners;
    }

    public IQueryable<Dinner> FindUpcomingDinners() {
        return from dinner in db.Dinners
               where dinner.EventDate > DateTime.Now
               orderby dinner.EventDate
               select dinner;
    }

    public Dinner GetDinner(int id) {
        return db.Dinners.SingleOrDefault(d => d.DinnerID == id);
    }

    //
    // Insert/Delete Methods

    public void Add(Dinner dinner) {
        db.Dinners.InsertOnSubmit(dinner);
    }

    public void Delete(Dinner dinner) {
        db.RSVPs.DeleteAllOnSubmit(dinner.RSVPs);
        db.Dinners.DeleteOnSubmit(dinner);
    }

    //
    // Persistence 

    public void Save() {
        db.SubmitChanges();
    }
}

Pobieranie, aktualizowanie, wstawianie i usuwanie przy użyciu klasy DinnerRepository

Teraz, gdy utworzyliśmy klasę DinnerRepository, przyjrzyjmy się kilku przykładom kodu, które pokazują typowe zadania, które możemy z nim wykonać:

Przykłady wykonywania zapytań

Poniższy kod pobiera pojedynczą kolację przy użyciu wartości DinnerID:

DinnerRepository dinnerRepository = new DinnerRepository();

// Retrieve specific dinner by its DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);

Poniższy kod pobiera wszystkie nadchodzące kolacje i pętle na nich:

DinnerRepository dinnerRepository = new DinnerRepository();

// Retrieve all upcoming Dinners
var upcomingDinners = dinnerRepository.FindUpcomingDinners();

// Loop over each upcoming Dinner and print out its Title
foreach (Dinner dinner in upcomingDinners) {
   Response.Write("Title" + dinner.Title);
}

Wstaw i zaktualizuj przykłady

Poniższy kod pokazuje dodawanie dwóch nowych kolacji. Dodatki/modyfikacje repozytorium nie są zatwierdzane w bazie danych, dopóki nie zostanie wywołana metoda "Save()". LINQ to SQL automatycznie opakowuje wszystkie zmiany w transakcji bazy danych — więc wszystkie zmiany zostaną wprowadzone lub żadne z nich nie zostaną zapisane w naszym repozytorium:

DinnerRepository dinnerRepository = new DinnerRepository();

// Create First Dinner
Dinner newDinner1 = new Dinner();
newDinner1.Title = "Dinner with Scott";
newDinner1.HostedBy = "ScotGu";
newDinner1.ContactPhone = "425-703-8072";

// Create Second Dinner
Dinner newDinner2 = new Dinner();
newDinner2.Title = "Dinner with Bill";
newDinner2.HostedBy = "BillG";
newDinner2.ContactPhone = "425-555-5151";

// Add Dinners to Repository
dinnerRepository.Add(newDinner1);
dinnerRepository.Add(newDinner2);

// Persist Changes
dinnerRepository.Save();

Poniższy kod pobiera istniejący obiekt Kolacja i modyfikuje na nim dwie właściwości. Zmiany są zatwierdzane z powrotem do bazy danych, gdy metoda "Save()" jest wywoływana w naszym repozytorium:

DinnerRepository dinnerRepository = new DinnerRepository();

// Retrieve specific dinner by its DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);

// Update Dinner properties
dinner.Title = "Update Title";
dinner.HostedBy = "New Owner";

// Persist changes
dinnerRepository.Save();

Poniższy kod pobiera kolację, a następnie dodaje do niego rsVP. Robi to przy użyciu kolekcji RSVPs w obiekcie Dinner, który LINQ to SQL utworzony dla nas (ponieważ istnieje relacja klucza podstawowego/klucza obcego między nimi w bazie danych). Ta zmiana jest utrwalana z powrotem do bazy danych jako nowy wiersz tabeli RSVP, gdy metoda "Save()" jest wywoływana w repozytorium:

DinnerRepository dinnerRepository = new DinnerRepository();

// Retrieve specific dinner by its DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);

// Create a new RSVP object
RSVP myRSVP = new RSVP();
myRSVP.AttendeeName = "ScottGu";

// Add RSVP to Dinner's RSVP Collection
dinner.RSVPs.Add(myRSVP);

// Persist changes
dinnerRepository.Save();

Usuń przykład

Poniższy kod pobiera istniejący obiekt Kolacja, a następnie oznacza jego usunięcie. Gdy metoda "Save()" jest wywoływana w repozytorium, zatwierdzi usunięcie z powrotem do bazy danych:

DinnerRepository dinnerRepository = new DinnerRepository();

// Retrieve specific dinner by its DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);

// Mark dinner to be deleted
dinnerRepository.Delete(dinner);

// Persist changes
dinnerRepository.Save();

Integrowanie logiki weryfikacji i reguł biznesowych z klasami modeli

Integrowanie logiki weryfikacji i reguł biznesowych jest kluczową częścią każdej aplikacji, która współpracuje z danymi.

Walidacja schematu

Gdy klasy modeli są definiowane przy użyciu projektanta LINQ to SQL, typy danych właściwości w klasach modelu danych odpowiadają typom danych tabeli bazy danych. Na przykład: jeśli kolumna "EventDate" w tabeli Dinners to "datetime", klasa modelu danych utworzona przez LINQ to SQL będzie mieć typ "DateTime" (który jest wbudowanym typem danych platformy .NET). Oznacza to, że podczas próby przypisania liczby całkowitej lub wartości logicznej z kodu wystąpi błąd automatycznie, jeśli spróbujesz niejawnie przekonwertować nieprawidłowy typ ciągu w czasie wykonywania.

LINQ to SQL będzie również automatycznie obsługiwać ucieczkę wartości SQL podczas używania ciągów — co pomaga chronić przed atakami polegającymi na wstrzyknięciu kodu SQL podczas korzystania z nich.

Walidacja i logika reguł biznesowych

Walidacja schematu jest przydatna jako pierwszy krok, ale rzadko jest wystarczająca. Większość rzeczywistych scenariuszy wymaga możliwości określenia bardziej rozbudowanej logiki walidacji, która może obejmować wiele właściwości, wykonać kod i często mieć świadomość stanu modelu (na przykład: czy jest tworzona /aktualizowana/usuwana lub w stanie specyficznym dla domeny, takim jak "zarchiwizowane"). Istnieje wiele różnych wzorców i struktur, których można użyć do definiowania i stosowania reguł weryfikacji do klas modeli, a istnieje kilka platform opartych na platformie .NET, które mogą być używane do tego celu. W aplikacjach MVC można ich używać prawie w ASP.NET.

Na potrzeby naszej aplikacji NerdDinner użyjemy stosunkowo prostego i prostego wzorca, w którym uwidaczniamy właściwość IsValid i metodę GetRuleViolations() na naszym obiekcie modelu Obiad. Właściwość IsValid zwróci wartość true lub false w zależności od tego, czy reguły sprawdzania poprawności i reguł biznesowych są prawidłowe. Metoda GetRuleViolations() zwróci listę błędów reguły.

Zaimplementujemy model IsValid i GetRuleViolations() dla naszego modelu kolacji, dodając do naszego projektu "klasę częściową". Klasy częściowe mogą służyć do dodawania metod/właściwości/zdarzeń do klas obsługiwanych przez projektanta programu VS (na przykład klasy Dinner wygenerowanej przez projektanta LINQ to SQL) i pomóc uniknąć bałaganu za pomocą naszego kodu. Możemy dodać nową klasę częściową do naszego projektu, klikając prawym przyciskiem myszy folder \Models, a następnie wybierając polecenie menu "Dodaj nowy element". Następnie możemy wybrać szablon "Klasa" w oknie dialogowym "Dodaj nowy element" i nadać mu nazwę Dinner.cs.

Zrzut ekranu przedstawiający folder Models. Wybrano pozycję Dodaj nowy element. Kolacja dot c s jest zapisywana w oknie dialogowym Dodawanie nowego elementu.

Kliknięcie przycisku "Dodaj" spowoduje dodanie pliku Dinner.cs do naszego projektu i otwarcie go w środowisku IDE. Następnie możemy zaimplementować podstawową strukturę reguł/wymuszania poprawności przy użyciu poniższego kodu:

public partial class Dinner {

    public bool IsValid {
        get { return (GetRuleViolations().Count() == 0); }
    }

    public IEnumerable<RuleViolation> GetRuleViolations() {
        yield break;
    }

    partial void OnValidate(ChangeAction action) {
        if (!IsValid)
            throw new ApplicationException("Rule violations prevent saving");
    }
}

public class RuleViolation {

    public string ErrorMessage { get; private set; }
    public string PropertyName { get; private set; }

    public RuleViolation(string errorMessage, string propertyName) {
        ErrorMessage = errorMessage;
        PropertyName = propertyName;
    }
}

Kilka uwag dotyczących powyższego kodu:

  • Klasa Dinner jest poprzedzona "częściowym" słowem kluczowym — co oznacza, że kod zawarty w nim zostanie połączony z klasą wygenerowaną/utrzymywaną przez projektanta LINQ to SQL i skompilowaną w jedną klasę.
  • Klasa RuleViolation jest klasą pomocnika dodamy do projektu, która umożliwia nam podanie dodatkowych szczegółów dotyczących naruszenia reguły.
  • Metoda Dinner.GetRuleViolations() powoduje, że nasze reguły weryfikacji i biznesowe zostaną ocenione (wkrótce je zaimplementujemy). Następnie zwraca sekwencję obiektów RuleViolation, które zawierają więcej szczegółów dotyczących błędów reguły.
  • Właściwość Dinner.IsValid zapewnia wygodną właściwość pomocnika, która wskazuje, czy obiekt Kolacja ma jakiekolwiek aktywne RuleViolations. Może być aktywnie sprawdzany przez dewelopera przy użyciu obiektu Kolacja w dowolnym momencie (i nie zgłasza wyjątku).
  • Metoda częściowa Dinner.OnValidate() to hak, który LINQ to SQL zapewnia, że pozwala nam otrzymywać powiadomienia w dowolnym momencie, w jaki obiekt kolacji ma być utrwalone w bazie danych. Nasza implementacja OnValidate() powyżej gwarantuje, że kolacja nie ma RegułaViolations przed zapisaniem. Jeśli jest on w nieprawidłowym stanie, zgłasza wyjątek, co spowoduje, że LINQ to SQL przerwać transakcję.

Takie podejście zapewnia prostą strukturę, z którą możemy zintegrować reguły weryfikacji i biznesowe. Na razie dodajmy poniższe reguły do metody GetRuleViolations():

public IEnumerable<RuleViolation> GetRuleViolations() {

    if (String.IsNullOrEmpty(Title))
        yield return new RuleViolation("Title required","Title");

    if (String.IsNullOrEmpty(Description))
        yield return new RuleViolation("Description required","Description");

    if (String.IsNullOrEmpty(HostedBy))
        yield return new RuleViolation("HostedBy required", "HostedBy");

    if (String.IsNullOrEmpty(Address))
        yield return new RuleViolation("Address required", "Address");

    if (String.IsNullOrEmpty(Country))
        yield return new RuleViolation("Country required", "Country");

    if (String.IsNullOrEmpty(ContactPhone))
        yield return new RuleViolation("Phone# required", "ContactPhone");

    if (!PhoneValidator.IsValidNumber(ContactPhone, Country))
        yield return new RuleViolation("Phone# does not match country", "ContactPhone");

    yield break;
}

Używamy funkcji "return return" języka C#, aby zwrócić sekwencję dowolnego elementu RuleViolations. Pierwsza sześć reguł sprawdza powyżej po prostu wymusza, że właściwości ciągu w naszej kolacji nie mogą być puste ani zerowe. Ostatnia reguła jest nieco bardziej interesująca i wywołuje metodę pomocnika PhoneValidator.IsValidNumber(), którą możemy dodać do naszego projektu, aby sprawdzić, czy format numeru ContactPhone jest zgodny z krajem/regionem kolacji.

Możemy użyć polecenia . Obsługa wyrażeń regularnych platformy NET w celu zaimplementowania obsługi walidacji telefonu. Poniżej znajduje się prosta implementacja PhoneValidator, którą możemy dodać do naszego projektu, która umożliwia dodawanie testów wzorców regex specyficznych dla kraju/regionu:

public class PhoneValidator {

    static IDictionary<string, Regex> countryRegex = new Dictionary<string, Regex>() {
           { "USA", new Regex("^[2-9]\\d{2}-\\d{3}-\\d{4}$")},
           { "UK", new Regex("(^1300\\d{6}$)|(^1800|1900|1902\\d{6}$)|(^0[2|3|7|8]{1}[0-9]{8}$)|(^13\\d{4}$)|(^04\\d{2,3}\\d{6}$)")},
           { "Netherlands", new Regex("(^\\+[0-9]{2}|^\\+[0-9]{2}\\(0\\)|^\\(\\+[0-9]{2}\\)\\(0\\)|^00[0-9]{2}|^0)([0-9]{9}$|[0-9\\-\\s]{10}$)")},
    };

    public static bool IsValidNumber(string phoneNumber, string country) {

        if (country != null && countryRegex.ContainsKey(country))
            return countryRegex[country].IsMatch(phoneNumber);
        else
            return false;
    }

    public static IEnumerable<string> Countries {
        get {
            return countryRegex.Keys;
        }
    }
}

Obsługa naruszeń walidacji i logiki biznesowej

Teraz, gdy dodaliśmy powyższy kod weryfikacji i reguły biznesowej, za każdym razem, gdy spróbujemy utworzyć lub zaktualizować kolację, nasze reguły logiki weryfikacji zostaną ocenione i wymuszone.

Deweloperzy mogą napisać kod podobny do poniższego, aby aktywnie określić, czy obiekt Kolacja jest prawidłowy, i pobrać listę wszystkich naruszeń w nim bez zgłaszania żadnych wyjątków:

Dinner dinner = dinnerRepository.GetDinner(5);

dinner.Country = "USA";
dinner.ContactPhone = "425-555-BOGUS";

if (!dinner.IsValid) {

    var errors = dinner.GetRuleViolations();
    
    // do something to fix the errors
}

Jeśli spróbujemy zapisać kolację w nieprawidłowym stanie, podczas wywoływania metody Save() w repozytorium DinnerRepository zostanie zgłoszony wyjątek. Dzieje się tak, ponieważ LINQ to SQL automatycznie wywołuje metodę częściową Dinner.OnValidate() przed zapisaniem zmian w kolacji i dodaliśmy kod do dinner.OnValidate(), aby zgłosić wyjątek, jeśli w kolacji istnieją jakiekolwiek naruszenia reguł. Możemy złapać ten wyjątek i reaktywnie pobrać listę naruszeń, aby naprawić:

Dinner dinner = dinnerRepository.GetDinner(5);

try {

    dinner.Country = "USA";
    dinner.ContactPhone = "425-555-BOGUS";

    dinnerRepository.Save();
}
catch {

    var errors = dinner.GetRuleViolations();

    // do something to fix errors
}

Ponieważ nasze reguły weryfikacji i działania biznesowe są implementowane w naszej warstwie modelu, a nie w warstwie interfejsu użytkownika, zostaną one zastosowane i użyte we wszystkich scenariuszach w naszej aplikacji. Możemy później zmienić lub dodać reguły biznesowe i mieć cały kod, który współpracuje z naszymi obiektami kolacji.

Mając elastyczność zmiany reguł biznesowych w jednym miejscu, bez konieczności wprowadzania tych zmian w całej logice aplikacji i interfejsu użytkownika, jest oznaką dobrze napisanej aplikacji i korzyścią, którą platforma MVC pomaga zachęcić.

Następny krok

Mamy teraz model, którego możemy użyć do wykonywania zapytań i aktualizowania bazy danych.

Teraz dodajmy do projektu kilka kontrolerów i widoków, których możemy użyć do utworzenia środowiska interfejsu użytkownika HTML.