Reverse Engineering mit Entity Framework Core – Integration eines bestehenden relationalen Datenbanksystems

Jan-Hendrik Precht

Jan-Hendrik Precht begeistert sich für die Themen "lebenslanges Lernen" und "systemische Unterstützung in Personalentwicklung und Vertrieb". Zum Einen, weil Ihn sowohl die Vielfalt an möglichen Themen als auch innovative Steuerungsmöglichkeiten faszinieren. Zum Anderen, weil letztendlich alle Beteiligten etwas davon haben.

Nicht alle Entwicklungsprojekte beginnen mit dem initialen Entwurf am Reißbrett. Besteht vielmehr die Anforderung ein bestehendes relationales Datenbanksystem in deine .NET Softwareanwendung zu integrieren, dann stellt dir Entity Framework Core (im Folgenden EF Core) ein nützliches Werkzeug zur Verfügung, den erforderlichen Database-First Approach zu meistern.

Dieser Blogbeitrag zeigt dir, wie die EF Core CLI das Reverse Engineering eines bestehenden relationalen Datenbanksystems vereinfacht und die Integration in deine Softwareanwendung entscheidend beschleunigen kann. EF Core ist das aktuell populärste ORM Framework für Microsoft .NET.

Vorteile

Folgende Vorteile bieten sich dir:

  • Du gelangst schnell zu einem objektorientierten Datenbankmodell für deine .NET Softwareanwendung.
  • EF Core bietet automatische Codegenerierung von Entity-Klassen (Modelle) und der DbContext-Klasse (Verbindung mit der Datenbank).
  • Durch Verwendung von partial Klassen und virtual Properties ist der generierte Code flexibel durch eigene Logik erweiterbar.
  • EF Core unterstützt u.a. LINQ-Abfragen und Änderungsverfolgung deiner Datensätze.

Relationale Referenzdatenbank

Das folgende Entity Relationship Diagramm zeigt die Struktur der im Beitrag verwendeten Referenzdatenbank.

Die gezeigte Datenbank könnte einer Kalenderanwendung zugrundeliegen, in der ein Nutzer Termine in seinem Kalender verwalten und diese dann mit Erinnerungen versehen kann. Die Tabellen verbindet jeweils eine 1:N Relation. Die Referenzdatenbank kannst du über das DDL-Skript db_server_conf.sql erzeugen.

Einstieg

Sofern du Docker auf deinem Rechner installiert hast, kannst du die Referenzdatenbank auf einem MySQL Server innerhalb eines Docker Containers ausführen. Starte dafür einfach den Docker Engine und führe anschließend das Shell-Skript run_mysql_server.sh aus.

Über den folgenden Connection String kannst du deine Anwendungen dann mit der Datenbank verbinden:

Server=localhost; Port=4200; Username=root; Password=pasSworD; Database=db_scaffold_efcore;

Wenn du auf deinem Rechner einen MySQL Datenbankserver installiert hast, dann kannst du auch diesen verwenden. Stelle dann aber eine entsprechende Konfiguration sicher.

Projekt für Codegenerierung erstellen und einrichten

Zu Beginn musst du EF Core einmalig auf deinem Rechner installieren.

#!/bin/sh

dotnet tool install --global dotnet-ef

Für die Referenzdatenbank kann EF Core jetzt den entsprechenden Code generieren. Dafür erstellst du vorab ein classlib Projekt und fügst anschließend die notwendigen Pakete hinzu.

#!/bin/sh

LIB="Scaffold.Library"

# Create project.
dotnet new classlib -n "$LIB"

# Add required packages.
cd "$LIB"
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Relational
dotnet add package Pomelo.EntityFrameworkCore.MySql

Die Pakete geben dir Zugriff auf die EF Core Basis- und Design-Time Komponenten. Weiterhin werden Komponenten für die Einbindung relationaler Datenbanken, sowie Pomelo’s MySQL Datenbank-Provider für EF Core verfügbar.

Shell-Skript für Codegenerierung erstellen

Im nächsten Schritt erstellst du ein Shell-Skript, welches du für die automatische Codegenerierung der Entity-Klassen und der DbContext-Klasse verwenden kannst.

Der folgende Codeblock zeigt dir beispielhaft, welche Möglichkeiten zur Konfiguration des Codegenerators zur Verfügung stehen und wie dieser gestartet wird. Das Projekt kann nach der Codegenerierung direkt kompiliert werden.

#!/bin/sh

# File: ef_dbcontext_scaffold.sh

# Configure code generator (required).
CONNECTION="Server=localhost; Port=4200; Username=root; Password=pasSworD; Database=db_scaffold_efcore;"
PROVIDER="Pomelo.EntityFrameworkCore.MySql"

# Configure code generator (optional).
CONTEXT="DatabaseContext"
CONTEXT_DIR="./Data/Generated"
CONTEXT_NAMESPACE="Scaffold.Library.Data"
OUTPUT_DIR="./Models/Generated"
NAMESPACE="Scaffold.Library.Models"

# Start code generation.
dotnet ef dbcontext scaffold "$CONNECTION" "$PROVIDER" \
  --context "$CONTEXT" \
  --context-dir "$CONTEXT_DIR" \
  --context-namespace "$CONTEXT_NAMESPACE" \
  --output-dir "$OUTPUT_DIR" \
  --namespace "$NAMESPACE" \
  --force

# Build project.
dotnet build

Die Angabe der Datenbankverbindung $CONNECTION und des Datenbank-Providers $PROVIDER sind obligatorisch. Alle weiteren Angaben sind optional und entsprechend deinen Anforderungen frei konfigurierbar.

Die folgende Tabelle beschreibt die gezeigten Optionen. Weitere Optionen sind auf der Webseite Microsoft Docs dokumentiert.

OptionBeschreibung
--contextName der DbContext-Klasse.
--context-dirVerzeichnis der DbContext-Klasse (relativ zum Projektverzeichnis).
--context-namespaceNamespace der DbContext-Klasse.
--output-dirVerzeichnis der Entity-Klassen (relativ zum Projektverzeichnis).
--namespaceNamespace der Entity-Klassen.
--forceErzwingt das Überschreiben vorhandener Dateien.

Analyse des generierten Codes

Im Anschluss an die Codegenerierung analysieren wir beispielhaft die Entity-Klasse Meeting, siehe dazu den folgenden Codeblock.

// Generated file: Meeting.cs

using System;
using System.Collections.Generic;

namespace Scaffold.Library.Models
{
    public partial class Meeting
    {
        public Meeting()
        {
            Reminders = new HashSet<Reminder>();
        }

        public uint Id { get; set; }
        public string Title { get; set; } = null!;
        public string? Description { get; set; }
        public DateTime? CreatedAt { get; set; }
        public DateTime? ChangedAt { get; set; }
        public DateTime StartAt { get; set; }
        public uint Duration { get; set; }
        public uint CalendarId { get; set; }

        public virtual Calendar Calendar { get; set; } = null!;
        public virtual ICollection<Reminder> Reminders { get; set; }
    }
}

Der Namespace stimmt mit der Konfiguration überein. Die Entity-Klasse Meeting ist partial und besitzt einen public Konstruktor, der das Navigation-Property Reminders initialisiert. Weiterhin verfügt die Entity-Klasse über auto-implemented public Property-Accessors. Optionale oder durch die Datenbank automatisch gesetzte Felder entsprechen nullable Properties, siehe z.B. Description oder CreatedAt. Die Navigation-Properties Calendar und Reminders sind virtual. Außerdem wandelt der Codegenerator die SQL konforme Snake-Case Notation changed_at in .NET konforme Pascal-Case Notation ChangedAt um.

Den kompletten generierten Code findest du auch im GitHub Repository.

Flexible Erweiterung des generierten Codes

Durch die partial Modifizierer sind alle generierten Klassen flexibel durch eigenen Code erweiterbar. So kannst du die Entity-Klasse Meeting leicht um eigene Methoden erweitern und diese in einer separaten Datei implementieren. Bei einer erneuten Codegenerierung gehen eigene Erweiterungen somit nicht verloren.

Der folgende Codeblock zeigt eine Fabrikmethode Create(), die neue Instanzen erstellen kann. Die Methode IsStartInFuture() kann bestimmen, ob der Startzeitpunkt StartAt eines Meeting in der Zukunft liegt.

// File: Meeting.cs

using System;

namespace Scaffold.Library.Models;

public partial class Meeting
{
    public static Meeting Create(
        string title,
        string? description,
        uint duration,
        DateTime startAt,
        uint calendarId)
    {
        return new Meeting
        {
            Title = title,
            Description = description,
            Duration = duration,
            StartAt = startAt,
            CalendarId = calendarId
        };
    }

    public bool IsStartInFuture()
    {
        return StartAt > DateTime.UtcNow;
    }
}

Solltest du die Entity-Klasse Meeting als Eltern-Klasse verwenden wollen, dann kann eine von ihr abgeleitete Kind-Klasse das Verhalten der auto-implemented virtual Navigation-Properties Calendar und Reminders überschreiben. Ein Beispiel dafür findest du auf der Webseite Microsoft Docs.

Anwendungsbeispiel (Proof of Concept)

In einer kleinen Beispielanwendung zeigst du, dass der Code aus dem oben erstellen classlib Projekt leicht in eine ausführbare Anwendung integriert werden kann.

Dafür erstellst du ein console Projekt und fügst anschließend die notwendigen Referenzen und Pakete hinzu.

#!/bin/sh

LIB="Scaffold.Library"
APP="Scaffold.App"

# Create project.
dotnet new console -n "$APP"

# Add required reference.
dotnet add "$APP"/"$APP".csproj reference "$LIB"/"$LIB".csproj

# Add required packages.
cd "$APP"
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Relational

Die Konsolenanwendung in Program.cs könnte die folgende Methode StartMinimal() ausführen. In der Methode werden der Datenbank über den verwendeten DatabaseContext Datensätze für die Entitäten Calendar, Meeting und Reminder hinzugefügt. Hierbei werden ebenfalls die von uns implementierten Fabrikmethoden Create(), sowie die Methode IsStartInFuture() verwendet.

// File: Demo.cs

using System;
using System.Linq;
using Scaffold.Library.Data;
using Scaffold.Library.Models;

namespace Scaffold.App;

public static class Demo
{
    public static void StartMinimal()
    {
        /*
         * Add database context into scope.
         */
        using var context = new DatabaseContext();

        /*
         * Add calendar.
         */
        var calendar = Calendar.Create(
            owner: "Arthur Dent"
        );
        context.Calendars.Add(calendar);
        context.SaveChanges();

        /*
         * Add meeting to calendar.
         */
        var meeting = Meeting.Create(
            title: "Have lunch with Zaphod Beeblebrox",
            description: "Ford's semi-half-cousin likes tea",
            duration: 42,
            startAt: DateTime.UtcNow.AddDays(10),
            calendarId: calendar.Id
        );
        context.Meetings.Add(meeting);
        context.SaveChanges();

        /*
         * Check meeting start date.
         */
        Console.WriteLine($"Does meeting '{meeting.Title}' start in future? " +
                          $"{meeting.IsStartInFuture()}.");

        /*
         * Add reminders to meeting.
         */
        context.Reminders.AddRange(
            Reminder.Create(
                remindBefore: 4,
                meetingId: meeting.Id),
            Reminder.Create(
                remindBefore: 2,
                meetingId: meeting.Id)
        );
        context.SaveChanges();
    }
}

Wenn du das Anwendungsbeispiel mit Hilfe von Docker und run_mysql_server.sh ausführst, dann kannst du in deinem Browser über die URL http://localhost:4300/ den Datenbank Adminer öffnen.

Die hier gezeigte und eine weitere Beispielanwendung findest du auch im GitHub Repository.

Fazit

Das besprochene Beispiel zeigt dir die ersten Schritte auf dem Weg, ein bestehendes relationales Datenbanksystem in deine .NET Softwareanwendung zu integrieren. Der Hauptaufwand liegt in der Einrichtung einer geeigneten Projektstruktur und der Implementierung einiger Shell-Skripte zur automatischen Codegenerierung mit Hilfe der EF Core CLI.

Weitere Schritte werden sicherlich nötig sein, aber der Aufwand wird sich lohnen. Nach erfolgreicher Integration stehen dir mächtige EF Core Funktionalitäten, wie z.B. LINQ-Abfragen und Änderungsverfolgung deiner Datensätze, zur Verfügung.

Den Code zum Blog findest du auch auf GitHub.

Happy Coding!

Die Beitragsbilder wurden zur Verfügung gestellt von Vecteezy.com.