Juli 2015

Band 30, Nummer 7

Datenpunkte – Untersuchen des Verhaltens von Entity Framework in der Befehlszeile mit Scriptcs

Von Julie Lerman

Julie LermanIch habe viel Zeit damit verbracht, Benutzern das Verhalten von Entity Framework im Hinblick auf Beziehungen und nicht verbundene Daten zu vermitteln. EF folgt nicht immer den von Ihnen angenommenen Regeln. Mein Ziel besteht also stets darin, Ihnen bewusst zu machen, was Sie erwarten können, warum einige Dinge auf eine bestimmte Weise funktionieren und wie Sie mit einem bestimmten Verhalten arbeiten oder dieses vermeiden können.

Wenn ich Entwicklern etwas zeigen möchte, steht mir eine Handvoll von Workflowoptionen zur Verfügung. Ich kann z. B. Integrationstests erstellen und diese nacheinander ausführen, während ich das einzelne Verhalten erörtere. Was ich an dieser Methode nicht mag: Ich möchte häufig debuggen, damit ich einen Drillvorgang für Objekte ausführen und zeigen kann, was hinter den Kulissen geschieht. Das Debuggen von automatisierten Tests kann jedoch langsam sein, weil Visual Studio alle Quellen laden muss.

Ein anderer Workflow, den ich verwendet habe, umfasst das Erstellen einer Konsolen-App und deren Debuggen mithilfe von Haltepunkten, an denen Objekte untersucht werden. Dies ist auch nicht so interaktiv, wie ich es mir wünschen würde, weil der gesamte Code im Vorfeld vorhanden sein muss.

Es wäre optimal, wenn ich etwas Code eingeben und dann ausführen könnte, damit mein Publikum die Auswirkungen sehen kann. Anschließend würde ich diesen Code gern ändern und erneut ausführen, um die abweichende Reaktion zu zeigen. Ich habe diese Methode z. B. für den folgenden Code verwendet, um den Unterschied zwischen diesem Code zu zeigen:

using (var context = new AddressContext()) {
  aRegionFromDatabase=context.Regions.FirstOrDefault();
  context.Address.Add(newAddress);
  newAddress.Region=aRegionObjectFromDatabase;
}

diesem:

using (var context = new AddressContext()) {
  context.Address.Add(newAddress);
  newAddress.Region=aRegionObjectFromDatabase;
}

und diesem:

newAddress.Region=aRegionObjectFromDatabase;
using (var context = new AddressContext()) {
  context.Address.Add(newAddress);
}

Der Unterschied besteht übrigens darin, dass im ersten Beispiel, in dem EF die Region in der gleichen Kontextinstanz abruft, EF über die Information verfügt, dass die Region bereits vorhanden ist, und nicht versucht, diese erneut der Datenbank hinzuzufügen. Im zweiten und dritten Beispiel wird durch die EF-Regeln abgeleitet, dass die Region (wie ihre "übergeordnete" Adresse) neu ist, und sie wird der Datenbank hinzugefügt. Auf lange Sicht empfehle ich, einen Fremdschlüsselwert und keine Instanz zu verwenden. Dies ist wirklich hilfreich, um Ursache und Wirkung zeigen zu können.

Natürlich könnte ich diese Wirkungen auch mit Tests zeigen, aber das wäre etwas verworren. Außerdem gilt das Argument, dass Tests nicht Theorien beweisen sollen, sondern Code überprüfen. Ich mag Unterschiede auch nicht anhand einer Konsolen-App zeigen, weil die App nach jeder Änderung am Code ausgeführt werden muss. Eine bessere Alternative besteht in der Verwendung der fantastischen LinqPad-Anwendung. Auf diese Option habe ich schon einige Male zurückgegriffen. Scriptcs stellt mir jetzt jedoch eine vierte Methode zur Verfügung, um meine Demofähigkeiten zu optimieren.

Scriptcs (scriptcs.net) ist seit 2013 verfügbar und uneingeschränkt Open Source. Die Anwendung basiert auf Roslyn und stellt eine Befehlszeilenlaufzeit bereit, die das Ausführen von C#-Code außerhalb von Visual Studio ermöglicht. Scriptcs bietet auch einen sehr einfachen Mechanismus zum Erstellen von Skripts mit C# in jedem gewünschten Text-Editor. Diese Skripts können dann über die Befehlszeile ausgeführt werden.

Ich hatte schon häufig von Scriptcs gehört, weil Glenn Block, vor dem ich großen Respekt habe, eine der Schlüsselpersonen hinter Scriptcs ist. Ich habe mich bis vor Kurzem jedoch nicht näher damit beschäftigt. Dies geschah erst, als ich die Ausführungen von Block zur rosigen Zukunft von Scriptcs in einer Folge von DotNetRocks (bit.ly/1AA1m4z) hörte. Mein erster Gedanke beim Kennenlernen dieses Tools war, dass es mir interaktive Demos direkt in der Befehlszeile ermöglichen könnte. Und durch die Kombination mit EF kann ich Scriptcs mit den Lesern dieser Kolumne, die sich auf Daten konzentriert, teilen.

Eine kurze Einführung in Scriptcs

Es gibt viele nützliche Ressourcen für Scriptcs. Ich bin alles andere als eine Expertin, daher stelle ich Ihnen nur einige Highlights vor und verweise auf die scriptcs.net-Website, auf der Sie weitere Informationen erhalten können. Ich fand auch das kurze Video von Latish Sengal unter bit.ly/1R6mF8s sehr hilfreich, um erste Einblicke in Scriptcs zu erhalten.

Zunächst müssen Sie Scriptcs auf Ihrem Entwicklungscomputer mithilfe von Chocolatey installieren. Dies bedeutet, dass auch Chocolatey auf Ihrem Computer installiert sein muss. Wenn Sie noch nicht über Chocolatey verfügen: Es ist ein großartiges Tool zum Installieren von Softwaretools. Chocolatey verwendet NuGet zum Installieren von Tools (und den Tools oder APIs, von denen sie abhängen) auf die gleiche Weise, wie NuGet Pakete zum Bereitstellen von Assemblys verwendet.

Scriptcs bietet eine REPL-Umgebung (Read, Evaluate, Play, Loop), die Sie sich ähnlich einer Laufzeitumgebung vorstellen können. Sie können Scriptcs zum schrittweisen Ausführen von C#-Befehlen in der Befehlszeile oder zum Ausführen von Scriptcs-Skriptdateien verwenden, die in einem Text-Editor erstellt wurden. Es sind sogar Scriptcs-IntelliSense-Erweiterungen für einige Editoren verfügbar. Die beste Möglichkeit, Scriptcs in Aktion zu erleben, besteht darin, mit der zeilenweisen Ausführung zu beginnen. Beginnen wir damit.

Nachdem Scriptcs installiert wurde, navigieren Sie zu dem Ordner, in dem der gespeicherte Code gespeichert werden soll, und führen Sie dann den Befehl "scriptcs" aus. Auf diese Weise wird die REPL-Umgebung gestartet, und Sie werden informiert, dass Scriptcs ausgeführt wird. Außerdem wird eine >-Eingabeaufforderung zurückgegeben.

An der Eingabeaufforderung können Sie beliebige C#-Befehle eingeben, die in einigen der am häufigsten verwendeten .NET-Namespaces enthalten sind. Diese sind alle direkt verfügbar und stammen aus den gleichen Assemblys, über die ein typisches .NET-Konsolenanwendungsprojekt standardmäßig verfügt. Abbildung 1 zeigt, wie Scriptcs an der Eingabeaufforderung gestartet wird und dann eine Zeile C#-Code ausführt, sowie die Ergebnisse, die angezeigt werden.

Ausführen von C#-Code in der Befehlszeile in der REPL-Umgebung von Scriptcs
Abbildung 1 – Ausführen von C#-Code in der Befehlszeile in der REPL-Umgebung von Scriptcs

Scriptcs verfügt über eine Reihe von Direktiven, mit denen Sie z. B. auf eine Assembly verweisen ("#r") oder eine vorhandene Skriptdatei laden können ("#load").

Als weiteres nützliches Feature können Sie mit Scriptcs auf einfache Weise NuGet-Pakete installieren. Dies geschieht in der Eingabeaufforderung mithilfe des Parameters "-install" des Scriptcs-Befehls. Abbildung 2 zeigt z. B., was geschieht, wenn ich Entity Framework installiere.

Installieren von NuGet-Paketen in Scriptcs
Abbildung 2 – Installieren von NuGet-Paketen in Scriptcs

Paketinstallationen bieten jedoch weit mehr, wie Sie dem Screenshot meines Ordners entnehmen können, nachdem ich Entity Framework installiert habe (siehe Abbildung 3) – Scriptcs hat einen Ordner "scriptcs_packages" zusammen mit den Inhalten des betreffenden Pakets erstellt.

Der NuGet-Paket-Installer von Scriptcs erstellt vertraute Paketordner und Konfigurationsdateien
Abbildung 3 – Der NuGet-Paket-Installer von Scriptcs erstellt vertraute Paketordner und Konfigurationsdateien

Erstellen eines Modells und allgemeinen Setupcodes

Ich werde meine Experimente anhand eines bestimmten Modells ausführen, das ich bereits in Visual Studio mithilfe von Code First erstellt habe. Ich verfüge über eine Assembly mit meinen Klassen – "Monkey", "Banana" und "Country" – und eine andere Assembly, die einen "MonkeysContext" enthält, der von "DbContext" von EF erbt und die "DbSets" "Monkeys", "Bananas" und "Countries" bereitstellt. Ich habe mithilfe des Features "Entitätsdatenmodell anzeigen" der Entity Framework Power Tools-Erweiterung für Visual Studio (bit.ly/1K8qhkO) bestätigt, dass mein Modell ordnungsgemäß eingerichtet wurde. Auf diese Weise weiß ich, dass die kompilierten Assemblys, die ich in meinen Befehlszeilentests verwende, zuverlässig sind.

Damit ich diese Experimente ausführen kann, muss ich ein allgemeines Setup ausführen. Ich benötige Verweise auf meine benutzerdefinierten Assemblys sowie auf die EF-Assembly, und ich benötige Code, der neue monkey -, country- und MonkeysContext-Objekte instanziiert.

Anstatt dieses Setup an der Eingabeaufforderung laufend zu wiederholen, erstelle ich eine Skriptdatei für Scriptcs. Skriptdateien, die die Erweiterung CSX aufweisen, werden häufig verwendet, um Scriptcs zu nutzen. Anfangs habe ich Notepad++ mit dem Scriptcs-Plug-In verwendet, dann wurde mir vorgeschlagen, Sublime Text 3 (sublimetext.com/3) zu testen. Auf diese Weise konnte ich nicht nur das Scriptcs-Plug-In verwenden, sondern auch OmniSharp, ein C#-IDE-Plug-In für Sublime Text 3, das eine erstaunliche Codierungsbenutzeroberfläche bereitstellt, wenn Sie berücksichtigen, dass Sie in einem Text-Editor arbeiten, der OSX und Linux sowie Windows unterstützt.

Mit OmniSharp können Sie Scriptcs erstellen sowie den Buildvorgang für viele andere Systeme ausführen. Nachdem Sublime eingerichtet wurde, bin ich bereit, eine CSX-Datei zu erstellen, die den allgemeine Setupcode kapselt, den ich zum Testen meines Modells verwenden möchte.

Im Text-Editor erstelle ich eine neue Datei ("SetupForMonkeyTests.csx"), füge einige Scriptcs-Befehle für Verweise auf die benötigten Assemblys hinzu und füge dann C#-Code zum Erstellen einiger Objekte wie hier gezeigt hinzu:

#r "..\DataModel Assemblies\DataModel.dll"
#r "..\DataModel Assemblies\DomainClasses.dll"
#r "..\scriptcs_packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll"
using DomainClasses;
using DataModel;
using System.Data.Entity;
var country=new Country{Id=1,Name="Indonesia"};
var monkey=Monkey.Create("scripting gal", 1);
Database.SetInitializer(new NullDatabaseInitializer<MonkeysContext>());
var ctx=new MonkeysContext();

Beachten Sie, dass die #r-Befehle Scriptcs-Befehle zum Hinzufügen von Verweisen auf erforderliche Assemblys sind. Auf zahlreiche häufig verwendete Systemassemblys wird standardmäßig verwiesen, um einen Eindruck beim ersten Ausführen bereitzustellen, der dem Eindruck ähnelt, den Sie beim Erstellen eines neuen Konsolenprojekts in Visual Studio erhalten. Diese muss ich also nicht angeben. Ich muss aber auf meine benutzerdefinierten Assemblys sowie auf die von NuGet installierte EntityFramework-Assembly verweisen. Der Rest ist einfach C#-Code. Beachten Sie, dass ich hier keine Klassen definiere. Dies ist ein Skript. Scriptcs liest und führt nur zeilenweise den Inhalt der Datei aus. In Kombination mit OmniSharp Sublime Text kann ich das Projekt sogar erstellen, um sicherzustellen, dass meine Syntax richtig ist.

Mit dieser Datei kehre ich zurück zur Befehlszeile und untersuche Aspekte des EF-Verhaltens mithilfe meines Modells und meiner Klassen.

Weiter mit meinen Befehlszeilenexperimenten mit EF

Zurück an der Eingabeaufforderung starte ich die REPL-Umgebung von Scriptcs nur mit dem Befehl "scriptcs" und ohne Parameter.

Sobald die REPL-Umgebung aktiv ist, lade ich im ersten Schritt die CSX-Datei. Anschließend verwende ich die Befehle ":references" und ":vars", um zu überprüfen, ob die REPL-Umgebung die CSX-Datei richtig ausgeführt hat. (Scriptcs weist eine Reihe von REPL-Befehlen auf, die mit einem Doppelpunkt beginnen. Sie können eine Liste dieser Befehle anzeigen, indem Sie ":help" eingeben.) Abbildung 4 zeigt meine Sitzung bis zu diesem Zeitpunkt. Sie sehen alle APIs, auf die verwiesen wird, sowie die Objekte, die ich erstellt habe.

Abbildung 4 – Starten von Scriptcs, Laden einer CSX-Datei, Überprüfen von Verweisen und vorhandenen Variablen

D:\ScriptCS Demo>scriptcs
scriptcs (ctrl-c to exit or :help for help)
> #load "SetupForMonkeyTests.csx"
> :references
[
  "System",
  "System.Core",
  "System.Data",
  "System.Data.DataSetExtensions",
  "System.Xml",
  "System.Xml.Linq",
  "System.Net.Http",
  "C:\\Chocolatey\\lib\\scriptcs.0.14.1\\tools\\ScriptCs.Core.dll",
  "C:\\Chocolatey\\lib\\scriptcs.0.14.1\\tools\\ScriptCs.Contracts.dll",
  "D:\\ScriptCS Demo\\scriptcs_packages\\EntityFramework.6.1.3\\lib\\   
    net45\\EntityFramework.dll",
  "D:\\ScriptCS Demo\\scriptcs_packages\\EntityFramework.6.1.3\\lib\\
    net45\\EntityFramework.SqlServer.dll",
  "D:\\ScriptCS Demo\\DataModel Assemblies\\DataModel.dll",
  "D:\\ScriptCS Demo\\DataModel Assemblies\\DomainClasses.dll"
]
> :vars
[
  "DomainClasses.Country country = DomainClasses.Country",
  "DomainClasses.Monkey monkey = DomainClasses.Monkey",
  "DataModel.MonkeysContext ctx = DataModel.MonkeysContext"
]
>

Ich kann die Objekte auch überprüfen, indem ich einfach den Variablennamen in die Eingabeaufforderung eingebe. Hier sehen Sie z. B. mein monkey-Objekt:

> monkey
{
  "Id": 0,
  "Name": "scripting gal",
  "Bananas": [],
  "CountryOfOrigin": null,
  "CountryOfOriginId": 1,
  "State": 1
}
>

Nun bin ich bereit, einige Aspekte des EF-Verhaltens zu untersuchen. Ich kehre zu meinen früheren Beispielen zurück und überprüfe, wie EF auf das Anfügen des country-Objekts an das monkey-Objekt reagiert, wenn der Kontext das monkey-Objekt nachverfolgt oder nicht nachverfolgt oder der Kontext das country-Objekt nachverfolgt oder nicht nachverfolgt.

Im ersten Test füge ich das bereits vorhandene country-Objekt an die CountryOfOrigin-Eigenschaft des monkey-Objekts an und füge dann das monkey-Objekt dem Kontext hinzu. Schließlich verwende ich die DbContext.Entry().State-Eigenschaft, um zu untersuchen, wie EF den Status der Objekte interpretiert. Es ist sinnvoll, dass das monkey-Objekt hinzugefügt wird. Beachten Sie jedoch, dass EF davon ausgeht, dass das country-Objekt ebenfalls hinzugefügt wird. Auf diese Weise behandelt EF ein Diagramm. Da ich die Add-Methode für den Stamm des Diagramms ("monkey") verwendet habe, markiert EF alle Elemente im Diagramm als hinzugefügt. Beim Aufrufen von "SaveChanges" wird das country-Objekt in die Datenbanktabelle eingefügt, und wie Sie in Abbildung 5 sehen können, sind zwei Zeilen für Indonesien vorhanden. Die Tatsache, dass das country-Objekt bereits einen Schlüsselwert (ID) besaß, wird ignoriert.

Verwenden von Scriptcs, um sofort zu sehen, wie EF auf die DbSet.Add-Methode mit einem Diagramm reagiert
Abbildung 5 – Verwenden von Scriptcs, um sofort zu sehen, wie EF auf die DbSet.Add-Methode mit einem Diagramm reagiert

Im nächsten Schritt verwende ich den Befehl ":reset", um den REPL-Verlauf zu löschen, und lade dann die CSX-Datei mit dem Befehl "#load" erneut. Anschließend werde ich einen neuen Workflow testen – ich werde dem Kontext das monkey-Objekt hinzufügen und dann das country-Objekt an das monkey-Objekt anfügen, das bereits nachverfolgt wird. Beachten Sie, dass nicht alle Reaktionen in der folgenden Liste enthalten sind:

> :reset
> #load "SetupForMonkeyTests.csx"
> ctx.Monkeys.Add(monkey);
{...response...}
> monkey.CountryOfOrigin=country;
{...response...}
> ctx.Entry(monkey).State.ToString()
Added
> ctx.Entry(country).State.ToString()
Added

Der Kontext weist dem country-Objekt erneut den Status "Added" zu, weil ich es an eine andere Entität mit dem Status "Added" angefügt habe.

Wenn Sie die Navigationseigenschaft wirklich verwenden möchten, besteht das Muster, das zum Erfolg führt, darin, sicherzustellen, dass der Kontext bereits für das Land aktiviert ist. Dies ist der Fall, weil Sie das Land mithilfe einer Abfrage in der gleichen Kontextinstanz abgerufen haben, was dazu führt, dass der Kontext dem Objekt den Status "Unchanged" zuweist, oder Sie haben den Status "Unchanged" manuell selbst zugewiesen.

Abbildung 6 zeigt ein Beispiel für letzteren Fall und ermöglicht mir, diesen Test auszuführen, ohne mit einer Datenbank zu arbeiten.

Abbildung 6 – Manuelles Zuweisen des Status "Unchanged"

> :reset
> #load "SetupForMonkeyTests.csx"
> ctx.Entry(country).State=EntityState.Unchanged;
2
> ctx.Monkeys.Add(monkey);
{...response...}
> monkey.CountryOfOrigin=country;
{...response...}
> ctx.Entry(monkey).State.ToString()
Added
> ctx.Entry(country).State.ToString()
Unchanged
>

Da der Kontext das Land bereits nachverfolgt hat, wird der Status des country-Objekts nicht geändert. EF ändert den Status des verbundenen Objekts nur, wenn sein Status unbekannt ist.

Scriptcs ist weit mehr als nur ein großartiges Lernhilfsmittel für mich

Persönlich ziehe ich es vor, die Verwirrung um diese Regeln ganz zu vermeiden und einfach den Fremdschlüsselwert festzulegen ("CountryOf­OriginId=country.Id"), ohne einen Verweis mithilfe einer Navigationseigenschaft anzufügen. Mit diesem Muster kann ich anschließend tatsächlich die CountryOfOrigin-Navigationseigenschaft genauer untersuchen und mir überlegen, ob sie überhaupt vorhanden sein soll. Das Demonstrieren der Varianten und der Reaktion von EF auf jedes Szenario ist eine Lektion, die Vielen die Augen öffnet, die diese Demo gesehen haben.

Beim Versuch, Entwicklern diese Verhalten zu zeigen, gefällt mir die sofortige und offensichtliche Reaktion, die ich von einem interaktiven Fenster erhalten kann. Dies kann ich zwar mit LINQPad ebenfalls erreichen, ich mag jedoch die Befehlszeileninteraktion. Noch wichtiger ist es, diese Experimente ausgeführt zu haben, um mich mit Scriptcs vertraut zu machen. Ich bin mir jetzt des großen Werts bewusst, der zusätzlich zum Ausführen von Befehlszeilentests für eine API bereitgestellt wird.


Julie Lerman ist Microsoft MVP, .NET-Mentor und Unternehmensberaterin. Sie lebt in den Bergen von Vermont. Sie hält bei User Groups und Konferenzen in der ganzen Welt Vorträge zum Thema Datenzugriff und anderen .NET-Themen. Julie Lerman führt unter thedatafarm.com/blog einen Blog. Sie ist die Autorin von "Programming Entity Framework" (2010) sowie von "Code First" (2011) und "DbContext" (2012). Alle Ausgaben sind im Verlag O’Reilly Media erschienen. Folgen Sie ihr auf Twitter unter twitter.com/julielerman, und besuchen Sie ihre Pluralsight-Kurse unter juliel.me/PS-Videos.

Unser Dank gilt dem folgenden technischen Experten von Microsoft für die Durchsicht dieses Artikels: Justin Rusbatch