Werden Daten in einer Datenbank gespeichert, dann besteht häufig die Anforderung die jeweiligen Datensätze mit Zeitstempeln zu versehen. Oft sind die Zeitpunkte der Anlage und Änderung eines Datensatzes von Interesse. Bequem ist es für dich als Entwickler, wenn diese Arbeit automatisch verrichtet wird.
Dieser Blogbeitrag zeigt dir, wie Entity Framework Core mit Hilfe des Change Trackers und einigen einfachen Erweiterungen dieser Anforderung automatisch nachkommt. Microsofts Entity Framework Core (im Folgenden EF Core) ist das aktuell populärste ORM Framework für .NET.
Vorteile
Folgende Vorteile bieten sich dir:
- Deine Datensätze werden auf Änderungen überwacht und automatisch mit Zeitstempeln versehen.
- Dein Business-Code wird vereinfacht, denn er ist frei von Logik zum Setzen der Zeitstempel.
- Du verwendest eine einheitliche und universell einsetzbare Vorgehensweise für das Setzen der Zeitstempel.
- Dein Testaufwand sinkt, denn nur die zuständige EF Core Erweiterung muss einmalig getestet werden.
Referenzimplementierung
Das relationale Datenbanksystem MySQL bieten für das Setzen automatischer Zeitstempel bei der Anlage und Änderung eines Datensatzes beispielsweise den folgenden Lösungsansatz. Siehe hier die Attribute created_at
und changed_at
unter Verwendung von current_timestamp
.
-- create and use database drop database if exists db_efcore_reference; create database db_efcore_reference character set utf8mb4 collate utf8mb4_unicode_ci; use db_efcore_reference; -- create table drop table if exists notes; create table notes ( id int unsigned not null auto_increment, message varchar(256) not null, created_at timestamp default current_timestamp, changed_at timestamp null on update current_timestamp, constraint pk_notes primary key (id) ) engine = innodb; -- insert table data insert into notes (message) values ('Note A'); insert into notes (message) values ('Note B'); -- update table data update notes set message = 'Note A - modified' where id = 1;
Das hier gezeigte MySQL Skript soll als Referenzimplementierung für die folgende Erweiterung deiner EF Core Anwendung dienen.
Einstieg
Im folgenden Anwendungsbeispiel implementierst du eine Datenbank zur Verwaltung von Notizen, siehe MySQL Referenzimplementierung. Dafür erzeugst du eine Tabelle Notes
, welche Notizen über das Property Message
speichert. Über die Properties CreatedAt
und ChangedAt
speichert die Tabelle zudem die Zeitpunkte der Anlage und Änderung einer Notiz. Diese sollen von EF Core automatisch gesetzt werden. Um die Datenbank zu erstellen, verwendest du im Folgenden den Code-First Approach.
Schritt eins – Interface ICurrentTimestamps erstellen
Zu Beginn erstellst du das Interface ICurrentTimestamps
mit den Properties CreatedAt
und ChangedAt
. So erzeugst du die Standard-Zeitstempel, welche EF Core nach der Erweiterung kennt und bearbeitet.
namespace Core.Database.Abstractions; public interface ICurrentTimestamps { DateTime CreatedAt { get; set; } DateTime? ChangedAt { get; set; } }
Der Änderungszeitpunkt ChangedAt
ist nullable, da er zur Anlage eines Datensatzes unbekannt ist.
Schritt zwei – Klasse Note erstellen
Als Nächstes erstellst du die Klasse Note
, welche EF Core als Entität für die Tabelle Notes
verwenden wird. Implementiere das Interface ICurrentTimestamps
, damit die Standard-Zeitstempel verfügbar werden.
namespace Core.Models; public class Note : ICurrentTimestamps { public Guid Id { get; } public string Message { get; set; } public DateTime CreatedAt { get; set; } public DateTime? ChangedAt { get; set; } private Note(Guid id, string message) { Id = id; Message = message; } public static Note Create(string message) { return new Note(Guid.NewGuid(), message); } }
Das Property Id
dient als Primärschlüssel und wird zur Instanziierung über die Fabrikmethode Create()
automatisch erzeugt, vgl. auto_increment
in der MySQL Referenzimplementierung. Das Property Message
muss über die Fabrikmethode Create()
initialisiert werden und kann später direkt gesetzt werden.
Schritt drei – Klasse DatabaseContext erstellen und Methode OnModelCreating() überschreiben
Erstelle nun die Klasse DatabaseContext
und leite von der EF Core Basisklasse DbContext
ab. Um über den Code-First Approach eine Datenbank erzeugen zu können, überschreibst du die Methode OnModelCreating()
.
namespace Core.Database; public class DatabaseContext : DbContext { #nullable disable public DbSet<Note> Notes { get; set; } #nullable enable public DatabaseContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Note>(entity => { entity.ToTable("Notes"); entity.HasKey(note => note.Id); entity.Property(note => note.Message).HasMaxLength(256); }); base.OnModelCreating(modelBuilder); } }
Mit DbSet
wird die Entität Note
dem zugehörigen DbContext
bekannt gemacht. EF Core kann jetzt über das Property Notes
Lese- und Schreibzugriffe in der Datenbank ausführen.
Über den Methodenaufruf ToTable()
erzeugst du die Tabelle Notes
. Die Methode HasKey()
definiert Id
als Primärschlüssel der Tabelle. Die Methode HasMaxLength()
limitiert die Länge der Notiz Message
auf maximal 256 Zeichen. Alle übrigen Einstellungen ergeben sich implizit aus der Implementierung der Entität Note
.
Schritt vier – Initiale Migration erstellen (Code-First Approach)
Bevor jetzt die initiale Migration zur Erzeugung der Datenbank erstellt werden kann, muss die Klasse DatabaseContext
noch als Dienst registriert und MySQL Server als Datenbanksystem konfiguriert werden. In der Datei appsettings.json
kannst du den ConnectionString
für die Verbindung zum Datenbankserver hinterlegen.
var connectionString = configuration.GetConnectionString("Default"); services.AddDbContext<DatabaseContext>(opt => opt.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)) );
Mit Hilfe der Shell Skripte efcore_migration_add.sh
und efcore_migration_remove.sh
kannst du jetzt die zugehörigen Migrationen erstellen und entfernen.
Schritt fünf – Klasse DatabaseContext erweitern und SaveChangesAsync() überschreiben
Implementiere jetzt die Kernfunktion der EF Core Erweiterung. Überschreibe in der zuvor erstellten Klasse DatabaseContext
die Methode SaveChangesAsync()
, welche alle im DbContext
vorgenommenen Änderungen in der zugrunde liegenden Datenbank speichert.
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) { var dateTime = DateTime.UtcNow; var entriesAdded = ChangeTracker.Entries() .Where(entry => entry.State == EntityState.Added) .ToList(); entriesAdded.ForEach(entry => entry.Property(nameof(ICurrentTimestamps.CreatedAt)).CurrentValue = dateTime); var entriesModified = ChangeTracker.Entries() .Where(entry => entry.State == EntityState.Modified) .ToList(); entriesModified.ForEach(entry => entry.Property(nameof(ICurrentTimestamps.ChangedAt)).CurrentValue = dateTime); return base.SaveChangesAsync(cancellationToken); }
Über die LINQ Filter Where()
auf ChangeTracker.Entries()
ermittelst du alle neu hinzugefügten und geänderten Datensätze im zugehörigen DbContext
. Über die ForEach()
Aufrufe setzt du dann die Standard-Zeitstempel, die du über das Interface ICurrentTimestamps
in beliebige Entitäten von DatabaseContext
integrieren kannst. Zuletzt rufst du die Methode SaveChangesAsync()
der Basisklasse DbContext
auf und speicherst die Änderungen in der Datenbank.
Schritt sechs – Datensätze einfügen und modifizieren
Die folgende Methode InsertNoteAsync()
zeigt dir, wie du einen neuen Datensatz note
in den DatabaseContext
einfügst. Mit Aufruf von SaveChangesAsync()
wird der Zeitstempel CreatedAt
automatisch gesetzt.
public async Task InsertNoteAsync( string message, DatabaseContext context, CancellationToken cancellationToken = default) { var note = Note.Create(message); await context.Notes.AddAsync(note, cancellationToken); await context.SaveChangesAsync(cancellationToken); }
Die folgende Methode UpdateNoteAsync()
zeigt dir, wie du einen bestehenden Datensatz modifizierst. Hier wird mit dem Aufruf von SaveChangesAsync()
der Zeitstempel ChangedAt
automatisch gesetzt.
public async Task UpdateNoteAsync( Guid noteId, string message, DatabaseContext context, CancellationToken cancellationToken = default) { var note = await context.Notes.FirstOrDefaultAsync(n => n.Id == noteId, cancellationToken); if (note is null) { return; } note.Message = message; await context.SaveChangesAsync(cancellationToken); }
Schritt sieben – Datenbank Updater erstellen
Abschließend implementierst du noch die Klasse DatabaseUpdater
, welche Sample-Daten in den DbContext
einfügt und modifiziert. So kannst du die Funktionsweise der EF Core Erweiterung in der Methode SaveChangesAsync()
unmittelbar validieren. Weiterhin führt der DatabaseUpdater
mit Start der Anwendung automatisch die Migration zur Erstellung der Datenbank aus.
Integrationstest
Das besprochene Anwendungsbeispiel kannst du über eine Web-API testen. Sofern du auf deinem Rechner Docker installiert hast, musst du hierzu keinen lokalen MySQL Server installieren und einrichten. Starte einfach den Docker Engine und führe die Shell Skripte run_mysql_server.sh
und run_efcore_webapi.sh
nacheinander aus. Achte darauf, dass der Docker Container mit dem MySQL Server vollständig gestartet ist, bevor du anschließend die Web-API startest.
Die Web-API verfügt über ein Swagger UI, welches du im Browser über die URL https://localhost:5001/swagger/
öffnest. Die entsprechenden curl
Befehle für den Test über ein Terminal findest du hier.
Der folgende HTTP POST Request fügt einen neuen Datensatz in die Datenbank ein.
#!/bin/sh # Request curl -X 'POST' \ 'https://localhost:5001/api/notes' \ -H 'accept: */*' \ -H 'Content-Type: application/json' \ -d '{ "message": "Arthur Dent" }' # Response body # { # "id":"86e4871d-c39c-481f-ba55-6cc095e3f5ec", # "message":"Arthur Dent", # "createdAt":"2021-12-27T16:35:00.143135Z", # "changedAt":null # }
Das Property CreatedAt
wurde mit Erstellung des neuen Datensatzes durch die EF Core Erweiterung automatisch gesetzt und zeigt den Zeitpunkt der Erstellung an. Das Property ChangedAt
ist null
(also unbearbeitet), da der Datensatz bisher nur erstellt und noch nicht modifiziert wurde.
Der folgende HTTP PUT Request modifiziert den zuvor erstellten Datensatz in der Datenbank.
#!/bin/sh # Request curl -X 'PUT' \ 'https://localhost:5001/api/notes' \ -H 'accept: */*' \ -H 'Content-Type: application/json' \ -d '{ "id": "86e4871d-c39c-481f-ba55-6cc095e3f5ec", "message": "The answer to the ultimate question of life, the universe, and everything is 42." }' # Response body # { # "id":"86e4871d-c39c-481f-ba55-6cc095e3f5ec", # "message":"The answer to the ultimate question of life, the universe, and everything is 42.", # "createdAt":"2021-12-27T16:35:00.143135Z", # "changedAt":"2021-12-27T16:52:19.940327Z" # }
Das Property CreatedAt
bleibt unverändert. Das Property ChangedAt
ist nun durch die EF Core Erweiterung automatisch gesetzt worden und zeigt den Zeitpunkt der letzten Modifikation an. Durch eine erneute Modifikation des Datensatzes wird ChangedAt
ebenfalls erneut gesetzt.
Fazit
Das besprochene Beispiel zeigt dir einen möglichen Lösungsansatz, wie du das Setzen von Zeitstempeln in deiner EF Core Anwendung automatisieren kannst. Der Hauptaufwand liegt in der Implementierung des Interfaces ICurrentTimestamps
in den gewünschten Entitäten und der Überschreibung der Methode SaveChangesAsync()
aus der EF Core Basisklasse DbContext
.
Durch die Verwendung der Klasse ChangeTracker
lassen sich ggf. weitere nützliche EF Core Erweiterungen implementieren, welche deine Anwendung weiter automatisieren. Beispielsweise könnten Datenbankeinträge gezielt durch den ChangeTracker
überwacht werden und bei deren Modifikation ein gewünschtes Event erzeugt werden.
Den Code zum Blog findest du auch auf GitHub.
Happy Coding!