September 2016

Band 31, Nummer 9

ASP.NET Core: Feature Slices für ASP.NET Core MVC

Von Steve Smith

Für große Web-Apps ist eine bessere Organisation als für kleine Apps erforderlich. Bei großen Apps beginnt die Standardorganisationsstruktur, die von ASP.NET MVC (und Core MVC) verwendet wird, gegen Sie zu arbeiten. Sie können zwei einfache Techniken zum Aktualisieren Ihres Organisationsansatzes verwenden und auf diese Weise eine wachsende Anwendung im Griff behalten.

Das MVC-Muster (Model-View-Controller) ist selbst im Microsoft ASP.NET-Umfeld ausgereift. Die erste Version von ASP.NET MVC wurde im Jahr 2009 ausgeliefert, die erste vollständige Neuauflage der Plattform (ASP.NET Core MVC) Anfang dieses Sommers. Während dieser ganzen Zeit, in der sich ASP.NET MVC weiterentwickelt hat, blieb die Standardprojektstruktur unverändert: Es wurden Ordner für Controller und Ansichten sowie häufig für Modelle (oder ggf. ViewModels) verwendet. Wenn Sie heute eine neue ASP.NET Core-App erstellen, sind diese durch die Standardvorlage erstellten Ordner vorhanden. Abbildung1 zeigt dies.

Standardvorlagenstruktur für ASP.NET Core-Web-Apps
Abbildung 1: Standardvorlagenstruktur für ASP.NET Core-Web-Apps

Diese Organisationsstruktur besitzt viele Vorteile. Sie sind mit ihr vertraut. Wenn Sie während der letzten Jahre an einem ASP.NET MVC-Projekt gearbeitet haben, erkennen Sie sie sofort wieder. Sie ist organisiert. Wenn Sie nach einem Controller oder einer Ansicht suchen, wissen Sie recht gut, wo Sie nachschauen müssen. Wenn Sie ein neues Projekt starten, funktioniert diese Organisationsstruktur zufriedenstellend, weil noch nicht viele Dateien vorhanden sind. Wenn das Projekt wächst gehen damit jedoch zunehmende Schwierigkeiten beim Ermitteln der gewünschten Controller- oder Ansichtendatei einher, da die Anzahl der Dateien und Ordner in diesen Hierarchien ansteigt.

Damit Sie verstehen, was ich meine, stellen Sie sich vor, Sie würden die Dateien Ihres Computers in der gleichen Struktur organisieren. Anstatt separate Ordner für verschiedene Projekte oder Aufgabentypen zu verwenden, wären Verzeichnisse vorhanden, die nur anhand des Dateityps organisiert wären. Dies könnten Ordner für Textdokumente, PDF-Dateien, Bilder und Tabellen sein. Wenn Sie unter diesen Umständen eine Aufgabe erledigen wollten, die mehrere Dokumenttypen umfasst, müssten Sie ständig zwischen den verschiedenen Ordnern hin- und herspringen und durch zahlreiche Dateien in jedem Ordner scrollen oder diese durchsuchen, obwohl sie gar keinen Bezug zur aktuellen Aufgabe besitzen. Dies entspricht genau der Arbeitsweise an Features in einer MVC-App, die standardmäßig organisiert ist.

Dies stellt ein Problem dar, weil Gruppen von Dateien, die nach dem Typ und nicht nach ihrem Zweck organisiert sind, der Zusammenhalt fehlt. „Zusammenhalt“ bezieht sich auf das Ausmaß, in dem Elemente eines Moduls zusammengehören. In einem typischen ASP.NET MVC-Projekt bezieht sich ein bestimmter Controller auf mindestens eine zugehörige Ansicht (in einem Ordner, der dem Namen des Controllers entspricht). Controller und Ansicht verweisen auf mindestens ein ViewModel, das sich auf die Verantwortlichkeit des Controllers bezieht. Normalerweise werden jedoch wenige ViewModel-Typen oder Ansichten von mehreren Controllertypen verwendet (außerdem wird üblicherweise das Domänenmodell oder Persistenzmodell in ein eigenes separates Projekt verschoben).

Ein Beispielprojekt

Angenommen, Sie verwenden ein einfaches Projekt, dessen Aufgabe darin besteht, vier lose in Beziehung stehende Anwendungskonzepte zu verwalten: Ninjas, Plants, Pirates und Zombies. Im Beispiel können Sie diese Konzepte nur auflisten, anzeigen und hinzufügen. Stellen Sie sich jedoch nun vor, dass eine größere Komplexität vorhanden ist, die weitere Ansichten mit sich bringt. Die Standardorganisationsstruktur für dieses Projekt ähnelt dann Abbildung 2.

Beispielprojekt mit Standardorganisation
Abbildung 2: Beispielprojekt mit Standardorganisation

Wenn Sie an einer neuen Funktion arbeiten möchten, für die „Pirates“ benötigt wird, müssen Sie zu „Controllers“ navigieren und nach „PiratesController“ suchen, dann aus „Views“ in „Pirates“ navigieren und die entsprechende Ansichtendatei ermitteln. Schon bei nur fünf Controllern ist ersichtlich, dass ziemlich viel Navigation in verschiedenen Ordnern erforderlich ist. Die Situation verschlechtert sich noch, wenn der Stamm des Projekts zahlreiche weitere Ordner enthält, weil „Controllers“ und „Views“ im Alphabet weit auseinander liegen (also weitere Ordner zwischen beiden in der Ordnerliste vorhanden sind).

Ein alternativer Ansatz zum Organisieren von Dateien nach ihrem Typ besteht in der Organisation entlang der Aufgabenlinien der Anwendung. Anstatt Ordner für Controller, Modelle und Ansichten zu verwenden, würde Ihr Projekt Ordner um Features oder Verantwortlichkeitsbereiche organisieren. Wenn Sie an einem Programmfehler oder Feature arbeiten, der bzw. das sich auf ein bestimmtes Feature der App bezieht, müssten weniger Ordner geöffnet werden, weil die in Zusammenhang stehenden Dateien zusammen gespeichert werden könnten. Dies kann auf verschiedene Weise erreicht werden, z. B. durch Verwenden des integrierten Bereichsfeatures und Erstellen einer eigenen Konvention für Featureordner.

Dateien in ASP.NET Core MVC

Ich möchte kurz erläutern, wie ASP.NET Core MVC mit den Standarddateitypen arbeitet, die eine in diesem Framework erstellte Anwendung verwendet. Die meisten Dateien auf der Serverseite der Anwendung sind Klassen, die in einer der .NET-Sprachen geschrieben wurden. Diese Codedateien können an einem beliebigen Speicherort auf dem Datenträger gespeichert werden. Sie müssen nur kompiliert werden können, und die Anwendung muss darauf verweisen können. Insbesondere Controller-Klassendateien müssen nicht in einem bestimmten Ordner gespeichert werden. Für verschiedene Typen von Modellklassen (Domänenmodell, Ansichtenmodell, Persistenzmodell usw.) gilt dies ebenfalls, und sie können problemlos in separaten Projekten gespeichert werden. Ihr Speicherort muss nicht das ASP.NET MVC Core-Projekt sein. Sie können die meisten Codedateien in der Anwendung wie gewünscht strukturieren bzw. neu organisieren.

Für Ansichten hingegen gilt dies nicht. Ansichten sind Inhaltsdateien. Es ist zwar irrelevant, wo diese relativ zu den Controllerklassen der Anwendung gespeichert sind, es ist jedoch wichtig, dass MVC weiß, wo nach diesen gesucht werden muss. Bereiche bieten integrierte Unterstützung für das Suchen nach Ansichten an anderen Speicherorten als im Standardordner „Views“. Sie können auch anpassen, wie MVC den Speicherort von Ansichten ermittelt.

Organisieren von MVC-Projekten mithilfe von Bereichen

Bereiche ermöglichen das Organisieren unabhängiger Module in einer ASP.NET MVC-Anwendung. Jeder Bereich weist eine Ordnerstruktur auf, die die Konventionen des Projektstamms imitiert. Ihre MVC-Anwendung weist daher die gleichen Stammordnerkonventionen auf sowie einen zusätzlichen Ordner namens „Areas“, in dem sich ein Ordner für jeden Abschnitt der App befindet, der jeweils Ordner für Controller und Ansichten (und ggf. Modelle oder ViewModels, wenn gewünscht) enthält.

Bereiche sind ein leistungsfähiges Feature, mit dem Sie eine große Anwendung in separate, logisch getrennte Unteranwendungen segmentieren können. Controller können z. B. bereichsübergreifend den gleichen Namen besitzen. Tatsächlich wird häufig eine Klasse „HomeController“ in jedem Bereich in einer Anwendung verwendet.

Wenn Sie einem ASP.NET MVC Core-Projekt Unterstützung für Bereiche hinzufügen möchten, müssen Sie einfach nur einen neuen Ordner auf Stammebene namens „Areas“ erstellen. Erstellen Sie in diesem Ordner einen neuen Ordner für jeden Teil Ihrer Anwendung, den Sie in einem Bereich organisieren möchten. Fügen Sie anschließend in diesem Ordner neue Ordner für Controller und Ansichten hinzu.

Beziehen Sie die Controllerdateien wie folgt ein:

/Areas/[area name]/Controllers/[controller name].cs

Auf Ihre Controller muss ein Attribut „Area“ angewendet werden, damit das Framework erkennen kann, dass sie zu einem bestimmten Bereich gehören:

namespace WithAreas.Areas.Ninjas.Controllers
{
  [Area("Ninjas")]
  public class HomeController : Controller

Binden Sie Ihre Ansichten wie folgt ein:

/Areas/[area name]/Views/[controller name]/[action name].cshtml

Alle Links zu Ansichten, die in Bereiche verschoben wurden, müssen aktualisiert werden. Wenn Sie Taghilfsprogramme verwenden, können Sie den Bereichsnamen als Teil des Taghilfsprogramms angeben. Beispiel:

<a asp-area="Ninjas" asp-controller="Home" asp-action="Index">Ninjas</a>

Links zwischen Ansichten im gleichen Bereich können das Attribut „asp-­area“ auslassen.

Im letzten Schritt müssen Sie zur Unterstützung von Bereichen in Ihrer App die Standardroutingregeln für die Anwendung in „Startup.cs“ in der Methode „Configure“ aktualisieren:

app.UseMvc(routes =>
{
  // Areas support
  routes.MapRoute(
    name: "areaRoute",
    template: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
  routes.MapRoute(
    name: "default",
    template: "{controller=Home}/{action=Index}/{id?}");
});

Die Beispielanwendung zum Verwalten verschiedener Ninjas, Piraten usw. könnte Bereiche verwenden, um die Projektorganisationsstruktur zu erzielen. Abbildung 3 zeigt dies.

Organisieren eines ASP.NET Core-Projekts mit Bereichen
Abbildung 3: Organisieren eines ASP.NET Core-Projekts mit Bereichen

Das Bereichsfeature stellt eine Verbesserung im Vergleich zur Standardkonvention dar, weil es separate Ordner für jeden logischen Abschnitt der Anwendung bereitstellt. Bereiche sind ein integriertes Feature in ASP.NET Core MVC und erfordern nur ein minimales Setup. Wenn Sie noch keine Bereiche verwenden, merken Sie sich einfach, dass sie sich zum Gruppieren verwandter Abschnitte in Ihrer App und Trennen vom Rest der App eignen.

Die Organisation in Bereichen ist aber dennoch recht ordnerlastig. Sie erkennen dies am vertikalen Platzbedarf, der zum Anzeigen der relativ kleinen Anzahl von Dateien im Ordner „Areas“ erforderlich ist. Wenn Sie nicht viele Controller pro Bereich und nicht viele Ansichten pro Controller verwenden, kann dieser Ordnermehraufwand genau so viel Reibung wie die Standardkonvention verursachen.

Glücklicherweise ist es einfach, eine eigene Konvention zu erstellen.

Featureordner in ASP.NET Core MVC

Abgesehen von der Standardordnerkonvention oder der Verwendung des integrierten Bereichsfeatures besteht die gängigste Methode zum Organisieren von MVC-Projekten in der Verwendung von Ordnern pro Feature. Dies gilt insbesondere für Teams, die Funktionalität in vertikalen Slices bereitstellen (siehe bit.ly/2abpJ7t), weil die meisten Schnittstellenerwägungen eines vertikalen Slice in einem dieser Featureordner gespeichert werden können.

Wenn Sie Ihr Projekt nach Feature (und nicht nach Dateityp) organisieren, verfügen Sie in der Regel über einen Stammordner (z. B. „Features“), in dem ein Unterordner pro Feature enthalten ist. Dies ähnelt der Organisationsweise von Bereichen sehr stark. In jedem Featureordner speichern Sie jedoch auch alle erforderlichen Controller-, Ansichten- und ViewModel-Typen. In den meisten Anwendungen führt dies zu einem Ordner mit vielleicht fünf bis 15 Elementen, die alle einen engen Bezug untereinander aufweisen. Der gesamte Inhalt des Featureordners kann im Projektmappen-Explorer angezeigt werden. Ein Beispiel für diese Organisation des Beispielprojekts finden Sie in Abbildung 4.

Featureordnerorganisation
Abbildung 4: Featureordnerorganisation

Beachten Sie, dass selbst die Ordner „Controllers“ und „Views“ auf Stammebene eliminiert wurden. Die Startseite für die App befindet sich nun in ihrem eigenen Featureordner namens „Home“, und freigegebene Dateien wie „_Layout.cshtml“ werden ebenso in einem Ordner „Shared“ im Ordner „Features“ gespeichert. Diese Projektorganisationsstruktur lässt sich recht gut skalieren, und Entwickler können sich auf weitaus weniger Ordner konzentrieren, wenn sie an einem bestimmten Abschnitt einer Anwendung arbeiten.

In diesem Beispiel sind (im Gegensatz zu Bereichen) keine zusätzlichen Routen erforderlich, und es werden keine Attribute für die Controller benötigt (beachten Sie jedoch, dass die Controllernamen für Features in dieser Implementierung eindeutig sein müssen). Damit diese Organisation unterstützt wird, benötigen Sie ein benutzerdefiniertes IViewLocationExpander- und IControllerModelConvention-Objekt. Beide werden zusammen mit einigen benutzerdefinierten ViewLocationFormats zum Konfigurieren von MVC in Ihrer Klasse „Startup“ verwendet.

Für einen bestimmten Controller sollte sinnvollerweise bekannt sein, welchem Feature er zugeordnet ist. Bereiche verwenden zu diesem Zweck Attribute. Dieser Ansatz verwendet eine Konvention. Die Konvention erwartet, dass sich der Controller in einem Namespace namens „Features“ befindet und dass das nächste Element in der Namespacehierarchie nach „Features“ der Featurename ist. Dieser Name wird Eigenschaften hinzugefügt, die während ViewLocation verfügbar sind. Abbildung 5 zeigt dies.

Abbildung 5: FeatureConvention: IControllerModelConvention

{
  public void Apply(ControllerModel controller)
  {
    controller.Properties.Add("feature", 
      GetFeatureName(controller.ControllerType));
  }
  private string GetFeatureName(TypeInfo controllerType)
  {
    string[] tokens = controllerType.FullName.Split('.');
    if (!tokens.Any(t => t == "Features")) return "";
    string featureName = tokens
      .SkipWhile(t => !t.Equals("features",
        StringComparison.CurrentCultureIgnoreCase))
      .Skip(1)
      .Take(1)
      .FirstOrDefault();
    return featureName;
  }
}

Sie fügen diese Konvention als Teil der MvcOptions hinzu, wenn Sie MVC in „Startup“ hinzufügen:

services.AddMvc(o => o.Conventions.Add(new FeatureConvention()));

Sie können zum Ersetzen der normalen von MVC verwendeten ViewLocation-Programmlogik durch die featurebasierte Konvention die Liste der von MVC verwendeten View­LocationFormats löschen und durch Ihre eigene Liste ersetzen. Dies geschieht als Teil des AddMvc-Aufrufs. Abbildung 6 zeigt dies.

Abbildung 6: Ersetzen der normalen von MVC verwendeten ViewLocation-Programmlogik

services.AddMvc(o => o.Conventions.Add(new FeatureConvention()))
  .AddRazorOptions(options =>
  {
    // {0} - Action Name
    // {1} - Controller Name
    // {2} - Area Name
    // {3} - Feature Name
    // Replace normal view location entirely
    options.ViewLocationFormats.Clear();
    options.ViewLocationFormats.Add("/Features/{3}/{1}/{0}.cshtml");
    options.ViewLocationFormats.Add("/Features/{3}/{0}.cshtml");
    options.ViewLocationFormats.Add("/Features/Shared/{0}.cshtml");
    options.ViewLocationExpanders.Add(new FeatureViewLocationExpander());
  }

Diese Formatzeichenfolgen enthalten standardmäßig Platzhalter für Aktionen („{0}“), Controller („{1}“) und Bereiche („{2}“). Für diesen Ansatz wird ein viertes Token („{3}“) für Features hinzugefügt.

Die verwendeten ViewLocation-Formate sollten Ansichten mit dem gleichen Name unterstützen, die jedoch von anderen Controllern in einem Feature verwendet werden. Recht häufig werden z. B. mehrere Controller in einem Feature verwendet, und für mehrere Controller wird eine Methode „Index“ verwendet. Dies wird durch die Suche nach Ansichten in einem Ordner unterstützt, der mit dem Controllernamen übereinstimmt. Aus diesem Grund würden „NinjasController.Index“ und „SwordsController.Index“ Ansichten in „/Features/Ninjas/Ninjas/Index.cshtml“ bzw. „/Features/Ninjas/Swords/Index.cshtml“ ermitteln (siehe Abbildung 7).

Mehrere Controller pro Feature
Abbildung 7: Mehrere Controller pro Feature

Beachten Sie, dass dies optional ist. Wenn Ihre Features keine Eindeutigkeitsforderung für Ansichten aufweisen müssen (etwa, weil das Feature nur einen Controller besitzt), können Sie die Ansichten einfach direkt im Featureordner speichern. Wenn Sie lieber Dateipräfixe als Ordner verwenden möchten, können Sie die zu verwendende Formatzeichenfolgen außerdem auf einfache Weise so anpassen, dass „{3}{1}“ anstelle von „{3}/{1}“ verwendet wird. Das Ergebnis sind Dateinamen für Ansichten wie etwa „NinjasIndex.cshtml“ und „SwordsIndex.cshtml“.

Freigegeben Ansichten werden ebenfalls im Stamm des Ordners „Features“ und in einem Unterordner „Shared“ unterstützt.

Die IViewLocationExpander-Schnittstelle stellt eine Methode („ExpandViewLocations“) zur Verfügung, die vom Framework zum Identifizieren von Ordnern verwendet wird, die Ansichten enthalten. Diese Ordner werden durchsucht, wenn eine Aktion eine Ansicht zurückgibt. Für diesen Ansatz ist nur erforderlich, dass der ViewLocationExpander das Token „{3}“ durch den Featurenamen des Controllers ersetzt, der durch die oben beschriebene FeatureConvention angegeben wird:

public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context,
  IEnumerable<string> viewLocations)
{
  // Error checking removed for brevity
  var controllerActionDescriptor =
    context.ActionContext.ActionDescriptor as ControllerActionDescriptor;
  string featureName = controllerActionDescriptor.Properties["feature"] as string;
  foreach (var location in viewLocations)
  {
    yield return location.Replace("{3}", featureName);
  }
}

Damit eine ordnungsgemäße Veröffentlichung unterstützt wird, müssen Sie außerdem die publishOptions der Datei „project.json“ so aktualisieren, dass der Ordner „Features“ enthalten ist:

"publishOptions": {
  "include": [
    "wwwroot",
    "Views",
    "Areas/**/*.cshtml",
    "Features/**/*.cshtml",
    "appsettings.json",
    "web.config"
  ]
},

Die neue Konvention der Verwendung eines Ordners „Features“ unterliegt vollständig Ihrer Kontrolle. Dies gilt auch dafür, wie die Ordner in diesem Ordner organisiert werden. Indem Sie die Sammlung der View­LocationFormats (und ggf. das Verhalten des FeatureViewLocationExpander-Typs) ändern, können Sie die vollständige Kontrolle über den Speicherort der Ansichten Ihrer App erlangen. Dies ist die einzige Voraussetzung für die Neuorganisation Ihrer Dateien, weil Controllertypen unabhängig von dem Ordner ermittelt werden, in dem sie gespeichert sind.

Parallele Featureordner

Wenn Sie Featureordner parallel mit dem MVC-Standardbereich und Konventionen für Ansichten testen möchten, sind nur minimale Änderungen erforderlich. Anstatt die ViewLocationFormats zu löschen, fügen Sie die Featureformate am Anfang der Liste ein (beachten Sie, dass die Reihenfolge umgekehrt ist):

options.ViewLocationFormats.Insert(0, "/Features/Shared/{0}.cshtml");
options.ViewLocationFormats.Insert(0, "/Features/{3}/{0}.cshtml");
options.ViewLocationFormats.Insert(0, "/Features/{3}/{1}/{0}.cshtml");

Damit Features in Kombination mit Bereichen unterstützt werden, ändern Sie auch die AreaViewLocationFormats-Sammlung:

options.AreaViewLocationFormats.Insert(0, "/Areas/{2}/Features/Shared/{0}.cshtml");
options.AreaViewLocationFormats.Insert(0, "/Areas/{2}/Features/{3}/{0}.cshtml");
options.AreaViewLocationFormats.Insert(0, "/Areas/{2}/Features/{3}/{1}/{0}.cshtml");

Was gilt für Modelle?

Scharfsinnige Leser werden bemerkt haben, dass ich meine Modelltypen nicht in die Featureordner (oder Bereiche) verschoben habe. In diesem Beispiel verwende ich keine separaten ViewModel-Typen, weil die von mir verwendeten Modelle unglaublich einfach sind. In einer echten App besitzt Ihr Domänen- oder Persistenzmodell wahrscheinlich eine größere Komplexität als für Ihre Ansichten erforderlich, und es wird voraussichtlich in einem eigenen, separaten Projekt definiert. Ihre MVC-App definiert wahrscheinlich ViewModel-Typen, die nur die für eine bestimmte Ansicht erforderlichen Daten enthalten, die für die Anzeige (oder den Verbrauch aus der API-Anforderung eines Clients) optimiert sind. Diese ViewModel-Typen sollten unter allen Umständen im Featureordner gespeichert werden, wenn sie genutzt werden (und diese Typen sollten nur selten von Features gemeinsam verwendet werden).

Zusammenfassung

Das Beispiel enthält alle drei Versionen der NinjaPiratePlantZombie-Organisationsanwendung mit Unterstützung zum Hinzufügen und Anzeigen jedes einzelnen Datentyps. Laden Sie das Beispiel herunter (oder zeigen Sie es auf GitHub an), und untersuchen Sie die verschiedenen Ansätze im Kontext einer Anwendung, an der Sie aktuell arbeiten. Experimentieren Sie mit dem Hinzufügen eines Bereichs oder eines Featureordners zu einer größeren Anwendung, an der Sie arbeiten, und überlegen Sie sich, ob Sie lieber mit Feature Slices als Organisationsmöglichkeit auf oberster Ebene der Ordnerstruktur Ihrer App oder Ordnern auf oberster Ebene basierend auf Dateitypen arbeiten möchten.

Der Quellcode für dieses Beispiel ist unter bit.ly/29MxsI0 verfügbar.


Steve Smithist ein unabhängiger Trainer, Mentor und Berater sowie ein ASP.NET MVP. Er hat Dutzende von Artikeln für die offizielle ASP.NET Core-Dokumentation (docs.asp.net) verfasst und unterstützt Teams dabei, sich schnell mit ASP.NET Core vertraut zu machen. Nehmen Sie unter ardalis.com Kontakt mit ihm auf.


Unser Dank gilt der folgenden technischen Expertin für die Durchsicht dieses Artikels: Ryan Nowak
Ryan Nowak ist ein Entwickler aus dem ASP.NET-Team bei Microsoft.