Freigeben über



Juni 2019

Band 34, Nummer 6

[Muster und Vorgehensweisen]

Super-DRY-Entwicklung für ASP.NET Core

Von Thomas Hansen

DRY ist in Bezug auf Softwarearchitektur ein wirklich wichtiges Akronym. Es bedeutet „Don't Repeat Yourself“ (wiederhole Dich nicht) und ist ein wichtiges Prinzip für jeden Entwickler, der ein Legacyquellcodeprojekt betreut. Dies bedeutet: Wenn Sie sich im Code wiederholen, werden Sie feststellen, dass Sie bei jedem Bugfix und Featureupdate Ihre Änderungen wiederholen müssen.

Codewiederholungen beeinträchtigen die Verwaltbarkeit Ihres Projekts und erschweren die Anwendung von Änderungen. Und je mehr Wiederholungen vorhanden sind, desto mehr Spaghetticode erhalten Sie. Wenn Sie andererseits Wiederholungen vermeiden, kann dies zu einem Projekt führen, dessen Verwaltung und Problembehandlung wesentlich einfacher ist, und Sie werden bei Ihrer Arbeit zufriedener und produktiver sein. Kurz gesagt: Die Einhaltung der DRY-Prinzipien kann Ihnen helfen, ausgezeichneten Code zu erstellen.

Sobald Sie anfangen, auf DRY-Art zu denken, können Sie mit diesem wichtigen architektonischen Ansatz eine neue Ebene erreichen, auf der es sich so anfühlt, als würde sich Ihr Projekt auf magische Weise vom Boden erheben – buchstäblich, ohne Mühe auf das Erstellen von Funktionalität verwenden zu müssen. Für Uneingeweihte mag es so aussehen, als ob Code aus dem Nichts durch die Mechanismen von „Super-DRY“ entsteht. Ausgezeichneter Code ist fast immer minimalistisch, aber brillanter Code ist noch minimalistischer.

In diesem Artikel werde ich Ihnen die Magie der Super-DRY-Entwicklung und einige der Tricks vorstellen, die ich im Lauf der Jahre verwendet habe. Sie können Ihnen helfen, Ihre ASP.NET Core-Web-APIs mit viel weniger Aufwand zu erstellen. Alles in diesem Artikel basiert auf verallgemeinerten Lösungen und dem Konzept von DRY-Code und verwendet nur bewährte Methoden aus unserer Branche. Doch zunächst einige Hintergrundinformationen.

CRUD, HTTP-REST und SQL

Erstellen, Lesen, Aktualisieren und Löschen (CRUD, Create, Read, Update und Delete) ist das grundlegende Verhalten der meisten Datenmodelle. In den meisten Fällen benötigen Ihre Datenentitätstypen diese vier Vorgänge, und tatsächlich sind sowohl HTTP als auch SQL um sie herum aufgebaut. HTTP POST dient zum Erstellen von Elementen, HTTP GET zum Lesen von Elementen, HTTP PUT zum Aktualisieren von Elementen und HTTP DELETE zum Löschen von Elementen. SQL dreht sich ebenfalls um CRUD mit Insert, Select, Update und Delete. Sobald Sie ein wenig darüber nachdenken, ist es ziemlich offensichtlich, dass es im Grunde genommen immer um CRUD geht, vorausgesetzt, Sie wollen nicht „bis an die Basis“ vordringen und eine CQRS-Architektur implementieren.

Sie verfügen also über die notwendigen Sprachmechanismen, um HTTP-Verben so zu verwenden, dass sie von der HTTP-Schicht des Clients über Ihren C#-Code bis hin in Ihre relationale Datenbank weitergegeben werden. Nun benötigen Sie nur noch eine generische Möglichkeit, diese Konzepte durch Ihre verschiedenen Schichten zu implementieren. Sie möchten dies natürlich erreichen, ohne sich zu wiederholen, und ein brillantes architektonisches Fundament verwenden. Fangen wir also an.

Laden Sie zunächst den Code von github.com/polterguy/magic/releases herunter. Entzippen Sie die Datei, und öffnen Sie „magic.sln“ in Visual Studio. Starten Sie Ihren Debugger, und beachten Sie, dass Sie bereits über fünf HTTP-REST-Endpunkte in der Swagger-Benutzeroberfläche verfügen. Woher stammen diese HTTP-Endpunkte? Nun, schauen wir uns den Code an, denn die Antwort auf diese Frage könnte Sie überraschen.

Sieh an, kein Code!

Das erste, was Sie beim Durchsuchen des Codes bemerken werden: Das ASP.NET Core-Web-Projekt selbst ist praktisch leer. Dies ist aufgrund einer ASP.NET Core-Funktion möglich, die es erlaubt, Controller dynamisch einzubinden. Wenn Sie die Interna einsehen möchten, die sich dahinter verbergen, können Sie die Datei „Startup.cs“ untersuchen. Im Grunde genommen wird jeder Controller aus allen Assemblys in Ihrem Ordner der AppDomain dynamisch hinzugefügt. Dieses einfache Prinzip ermöglicht es Ihnen, Ihre Controller wiederzuverwenden und beim Entwurf Ihrer Lösungen modular zu denken. Die Möglichkeit, Controller projektübergreifend wiederzuverwenden, ist Schritt 1 auf dem Weg zum Super-DRY-Experten.

Öffnen Sie das web/controller/magic.todo.web.controller-Projekt, und sehen Sie sich die Datei „TodoController.cs“ an. Sie werden feststellen, dass sie leer ist. Woher also kommen diese fünf HTTP-REST-Endpunkte? Die Antwort lautet: durch den Mechanismus der objektorientierten Programmierung (OOP) und C#-Generika. Die TodoController-Klasse erbt von CrudController und übergibt ihr ViewModel und ihr Datenbankmodell. Darüber hinaus verwendet sie Dependency Injection, um eine Instanz von ITodoService zu erstellen, die an die CrudController-Basisklasse übergeben wird.

Da die ITodoService-Schnittstelle von ICrudService mit der richtigen generischen Klasse erbt, akzeptiert die CrudController-Basisklasse Ihre Dienstinstanz problemlos. Darüber hinaus kann sie den Dienst an dieser Stelle bereits polymorphistisch nutzen, als wäre es ein einfacher ICrudService, der natürlich eine generische Schnittstelle mit parametrisierten Typen ist. Dies ermöglicht den Zugriff auf fünf generisch definierte Dienstmethoden im CrudController. Um die Auswirkungen zu verstehen, sollten Sie sich darüber im Klaren sein, dass Sie mit dem folgenden einfachen Code buchstäblich alle CRUD-Vorgänge erstellt haben, die Sie jemals benötigen werden, und dass Sie sie von der HTTP-REST-Schicht über die Dienstschicht in die Domänenklassenhierarchie und bis in die relationale Datenbankschicht weitergegeben haben. Hier ist der gesamte Code für den Controllerendpunkt:

[Route("api/todo")]
public class TodoController : CrudController<www.Todo, db.Todo>
{
  public TodoController(ITodoService service)
    : base(service)
  { }
}

Dieser Code stellt Ihnen fünf HTTP-REST-Endpunkte zur Verfügung, mit denen Sie Datenbankelemente erstellen, lesen, aktualisieren, löschen und zählen können – fast wie durch Zauberei. Und Ihr gesamter Code wurde „deklariert“ und enthielt keine einzige Codezeile zur Funktionalität. Natürlich stimmt es, dass Code sich nicht selbst generiert, und ein Großteil der Arbeiten wird hinter den Kulissen erledigt, aber dieser Code ist als „Super-DRY“ anzusehen. Es stellt einen echten Vorteil dar, mit einer höheren Abstraktionsebene zu arbeiten. Eine gute Analogie wäre die Beziehung zwischen einer if-then-Anweisung in C# und dem zugrunde liegenden Assemblysprachcode. Der von mir skizzierte Ansatz ist einfach eine höhere Abstraktionsebene als die Hardcodierung Ihres Controllercodes.

In diesem Fall ist der www.Code-Codetyp Ihr ViewModel, der db.Todo-Typ Ihr Datenbankmodell und ITodoService Ihre Dienstimplementierung. Indem Sie der Basisklasse einfach einen Hinweis geben, welchen Typ Sie persistent speichern möchten, haben Sie Ihre Aufgabe bereits erledigt. Die Dienstschicht ist ebenfalls leer. Den gesamten Code sehen Sie hier:

public class TodoService : CrudService<Todo>, ITodoService
{
  public TodoService([Named("default")] ISession session)
    : base(session, LogManager.GetLogger(typeof(TodoService)))
  { }
}

Null Methoden, null Eigenschaften, null Felder, und doch gibt es immer noch eine vollständige Dienstschicht für Ihre TODO-Elemente. Tatsächlich ist sogar die Dienstschnittstelle leer. Das folgende Beispiel zeigt den gesamten Code für die Dienstschnittstelle:

public interface ITodoService : ICrudService<Todo>
{ }

Auch hier wieder: alles leer! Und trotzdem – Simasalabim und Abrakadabra: Sie haben eine vollständige TODO-HTTP-REST-Web-API-Anwendung. Wenn Sie das Datenbankmodell öffnen, sehen Sie Folgendes:

public class Todo : Model
{
  public virtual string Header { get; set; }
  public virtual string Description { get; set; }
  public virtual bool Done { get; set; }
}

Auch hier ist nichts vorhanden – nur einige virtuelle Eigenschaften und eine Basisklasse. Und doch sind Sie in der Lage, den Typ in Ihrer Datenbank persistent zu speichern. Die eigentliche Zuordnung zwischen Ihrer Datenbank und Ihrem Domänentyp erfolgt in der TodoMap.cs-Klasse innerhalb des magic.todo.model-Projekts. Hier sehen Sie die gesamte Klasse:

public class TodoMap : ClassMap<Todo>
{
  public TodoMap()
  {
    Table("todos");
    Id(x => x.Id);
    Map(x => x.Header).Not.Nullable().Length(256);
    Map(x => x.Description).Not.Nullable().Length(4096);
    Map(x => x.Done).Not.Nullable();
  }
}

Dieser Code weist die ORM-Bibliothek an, die todos-Tabelle mit der Id-Eigenschaft als Primärschlüssel zu verwenden, und legt einige zusätzliche Eigenschaften für den Rest der Spalten/Eigenschaften fest. Beachten Sie, dass Sie zu Beginn dieses Projekts nicht einmal über eine Datenbank verfügt haben. Dies liegt daran, dass NHibernate Ihre Datenbanktabellen automatisch erstellt, wenn sie noch nicht vorhanden sind. Und da Magic standardmäßig SQLite verwendet, ist nicht einmal eine Verbindungszeichenfolge erforderlich. Es wird automatisch eine dateibasierte SQLite-Datenbank in einem relativen Dateipfad erstellt, es sei denn, Sie überschreiben die Verbindungseinstellungen in „appsettings.config“, um MySQL oder MSSQL zu verwenden.

Ob Sie es glauben oder nicht: Ihre Lösung unterstützt bereits transparent fast alle relationalen Datenbanken, die Sie sich vorstellen können. Tatsächlich befindet sich die einzige Zeile Code, die Sie hinzufügen müssten, damit dies funktioniert, im magic.todo.services-Projekt in der ConfigureNinject-Klasse, die einfach eine Bindung zwischen der Dienstschnittstelle und der Dienstimplementierung herstellt. Also haben Sie eine Zeile Code hinzugefügt und dadurch eine vollständige Anwendung erhalten. Die folgende Zeile ist der einzige tatsächliche „Code“, der zum Erstellen der TODO-Anwendung verwendet wird:

public class ConfigureNinject : IConfigureNinject
{
  public void Configure(IKernel kernel, Configuration configuration)
  {
    // Warning, this is a line of C# code!
    kernel.Bind<ITodoService>().To<TodoService>();
  }
}

Wir sind durch den intelligenten Einsatz von OOP, Generika und den Prinzipien von DRY zu Super-DRY-Magiern geworden. Die Frage lautet also: Wie können Sie diesen Ansatz verwenden, um Ihren Code zu verbessern?

Die Antwort: Beginnen Sie mit Ihrem Datenbankmodell, und erstellen Sie Ihre eigene Modellklasse, was entweder durch Hinzufügen eines neuen Projekts in Ihrem Modellordner oder durch Hinzufügen einer neuen Klasse zum vorhandenen magic.todo.model-Projekt geschehen kann. Erstellen Sie dann die Dienstschnittstelle im Ordner „contracts“. Implementieren Sie nun Ihren Dienst in Ihrem Ordner „services“, und erstellen Sie Ihr ViewModel und Ihren Controller. Stellen Sie sicher, dass Sie eine Bindung zwischen der Dienstschnittstelle und Ihrer Dienstimplementierung herstellen. Wenn Sie dann neue Projekte erstellen, müssen Sie sicherstellen, dass ASP.NET Core Ihre Assembly lädt, indem Sie einen Verweis darauf in Ihrem magic.backend-Projekt hinzufügen. Beachten Sie, dass nur auf das Dienstprojekt und den Controller durch Ihr Back-End verwiesen werden muss.

Wenn Sie sich für die Verwendung der vorhandenen Projekte entscheiden, ist nicht einmal der letzte Teil erforderlich. Eine einfache tatsächliche Codezeile für die Bindung zwischen Ihrer Dienstimplementierung und Ihrer Dienstschnittstelle, und Sie haben eine vollständige ASP.NET Core-Web API-Lösung erstellt. Das ist eine außerordentlich mächtige Codezeile, wenn Sie mich fragen. Sie können in meinem Video „Super DRY Magic for ASP.NET Core“ unter youtu.be/M3uKdPAvS1I sehen, wie ich den gesamten Prozess für eine frühere Version des Codes durchlaufe.

Stellen Sie sich dann vor, was passiert, wenn Sie feststellen, dass Sie diesen Code für den Gerüstbau verwenden und automatisch gemäß Ihrem Datenbankschema generieren können. An diesem Punkt führt Ihr Computergerüstsoftware-System wahrscheinlich Ihre Programmierung durch und generiert dabei eine perfekt gültige DDD-Architektur (Domain-Driven Design).

Kein Code, keine Fehler, kein Problem

Die meisten Gerüstbauframeworks verwenden Verknüpfungen oder verhindern, dass Sie den sich daraus ergebenden Code erweitern und ändern können, sodass die Verwendung für reale Anwendungen unmöglich wird. Mit Magic gilt dieser Nachteil einfach nicht. Magic erstellt eine Dienstschicht für Sie und verwendet Dependency Injection, um eine Dienstschnittstelle in Ihren Controller einzubinden. Außerdem werden absolut gültige DDD-Muster für Sie generiert. Und wie beim erste Code kann jeder einzelne Teil Ihrer Lösung nach Bedarf erweitert und geändert werden. Ihr Projekt genügt perfekt allen Anforderungen der SOLID-Prinzipien.

Beispielsweise habe ich in einer meiner eigenen Lösungen einen Abrufthread eines POP3-Servers in meinem Dienst verwendet, der für einen EmailAccount-Domänenmodelltyp deklariert ist. Dieser POP3-Dienst speichert E-Mails von meinem POP3-Server in einer Datenbank, die in einem Hintergrundthread ausgeführt wird. Wenn eine E-Mail gelöscht wird, möchte ich sicherstellen, dass auch ihre Anlagen physisch im Speicher gelöscht werden, und wenn der Benutzer einen EmailAccount löscht, möchte ich natürlich die zugehörigen E-Mails ebenfalls löschen.

Der Code in Abbildung1 zeigt, wie ich das Löschen eines EmailAccount überschrieben habe, wodurch auch alle E-Mails und Anlagen gelöscht werden sollten. Für das Protokoll: Hibernate Query Language (HQL) wird für die Kommunikation mit der Datenbank verwendet. Dadurch wird sichergestellt, dass NHibernate automatisch die richtige SQL-Syntax erstellt, je nachdem, mit welcher Datenbank eine physische Verbindung besteht.

Abbildung 1: Überschreiben der Löschung eines EmailAccount

public sealed class EmailAccountService : CrudService<EmailAccount>,
  IEmailAccountService
{
  public EmailAccountService(ISession session)
    : base(session, LogManager.GetLogger(typeof(EmailAccountService)))
  { }
  public override void Delete(Guid id)
  {
    var attachments = Session.CreateQuery(
      "select Path from EmailAttachment where Email.EmailAccount.Id = :id");
    attachments.SetParameter("id", id);
    foreach (var idx in attachments.Enumerable<string>())
    {
      if (File.Exists(idx))
        File.Delete(idx);
    }
    var deleteEmails = Session.CreateQuery(
      "delete from Email where EmailAccount.Id = :id");
    deleteEmails.SetParameter("id", id);
    deleteEmails.ExecuteUpdate();
    base.Delete(id);
  }
}

Die mathematische Seite

Sobald Sie anfangen, über diese Konzepte zu philosophieren, kommt die Inspiration. Stellen Sie sich zum Beispiel ein Gerüstbauframework vor, das um Magic herum aufgebaut ist. Wenn Sie eine Datenbank mit 100 Tabellen mit jeweils durchschnittlich 10 Spalten verwenden, werden Sie aus mathematischer Sicht feststellen, dass sich die Kosten in Form von Gesamtzeilen von Code schnell summieren können. Um alle diese Tabellen in eine HTTP-REST-API einzubinden, sind beispielsweise sieben Codezeilen pro Dienstschnittstelle erforderlich, während 14 Codezeilen pro Tabelle für jeden Dienst und 19 Zeilen pro Tabelle für jeden Controller erforderlich sind. Abbildung 2 führt die beteiligten Elemente und die erforderlichen Codezeilen auf.

Abbildung 2: Addieren der Kosten in Code

Komponente Verträge Durchschnittliche Codezeilen Gesamtzahl Codezeilen
Dienstschnittstellen 100 7 700
Services 100 14 1.400
Controller 100 19 1.900
Dienstschnittstelle und -implementierung 100 1 100
ViewModels 100 17 1.700
Datenbankmodelle 100 17 1.700
Datenbankzuordnungen 100 20 2.000
Gesamtzahl Codezeilen: 9.500

 

Nachdem alles erledigt ist, erhalten Sie 9.500 Zeilen Code. Wenn Sie einen Metadienst erstellen, der in der Lage ist, ein vorhandenes Datenbankschema zu extrahieren, wird es offensichtlich, dass Sie diesen Code mit Gerüstbau generieren können – zweifellos ohne jegliche Codierung. Dennoch werden 9.500 Zeilen perfekt gestalteter Code generiert, der leicht erweiterbar ist und alle relevanten Entwurfsmuster und bewährten Methoden verwendet. Mit nur zwei Sekunden Gerüstbau hat Ihr Computer 80 Prozent Ihrer Aufgaben erledigt.

Jetzt müssen Sie nur noch die Ergebnisse des Gerüstbauprozesses durchgehen und Methoden für Ihre Dienste und Controller für die Domänentypen überschreiben, die aus irgendeinem Grund besondere Aufmerksamkeit erfordern. Ihre Web-API ist fertig. Da die Controllerendpunkte alle genau die gleiche Struktur aufweisen, ist die Duplizierung dieses Gerüstbauprozesses in der Clientschicht so einfach wie das Lesen der von Swagger generierten API-JSON-Deklarationsdateien. Dies ermöglicht Ihnen die Erstellung Ihrer Dienstschicht beispielsweise für Angular oder React. Und das alles, weil Ihr Code und Ihre Web-API vorhersehbare Strukturen besitzen, die auf Generalisierungsprinzipien und der Vermeidung von Wiederholungen basieren.

Um dies in Relation zu setzen: Sie haben es geschafft, ein HTTP-REST-Web-API-Projekt zu erstellen, das in seiner Komplexität wahrscheinlich doppelt so groß ist wie das Open-Source-CRM-Projekt Sugar, und Sie haben 80 Prozent der Aufgaben innerhalb von Sekunden erledigt. Sie haben eine Software Factory-Montagezeile ermöglicht, die auf der Standardisierung von Komponenten und der Wiederverwendung von Strukturen basiert, während der Code für alle Ihre Projekte viel einfacher zu lesen und zu verwalten ist. Dank der Art und Weise, wie Endpunkte und Dienste des Controllers dynamisch und ohne Abhängigkeiten in Ihre Web-API geladen werden, können selbst die Teile, die Änderungen und ein besonderes Verhalten erfordern, in Ihrem nächsten Projekt wiederverwendet werden.

Wenn Sie für ein Beratungsunternehmen arbeiten, starten Sie wahrscheinlich jedes Jahr mehrere neue Projekte mit ähnlichen Anforderungen, bei denen Sie die Gemeinsamkeiten für jedes neue Projekt lösen müssen. Mit Verständnis für die Anforderungen eines Kunden und einer anfänglichen Implementierung ermöglicht Ihnen ein Super-DRY-Ansatz, ein ganzes Projekt in Sekundenschnelle abzuschließen. Und natürlich kann die Zusammensetzung der Elemente in Ihren Projekten weiterverwendet werden, indem allgemeine Module wie Authentifizierung und Autorisierung identifiziert werden. Durch die Implementierung dieser Module in einem allgemeinen Web-API-Projekt können Sie sie auf jedes neue Projekt anwenden, das ähnliche Probleme aufweist wie die, die Sie bereits kennen.

Für das Protokoll: Das klingt einfach, aber Tatsache ist, dass es schwierig ist, Wiederholungen zu vermeiden. Dieser Ansatz erfordert die Bereitschaft, wieder und wieder Refactorings vorzunehmen. Und wenn das Refactoring abgeschlossen ist, müssen Sie noch mehr Refactoring ausführen. Aber der Vorteil, der sich daraus ergibt, sollte nicht ignoriert werden. Mit den DRY-Prinzipien können Sie fast wie von Zauberhand Code erstellen, indem Sie einfach mit dem Gerüstbau-Zauberstab wedeln und Module aus bereits vorhandenen Bestandteilen zusammenstellen.

Am Ende des Tages können die hier formulierten Prinzipien Ihnen helfen, vorhandene bewährte Methoden zu nutzen, um Ihre eigenen Web-APIs zu erstellen und dabei Wiederholungen zu vermeiden. Es gibt eine Menge Gutes, das dieser Ansatz bewirken kann, und hoffentlich hilft er Ihnen, die Großartigkeit von DRY schätzen zu lernen.


Thomas Hansenist ein Zen-Softwarezauberer. Er lebt zurzeit auf Zypern und jongliert dort mit Softwarecode für FinTech und Handelssysteme.

Unser Dank gilt dem folgenden technischen Experten bei Microsoft für die Durchsicht dieses Artikels: James McCaffrey


Diesen Artikel im MSDN Magazine-Forum diskutieren