Screencast
Vorbemerkungen
Seit meinen letzten Postings zum Thema ist eine Menge Zeit vergangen. Mit Stand dieses Artikels steht uns EF mittlweile komplett als Nuget-Package zur Verfügung und hat Version 6.1.2 erreicht. Der Nachfolger kann bereits als Pre-Version geladen werden und es sind so einige coole neue Features und Detailverbesserungen eingeflossen.
In dieser Artikel-Serie möchte ich einfach den aktuellen Stand aus meiner ganz persönlichen Sicht präsentieren. Wer mein Blog regelmäßiger liest weiß, dass ich kein großer Fan von EF Code-First bin. Das hat sich nicht geändert und daher gehe ich auf Themen im Zusammenhang damit auch nicht weiter ein.
Ich werde versuchen, eine kleine aber (subjektiv) feine EF-Solution aufzubauen.
Im ersten Teil geht es vornehmlich um Anpassungen auf Ebene des T4 und den sauberen Umgang mit dem EF-Context. Teil 2 wird sich dann mit EF-Spezifika, wie Dependency Resolver, NLog-Integration und Log-Formatting beschäftigen. ##Die Sample-Solution Um ein voll funktionales Beispiel für die Nachvollziehbarkeit anbieten zu können, habe ich eine solche mal fertig gebaut. Die Solution im Explorer zeigt sich wie folgt:
Dazu ein paar Bemerkungen. Ich benutze als Sample-Datenbank eine SQL Azure Datenbank mit 2 Tabellen. Damit wir gleich mal die ganze Bandbreite der Möglichkeiten anreißen, habe ich ein Projekt vom Typ “SQL Server Database Project” in der Solution enthalten. Dieses kann man nur dann im VS bewundern, wenn man die entsprechende Extension (SQL Server Data Tools - SSDT) (kostenfrei) installiert. In diesem Artikel habe ich bereits ansatzweise darüber berichtet. Ich habe mir fest vorgenommen, mehr darüber in einem künftigen Post zu berichten.
Alle in Abb. 1 enthaltenen Elemente werden in diesem Artikel noch eine Rolle spielen, woran Kenner von EF vielleicht sehen können, dass es hier ein wenig mehr in die Tiefe gehen soll.
Das Standard-Template und seine Problemchen
Fangen wir einmal ganz seicht an und kümmern uns erstmal direkt mit dem Projekt “Data.Core”, das u.a. unsere EDMX-Datei und damit das gemappte Model enthält. Hier zunächst einmal das EF-Modell nachdem ich es per Database-First aus meiner Azure-Datenbank erzeugt habe:
Ich habe also 2 simple Tabellen in der DB, die über einen Foreign Key miteinander verbinden sind. EF hat dies richtig erkannt und somit zusätzlich zu den “normalen” Eigenschaften die Navigationseigenschaften erzeugt. Bis hier alles ganz normal. Im Unit-Test-Projekt, das in meiner Sample-Solution das UI ersetzt, könnte ich also jetzt folgendes benutzen:
using (var ctx = new SampleEntities())
{
// work with ctx
}
Das ist so ziemlich das bekannteste Stück Code, wenn es um die Programmierung mit EF geht überhaupt. Wir benutzen also die durch den EF-Designer erzeugte Klasse SampleEntities
als Wrapper für den Zugriff auf die Datenbank. Wir sind sogar so sauber, das ganze mit einem using
zu klammern, damit nach Benutzung der teuren Ressource EF-Context, die wiederum unmanaged Ressourcen (SQL-Server-Netzwerk-Connection) benutzt, alles sauber aufgeräumt (Dispose()
) wird. Super?
Ich sage: “Nein!”
Das Problem hier ist aus Sicht einer sauberen Architektur, dass aus dem Unit-Test-Projekt heraus direkt eine neue Instanz unseres Context erzeugt wird. Das ist funktional korrekt, führt aber dazu, dass nun jeder unseren Context so nutzen kann, wie er es gerade für richtig hält. Eine in EF 6 neu hinzugekommene Methode veranschaulich das. Erweitern wir das Sample an der immer noch falschen Zeile doch mal um eine Zeile:
using (var ctx = new SampleEntities())
{
// set up ctx
ctx.Database.Log = m => Trace.TraceInformation(m);
// work with ctx
}
Die Database.Log
-Eigenschaft eines jeden Context ist vom Typ Action<string>
und nimmt einfach eine Methode entgegen, deren einzige Bedingung die ist, dass sie einen string entgegen nimmt. Was sie damit tut, ist dem Context völlig gleich. EF ruft diese Methode nun immer auf, wenn es eine Nachricht loggen möchte. In unserem Fall tauchen dann ganz einfach Meldungen im Output-Fenster des Visual Studio auf. Wer mag, kann das gern mal probieren. Ich habe die Lambda-Version gewählt, weil TraceInformation überladen ist und EF sonst nicht wüsste,welche der Überladungen ich wohl meine.
Doch warum zeigt uns Listing 2, dass Listing 1 keine gute Idee ist? Wenn man dieses Pattern weiter verfolgt, dann taucht plötzlich an jeder Stelle unserer Solution, die gerade einen neuen EF-Context braucht, Listing 1 auf. Manche Teammitglieder werden an den Logger aus Listing 2 denken, andere vielleicht nicht und schon hat man ein Tracing, das einem nichts bringt. Listing 2 zeigt uns daher einfach nur eins: Wir brauchen einen einzelnen Punkt, der uns einen Context liefert. Wir brauchen eine Factory für unseren Context:
namespace codingfreaks.EfSample.Data.Core
{
using System.Diagnostics;
/// <summary>
/// Is used to provide access to the database context.
/// </summary>
public static class DbContextUtil
{
#region properties
/// <summary>
/// Retrieves a fresh usable context to the caller.
/// </summary>
public static SampleEntities Context
{
get
{
var context = new SampleEntities();
// set options for the context here
context.Database.Log = m => Trace.TraceInformation(m);
return context;
}
}
#endregion
}
}
Das ist schonmal eher, was ich mir so vorstelle. Da gibt es eine statische Klasse, die mir immer einen frischen Context liefert und ich habe einen einzelnen Punkt, an dem ich das Logging einschalten kann. Listing 1 wäre dann umzuschreiben in:
using (var ctx = DbContextUtil.Context)
{
// use ctx
}
Na super, das war’s? Immer noch: “Nein, nicht ganz!“. Nur, weil wir nun die Factory haben, haben wir immer noch keinen “gezwungen” diese auch zu nutzen. Das ist aber genau das, was ich als Designer meines “Mini-Frameworks” möchte. Die Programmierer (also auch ich) sollen ausschließlich die Factory benutzen müssen. Aber das wirft Fragen auf.
Woher kommt der Context?
Der Context SampleEntities
ist ein Typ in unserer Solution. Er wurde durch EF in dem Moment erzeugt, in dem wir das Model aus der Datenbank generiert haben. Man kann sich die entsprechende Klasse auch ganz einfach ansehen:
Im Solution Explorer kann die EDMX-Datei “aufgeklappt” werden. In ihr finden wir u.a. 2 Dateien mit der Endung “tt”. Dies sind T4-Templates. Das ist vereinfacht eine Sprache, die innerhalb des VS (also nicht beim Build) dazu genutzt wird, nach bestimmten Regeln Quellcode zu erzeugen. die SampleModelContext.tt kümmert sich darum, unseren Context zu “programmieren”. Sehen wir uns einmal einen Auszug aus dieser Datei an:
using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
<#
if (container.FunctionImports.Any())
{
#>
using System.Data.Entity.Core.Objects;
using System.Linq;
<#
}
#>
<#=Accessibility.ForType(container)#> partial class <#=code.Escape(container)#> : DbContext
{
public <#=code.Escape(container)#>()
: base("name=<#=container.Name#>")
{
<#
Das sieht fast aus, wie C#, aber eben nur fast. Das wichtigste ist hier erstmal die Erkenntnis, dass man die Datei anpassen kann und darf. Eine einfache Übung. Wenn wir in Zeile 13 einfach einfügen:
// Hello
und dann die T4-Datei speichern, arbeitet das VS kurz und wir können uns nun per Doppelklick auf die darunter verborgene SampleModel.Context.cs
das Ergebnis sofort ansehen:
using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
// Hello
public partial class SampleEntities : DbContext
{
Wir können also konfigurieren (genauer gesagt, skripten), wie EF nach dem Auslesen der Datenbankinformationen die entsprechenden Typen für uns baut. Das ist gut, denn es löst unser Problem von oben. Wie? Gehen wir zurück in die Datei SampleModel.Context.tt
, entfernen wir unser lustiges Hallo und machen aus dem public
in Zeile 58 einfach ein internal
:
internal <#=code.Escape(container)#>()
Dann wieder Speichern und fertig ist unser sauberes Pattern. Die Klasse DbContextUtil
ist in der gleichen Assembly, wie die Klasse SampleEntities
und kann daher einen Internal-Konstruktur nutzen. Alle anderen Klassen außen können das aber nicht mehr. Ab sofort funktioniert Listing 4 ohne Probleme, Listing 1 aber nicht mehr.
Das ist, wie ich finde, ein elegantes Beispiel für die nützliche Verwendung von T4 im EF-Umfeld. Es ist aber nicht das Einzige!
Erkenne Deine Pappenheimer!
Ein gängige Problem bei der Verwendung von EF ist, dass es einfach nur POCOs erzeugt, also Klassen, die einfach nur von Object erben. Eigentlich ist das gar nicht so verkehrt. Es sorgt nur in vielen Projekten dafür, dass wir keinen Hebel haben, um die von EF erzeugten Entitäten irgendwie von anderen normalen Typen zu unterscheiden. Über T4 und Interfaces kann hier allerdings relativ einfach Abhilfe geschaffen werden.
In meinen Projekten weiß ich eines über alle meine Entities. Jede meiner Tabellen hat immer eine Spalte namens “Id” vom Typ bigint
. Diese Konvention nutze ich nun, um meinen Entities etwas mehr Grips zu geben. Ich entwerfe mir zunächst in einem eigenen Projekt (Klassen-Bibliothek) ein Interface:
namespace codingfreaks.EfSaple.Logic.Interfaces
{
/// <summary>
/// Must be implemented by all entities.
/// </summary>
public interface IEntity
{
#region properties
/// <summary>
/// The database id of the entity.
/// </summary>
long Id { get; set; }
#endregion
}
}
Jetzt muss ich meinem Projekt, das das EF-Model hält natürlich eine Referenz auf diese DLL geben. Danach kann man nun die Datei SampleModel.tt
unterhalb von SampleModel.edmx
anpassen. Diese Anpassung ist nun etwas komplexer, als in Listing 7 und daher gehen wir Schritt für Schritt vor.
Namespace einbinden
Als erstes müssen wir dafür sorgen, dass jede der durch das T4 generierten Klassen unseren Namespace aus Listing 8 in das using
einbindet. Die Namespaces werden im Original-T4 über eine T4-Methode namens codeStringGenerator.UsingDirectives
eingebunden (siehe Zeile 26 in T4). Diese passen wir nun an. Die Methode beginnt innerhalb der SampleModel.tt
in Zeile 422. Nach meinen Anpassungen sieht sie so aus:
public string UsingDirectives(bool inHeader, bool includeCollections = true)
{
return inHeader == string.IsNullOrEmpty(_code.VsNamespaceSuggestion())
? string.Format(
CultureInfo.InvariantCulture,
"{0}using System;{1}" +
"{2}",
inHeader ? Environment.NewLine : "",
includeCollections ? (Environment.NewLine + "using System.Collections.Generic;" + Environment.NewLine + "using codingfreaks.EfSaple.Logic.Interfaces;") : "",
inHeader ? "" : Environment.NewLine)
: "";
}
Ich habe in Listing 10 einfach nur ein using angehängt.
Wenn man nun das T4 speichert und dann aufdrillt:
Sieht man sich nun z.B. die Klasse Vehicle an (die ja gerade neu generiert wurde), sieht man, dass das using tatsächlich erscheint.
Interface einbinden
Nun ist es Zeit, das IEntity
-Interface einzubinden. Zurück in der SampleModel.tt findet man eine weitere Methode codeStringGenerator.EntityClassOpening
. Diese befindet sich in Zeile 307 und wurde durch mich wie folgt angepasst:
public string EntityClassOpening(EntityType entity)
{
var result = string.Format(
CultureInfo.InvariantCulture,
"{0} {1}partial class {2}{3}",
Accessibility.ForType(entity),
_code.SpaceAfter(_code.AbstractOption(entity)),
_code.Escape(entity),
_code.StringBefore(" : ", _typeMapper.GetTypeName(entity.BaseType)));
var pattern = "{0} : {1}";
if (result.Contains(":"))
{
pattern = "{0}, {1}";
}
return string.Format(CultureInfo.InvariantCulture, pattern, result, "IEntity");
}
Hier war ein wenig mehr Arbeit notwendig, damit wir wirklich sauber unser Interface anhängen, selbst wenn EF entschieden hätte, bereits eine Vererbung vorzunehmen. EF ist von Haus aus in der Lage, von etwas anderem als Object zu erben (mehr dazu unter Entitiy Framework und WCF in SOA-Projekten). Je nachdem, ob bereits ”:” im Class Opening erstellt wurde, erzeugen wir nun eine gültige Interface-Implementierung.
Nach erneutem Speichern des T4 ist alles erledigt und jedes unserer Entities implementiert das Interface.
Wozu das alles?
Die Antwort auf diese Frage lautet: Das hängt davon ab. Es hängt z.B. davon ab, wie sehr man innerhalb seiner Projekte auf Vererbung und Wiederverwendung achtet. Ich habe z.B. eine wiederverwendbare Bibliothek, die mir für Entity-Objekte typische Logik, wie “Gib mir alle Elemente des Typs xy.” oder “Speichere diese Entity und erzeuge dann einen Log-Eintrag.” usw. abnimmt. Um hier sicherzustellen, dass diese Utils wissen, was sie sich schnappen dürfen, setze ich Generic Constraints ein und im Fall der Entity-Typen ist das Constraint das Interface IEntity.