Freigeben über


Erstellen eines Modells mit Geschäftsregelüberprüfungen

von Microsoft

PDF herunterladen

Dies ist Schritt 3 eines kostenlosen "NerdDinner"-Anwendungstutorial , in dem Sie schrittweise eine kleine, aber vollständige Webanwendung mit ASP.NET MVC 1 erstellen.

Schritt 3 zeigt, wie Sie ein Modell erstellen, das zum Abfragen und Aktualisieren der Datenbank für unsere NerdDinner-Anwendung verwendet werden kann.

Wenn Sie ASP.NET MVC 3 verwenden, empfehlen wir Ihnen, die Tutorials Erste Schritte Mit MVC 3 oder MVC Music Store zu befolgen.

NerdDinner Schritt 3: Erstellen des Modells

In einem Modellansichtscontrollerframework bezieht sich der Begriff "Modell" auf die Objekte, die die Daten der Anwendung darstellen, sowie auf die entsprechende Domänenlogik, die Validierungs- und Geschäftsregeln integriert. Das Modell ist in vielerlei Hinsicht das "Herzstück" einer MVC-basierten Anwendung und steuert, wie wir später sehen werden, das Verhalten des Modells grundlegend.

Das ASP.NET MVC-Framework unterstützt die Verwendung beliebiger Datenzugriffstechnologien, und Entwickler können aus einer Vielzahl von umfangreichen .NET-Datenoptionen wählen, um ihre Modelle zu implementieren, einschließlich: LINQ to Entities, LINQ to SQL, NHibernate, LLBLGen Pro, SubSonic, WilsonORM oder einfach nur rohe ADO.NET DataReaders oder DataSets.

Für unsere NerdDinner-Anwendung verwenden wir LINQ to SQL, um ein einfaches Modell zu erstellen, das unserem Datenbankentwurf ziemlich genau entspricht, und fügt einige benutzerdefinierte Validierungslogik und Geschäftsregeln hinzu. Anschließend implementieren wir eine Repositoryklasse, die dazu beiträgt, die Datenpersistenzimplementierung vom Rest der Anwendung abstrahieren zu lassen und es uns ermöglicht, sie einfach zu testen.

LINQ to SQL

LINQ to SQL ist ein ORM (object relational mapper), der als Teil von .NET 3.5 ausgeliefert wird.

LINQ to SQL bietet eine einfache Möglichkeit, Datenbanktabellen .NET-Klassen zuzuordnen, für die wir programmieren können. Für unsere NerdDinner-Anwendung verwenden wir sie, um die Dinners- und RSVP-Tabellen in unserer Datenbank den Dinner- und RSVP-Klassen zuzuordnen. Die Spalten der Tabellen Dinners und RSVP entsprechen den Eigenschaften der Dinner- und RSVP-Klassen. Jedes Dinner- und RSVP-Objekt stellt eine separate Zeile innerhalb der Dinners- oder RSVP-Tabellen in der Datenbank dar.

LINQ to SQL ermöglicht es uns, die manuelle Erstellung von SQL-Anweisungen zum Abrufen und Aktualisieren von Dinner- und RSVP-Objekten mit Datenbankdaten zu vermeiden. Stattdessen definieren wir die Dinner- und RSVP-Klassen, wie sie der Datenbank bzw. aus der Datenbank zugeordnet werden, und die Beziehungen zwischen ihnen. LINQ to SQL kümmert sich dann um die Generierung der entsprechenden SQL-Ausführungslogik, die zur Laufzeit verwendet werden soll, wenn wir sie interagieren und verwenden.

Wir können die LINQ-Sprachunterstützung in VB und C# verwenden, um ausdrucksstarke Abfragen zu schreiben, die Dinner- und RSVP-Objekte aus der Datenbank abrufen. Dies minimiert die Menge an Datencode, die wir schreiben müssen, und ermöglicht es uns, wirklich sauber Anwendungen zu erstellen.

Hinzufügen LINQ to SQL Klassen zu unserem Projekt

Klicken Sie zunächst mit der rechten Maustaste auf den Ordner "Modelle" in unserem Projekt, und wählen Sie den Menübefehl Add-New> Item (Neues Element hinzufügen ) aus:

Screenshot des Ordners

Dadurch wird das Dialogfeld "Neues Element hinzufügen" geöffnet. Wir filtern nach der Kategorie "Daten" und wählen die darin enthaltene Vorlage "LINQ to SQL Classes" aus:

Screenshot des Dialogfelds

Wir nennen das Element "NerdDinner" und klicken auf die Schaltfläche "Hinzufügen". Visual Studio fügt eine Datei NerdDinner.dbml unter dem Verzeichnis \Models hinzu und öffnet dann den LINQ to SQL objekt relationalen Designer:

Screenshot des Dialogfelds

Erstellen von Datenmodellklassen mit LINQ to SQL

LINQ to SQL ermöglicht es uns, Datenmodellklassen schnell aus einem vorhandenen Datenbankschema zu erstellen. Dazu öffnen wir die NerdDinner-Datenbank im Server-Explorer und wählen die Tabellen aus, die wir modellieren möchten:

Screenshot: Server-Explorer. Tabellen werden erweitert. Abendessen und R S V P sind hervorgehoben.

Anschließend können wir die Tabellen auf die LINQ to SQL Designeroberfläche ziehen. In diesem Fall erstellt LINQ to SQL automatisch Dinner- und RSVP-Klassen mithilfe des Schemas der Tabellen (mit Klasseneigenschaften, die den Datenbanktabellenspalten zugeordnet sind):

Screenshot des Dialogfelds

Standardmäßig "pluralisiert" der LINQ to SQL Designer Tabellen- und Spaltennamen automatisch, wenn klassen basierend auf einem Datenbankschema erstellt werden. Beispiel: Die Tabelle "Dinners" in unserem obigen Beispiel führte zu einer "Dinner"-Klasse. Diese Klassenbenennung trägt dazu bei, dass unsere Modelle mit .NET-Benennungskonventionen konsistent sind, und ich finde es in der Regel praktisch, dass der Designer dies beheben kann (insbesondere beim Hinzufügen vieler Tabellen). Wenn Ihnen der Name einer Klasse oder Eigenschaft, die vom Designer generiert wird, jedoch nicht gefällt, können Sie ihn jederzeit überschreiben und in einen beliebigen Namen ändern. Dazu können Sie entweder den Entitäts-/Eigenschaftsnamen im Designer inline bearbeiten oder ihn über das Eigenschaftenraster ändern.

Standardmäßig überprüft der LINQ to SQL Designer auch die Primärschlüssel-/Fremdschlüsselbeziehungen der Tabellen und erstellt basierend auf ihnen automatisch standardbasierte "Beziehungszuordnungen" zwischen den verschiedenen Modellklassen, die er erstellt. Wenn wir beispielsweise die Tabellen Dinners und RSVP auf die LINQ to SQL Designer gezogen, wurde eine 1:n-Beziehungszuordnung zwischen den beiden abgeleitet, basierend auf der Tatsache, dass die RSVP-Tabelle einen Fremdschlüssel für die Tabelle Dinners hatte (dies wird durch den Pfeil im Designer angegeben):

Screenshot der Tische

Die obige Zuordnung führt dazu, dass LINQ to SQL der RSVP-Klasse eine stark typisierte "Dinner"-Eigenschaft hinzufügen, die Entwickler verwenden können, um auf das Dinner zuzugreifen, das einem bestimmten RSVP zugeordnet ist. Dies führt auch dazu, dass die Dinner-Klasse über eine "RSVPs"-Sammlungseigenschaft verfügt, mit der Entwickler RSVP-Objekte abrufen und aktualisieren können, die einem bestimmten Dinner zugeordnet sind.

Unten sehen Sie ein Beispiel für IntelliSense in Visual Studio, wenn wir ein neues RSVP-Objekt erstellen und es einer RSVPs-Auflistung von Dinner hinzufügen. Beachten Sie, dass LINQ to SQL automatisch eine "RSVPs"-Auflistung für das Dinner-Objekt hinzugefügt haben:

Screenshot: IntelliSense in Visual Studio. R S V Ps ist hervorgehoben.

Durch Hinzufügen des RSVP-Objekts zur RSVPs-Auflistung von Dinner weisen wir LINQ to SQL an, eine Fremdschlüsselbeziehung zwischen der Dinner- und der RSVP-Zeile in unserer Datenbank zuzuordnen:

Screenshot des R S V P-Objekts und der R S V P-Auflistung von Dinner.

Wenn Ihnen nicht gefällt, wie der Designer eine Tabellenzuordnung modelliert oder benannt hat, können Sie sie überschreiben. Klicken Sie einfach im Designer auf den Zuordnungspfeil, und greifen Sie über das Eigenschaftenraster auf die Eigenschaften zu, um ihn umzubenennen, zu löschen oder zu ändern. Für unsere NerdDinner-Anwendung funktionieren die Standardzuordnungsregeln jedoch gut für die Datenmodellklassen, die wir erstellen, und wir können einfach das Standardverhalten verwenden.

NerdDinnerDataContext-Klasse

Visual Studio erstellt automatisch .NET-Klassen, die die mit dem LINQ to SQL-Designer definierten Modelle und Datenbankbeziehungen darstellen. Eine LINQ to SQL DataContext-Klasse wird auch für jede LINQ to SQL Designerdatei generiert, die der Projektmappe hinzugefügt wird. Da wir unser LINQ to SQL Klassenelement "NerdDinner" benannt haben, wird die erstellte DataContext-Klasse als "NerdDinnerDataContext" bezeichnet. Diese NerdDinnerDataContext-Klasse ist die primäre Art und Weise, wie wir mit der Datenbank interagieren.

Unsere NerdDinnerDataContext-Klasse macht zwei Eigenschaften verfügbar – "Dinners" und "RSVPs", die die beiden Tabellen darstellen, die wir innerhalb der Datenbank modelliert haben. Wir können C# verwenden, um LINQ-Abfragen für diese Eigenschaften zu schreiben, um Dinner- und RSVP-Objekte aus der Datenbank abzufragen und abzurufen.

Der folgende Code veranschaulicht, wie ein NerdDinnerDataContext-Objekt instanziiert und eine LINQ-Abfrage für dieses Objekt ausgeführt wird, um eine Sequenz von Dinners abzurufen, die in der Zukunft auftreten. Visual Studio bietet vollständiges IntelliSense beim Schreiben der LINQ-Abfrage, und die von ihr zurückgegebenen Objekte sind stark typisiert und unterstützen auch IntelliSense:

Screenshot von Visual Studio. Beschreibung ist hervorgehoben.

Neben der Möglichkeit, Dinner- und RSVP-Objekte abzufragen, verfolgt ein NerdDinnerDataContext auch automatisch alle Änderungen, die wir später an den Dinner- und RSVP-Objekten vornehmen, die wir über sie abrufen. Wir können diese Funktionalität verwenden, um die Änderungen problemlos wieder in der Datenbank zu speichern , ohne expliziten SQL-Updatecode schreiben zu müssen.

Der folgende Code veranschaulicht beispielsweise, wie sie mithilfe einer LINQ-Abfrage ein einzelnes Dinner-Objekt aus der Datenbank abrufen, zwei der Dinner-Eigenschaften aktualisieren und dann die Änderungen wieder in der Datenbank speichern:

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

Das NerdDinnerDataContext-Objekt im obigen Code hat automatisch die Eigenschaftenänderungen nachverfolgt, die am Dinner-Objekt vorgenommen wurden, das wir daraus abgerufen haben. Wenn wir die Methode "SubmitChanges()" aufgerufen haben, wird eine entsprechende SQL-Anweisung "UPDATE" in der Datenbank ausgeführt, um die aktualisierten Werte wieder beizubehalten.

Erstellen einer DinnerRepository-Klasse

Für kleine Anwendungen ist es manchmal in Ordnung, dass Controller direkt für eine LINQ to SQL DataContext-Klasse arbeiten und LINQ-Abfragen in die Controller einbetten. Wenn Anwendungen jedoch größer werden, wird dieser Ansatz umständlich zu warten und zu testen. Dies kann auch dazu führen, dass wir dieselben LINQ-Abfragen an mehreren Stellen duplizieren.

Ein Ansatz, der die Verwaltung und Das Testen von Anwendungen vereinfachen kann, ist die Verwendung eines "Repository"-Musters. Eine Repositoryklasse hilft beim Kapseln von Datenabfragen und Persistenzlogik und abstrahiert die Implementierungsdetails der Datenpersistenz aus der Anwendung. Zusätzlich zu einer übersichtlicheren Anwendungscodegestaltung kann die Verwendung eines Repositorymusters das Ändern von Datenspeicherimplementierungen in Zukunft vereinfachen und das Testen von Komponenten einer Anwendung erleichtern, ohne dass eine echte Datenbank erforderlich ist.

Für unsere NerdDinner-Anwendung definieren wir eine DinnerRepository-Klasse mit der folgenden Signatur:

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

Hinweis: Weiter unten in diesem Kapitel extrahieren wir eine IDinnerRepository-Schnittstelle aus dieser Klasse und aktivieren die Abhängigkeitsinjektion damit auf unseren Controllern. Zunächst beginnen wir jedoch einfach und arbeiten direkt mit dem DinnerRepository-Kurs.

Um diese Klasse zu implementieren, klicken Wir mit der rechten Maustaste auf den Ordner "Modelle" und wählen den Menübefehl Add-New> Item (Neues Element hinzufügen ) aus. Im Dialogfeld "Neues Element hinzufügen" wählen wir die Vorlage "Klasse" aus und nennen die Datei "DinnerRepository.cs":

Screenshot des Ordners

Wir können dann unsere DinnerRepository-Klasse mithilfe des folgenden Codes implementieren:

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

Abrufen, Aktualisieren, Einfügen und Löschen mit der DinnerRepository-Klasse

Nachdem wir nun unsere DinnerRepository-Klasse erstellt haben, sehen wir uns einige Codebeispiele an, die allgemeine Aufgaben veranschaulichen, die wir damit ausführen können:

Abfragebeispiele

Der folgende Code ruft ein einzelnes Dinner mit dem DinnerID-Wert ab:

DinnerRepository dinnerRepository = new DinnerRepository();

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

Der folgende Code ruft alle bevorstehenden Abendessen ab und führt eine Schleife über sie aus:

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

Einfügen und Aktualisieren von Beispielen

Der folgende Code veranschaulicht das Hinzufügen von zwei neuen Abendessen. Ergänzungen/Änderungen am Repository werden erst dann für die Datenbank committet, wenn die Methode "Save()" aufgerufen wird. LINQ to SQL umschließt automatisch alle Änderungen in einer Datenbanktransaktion. Daher erfolgen entweder alle Änderungen oder keine änderungen, wenn unser Repository speichert:

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

Der folgende Code ruft ein vorhandenes Dinner-Objekt ab und ändert zwei Eigenschaften dafür. Die Änderungen werden in die Datenbank zurückgeschrieben, wenn die Methode "Save()" in unserem Repository aufgerufen wird:

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

Der folgende Code ruft ein Abendessen ab und fügt diesem dann einen RSVP hinzu. Dazu wird die RSVPs-Auflistung für das Dinner-Objekt verwendet, das für uns erstellt LINQ to SQL (da zwischen den beiden in der Datenbank eine Primärschlüssel-Fremdschlüssel-Beziehung besteht). Diese Änderung wird als neue RSVP-Tabellenzeile in der Datenbank beibehalten, wenn die Methode "Save()" im Repository aufgerufen wird:

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

Beispiel löschen

Der folgende Code ruft ein vorhandenes Dinner-Objekt ab und markiert es dann zum Löschen. Wenn die "Save()"-Methode im Repository aufgerufen wird, wird der Löschvorgang zurück in die Datenbank übertragen:

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

Integrieren von Validierungs- und Geschäftsregellogik in Modellklassen

Die Integration von Validierungs- und Geschäftsregellogik ist ein wichtiger Bestandteil jeder Anwendung, die mit Daten arbeitet.

Schemaüberprüfung

Wenn Modellklassen mithilfe des LINQ to SQL-Designers definiert werden, entsprechen die Datentypen der Eigenschaften in den Datenmodellklassen den Datentypen der Datenbanktabelle. Beispiel: Wenn die Spalte "EventDate" in der Tabelle Dinners ein "datetime" ist, ist die von LINQ to SQL erstellte Datenmodellklasse vom Typ "DateTime" (ein integrierter .NET-Datentyp). Dies bedeutet, dass Kompilierungsfehler auftreten, wenn Sie versuchen, ihr eine ganze Zahl oder einen booleschen Wert aus Code zuzuweisen, und es wird automatisch ein Fehler ausgelöst, wenn Sie versuchen, einen nicht gültigen Zeichenfolgentyp implizit zur Laufzeit zu konvertieren.

LINQ to SQL übernimmt auch automatisch das Entfernen von SQL-Werten für Sie, wenn Sie Zeichenfolgen verwenden . Dies trägt zum Schutz vor SQL-Einschleusungsangriffen bei der Verwendung bei.

Validierungs- und Geschäftsregellogik

Die Schemavalidierung ist als erster Schritt nützlich, reicht aber selten aus. Die meisten realen Szenarien erfordern die Möglichkeit, eine umfassendere Validierungslogik anzugeben, die mehrere Eigenschaften umfassen kann, Code ausführt und häufig den Zustand eines Modells kennen kann (z. B. wird es erstellt /aktualisiert/gelöscht oder innerhalb eines domänenspezifischen Zustands wie "archiviert"). Es gibt eine Vielzahl verschiedener Muster und Frameworks, mit denen Validierungsregeln definiert und auf Modellklassen angewendet werden können, und es gibt mehrere .NET-basierte Frameworks, die dazu verwendet werden können. Sie können so ziemlich jede davon in ASP.NET MVC-Anwendungen verwenden.

Für die Zwecke unserer NerdDinner-Anwendung verwenden wir ein relativ einfaches und einfaches Muster, bei dem wir eine IsValid-Eigenschaft und eine GetRuleViolations()-Methode für unser Dinner-Modellobjekt verfügbar machen. Die IsValid-Eigenschaft gibt true oder false zurück, je nachdem, ob die Validierungs- und Geschäftsregeln alle gültig sind. Die GetRuleViolations()-Methode gibt eine Liste aller Regelfehler zurück.

Wir implementieren IsValid und GetRuleViolations() für unser Dinner-Modell, indem wir unserem Projekt eine "partielle Klasse" hinzufügen. Partielle Klassen können verwendet werden, um Methoden/Eigenschaften/Ereignisse zu Klassen hinzuzufügen, die von einem VS-Designer verwaltet werden (z. B. die Dinner-Klasse, die vom LINQ to SQL-Designer generiert wird) und verhindern, dass das Tool mit unserem Code probleme hat. Wir können unserem Projekt eine neue partielle Klasse hinzufügen, indem Sie mit der rechten Maustaste auf den Ordner \Models klicken und dann den Menübefehl "Neues Element hinzufügen" auswählen. Anschließend können wir die Vorlage "Klasse" im Dialogfeld "Neues Element hinzufügen" auswählen und ihr den Namen Dinner.cs geben.

Screenshot des Ordners Models Neues Element hinzufügen ist ausgewählt. Dinner dot c s wird im Dialogfeld Neues Element hinzufügen geschrieben.

Durch Klicken auf die Schaltfläche "Hinzufügen" wird dem Projekt eine Dinner.cs-Datei hinzugefügt und in der IDE geöffnet. Anschließend können wir mithilfe des folgenden Codes ein grundlegendes Framework für die Regel-/Validierungserzwingung implementieren:

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

Einige Hinweise zum obigen Code:

  • Der Dinner-Klasse ist eine "partielle" Schlüsselwort (keyword) vorangestellt. Das bedeutet, dass der darin enthaltene Code mit der klasse kombiniert wird, die vom LINQ to SQL-Designer generiert/verwaltet und in einer einzelnen Klasse kompiliert wird.
  • Die RuleViolation-Klasse ist eine Hilfsklasse, die wir dem Projekt hinzufügen, mit der wir weitere Details zu einer Regelverletzung bereitstellen können.
  • Die Dinner.GetRuleViolations()-Methode bewirkt, dass unsere Validierungs- und Geschäftsregeln ausgewertet werden (wir werden sie in Kürze implementieren). Anschließend wird eine Sequenz von RuleViolation-Objekten zurückgegeben, die weitere Details zu Regelfehlern bereitstellen.
  • Die Dinner.IsValid-Eigenschaft stellt eine praktische Hilfseigenschaft bereit, die angibt, ob das Dinner-Objekt über aktive RuleViolations verfügt. Es kann von einem Entwickler mithilfe des Dinner-Objekts jederzeit proaktiv überprüft werden (und löst keine Ausnahme aus).
  • Die partielle Dinner.OnValidate()-Methode ist ein Hook, den LINQ to SQL bereitstellt, mit dem wir benachrichtigt werden können, wenn das Dinner-Objekt in der Datenbank beibehalten werden soll. Unsere obige OnValidate()-Implementierung stellt sicher, dass das Dinner keine RuleViolations aufweist, bevor es gespeichert wird. Wenn sie sich in einem ungültigen Zustand befindet, löst sie eine Ausnahme aus, die dazu führt, dass LINQ to SQL die Transaktion abbricht.

Dieser Ansatz bietet ein einfaches Framework, in das wir Validierungs- und Geschäftsregeln integrieren können. Vorerst fügen wir der GetRuleViolations()-Methode die folgenden Regeln hinzu:

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

Wir verwenden das Feature "Return" von C#, um eine Sequenz aller RuleViolations zurückzugeben. Die ersten sechs oben genannten Regelprüfungen erzwingen einfach, dass Zeichenfolgeneigenschaften auf unserem Dinner nicht NULL oder leer sein dürfen. Die letzte Regel ist etwas interessanter und ruft eine PhoneValidator.IsValidNumber()-Hilfsmethode auf, die wir unserem Projekt hinzufügen können, um zu überprüfen, ob das ContactPhone-Nummernformat dem Land/der Region des Dinners entspricht.

Wir können verwenden. Net unterstützt reguläre Ausdrücke, um diese Unterstützung für die Telefonüberprüfung zu implementieren. Im Folgenden finden Sie eine einfache PhoneValidator-Implementierung, die wir unserem Projekt hinzufügen können, mit der wir länder-/regionsspezifische Regex-Musterprüfungen hinzufügen können:

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

Behandeln von Validierungs- und Geschäftslogikverletzungen

Nachdem wir nun den oben genannten Validierungs- und Geschäftsregelcode hinzugefügt haben, werden bei jedem Versuch, ein Dinner zu erstellen oder zu aktualisieren, unsere Validierungslogikregeln ausgewertet und erzwungen.

Entwickler können Code wie unten schreiben, um proaktiv zu ermitteln, ob ein Dinner-Objekt gültig ist, und eine Liste aller Verstöße abrufen, ohne Ausnahmen auszuwerfen:

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
}

Wenn wir versuchen, ein Dinner in einem ungültigen Zustand zu speichern, wird eine Ausnahme ausgelöst, wenn wir die Save()-Methode für dinnerRepository aufrufen. Dies tritt auf, weil LINQ to SQL automatisch unsere Dinner.OnValidate()-Partielle Methode aufruft, bevor die Änderungen des Dinner gespeichert werden. Außerdem haben wir Code zu Dinner.OnValidate() hinzugefügt, um eine Ausnahme auszulösen, wenn Regelverstöße im Dinner vorliegen. Wir können diese Ausnahme abfangen und reaktiv eine Liste der Verstöße abrufen, die behoben werden sollen:

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
}

Da unsere Validierungs- und Geschäftsregeln innerhalb unserer Modellebene und nicht innerhalb der UI-Ebene implementiert werden, werden sie in allen Szenarien in unserer Anwendung angewendet und verwendet. Wir können später Geschäftsregeln ändern oder hinzufügen und alle Code, der mit unseren Dinner-Objekten funktioniert, berücksichtigen.

Die Flexibilität, Geschäftsregeln an einem Ort zu ändern, ohne dass sich diese Änderungen in der Anwendungs- und Ui-Logik bewegen, ist ein Zeichen für eine gut geschriebene Anwendung und ein Vorteil, den ein MVC-Framework fördert.

Nächster Schritt

Wir haben jetzt ein Modell, das wir verwenden können, um unsere Datenbank sowohl abzufragen als auch zu aktualisieren.

Nun fügen wir dem Projekt einige Controller und Ansichten hinzu, die wir verwenden können, um eine HTML-Benutzeroberfläche um es herum zu erstellen.