MVC: Model, View, Controller
Kurz gesagt: MVC (Model-View-Controller) teilt eine Webanwendung in ein Model mit den Daten, eine View mit dem HTML und einen Controller, der die Anfrage annimmt und entscheidet, was zurückgegeben wird.
Problem
Alles in Program.cs
Der Katalog aus der vorigen Lektion funktioniert. Jetzt will der Shop eine Seite für jedes Produkt, einen Warenkorb, Bestellungen, eine Anmeldung für Kunden und eine Seite für das Lager. Mit MapGet kommt das alles in Program.cs, und jeder Handler holt die Daten selbst, entscheidet selbst, was er zeigt, und klebt das HTML selbst zusammen:
app.MapGet("/products/{id:int}", (int id) =>{ Product product = products.FirstOrDefault(p => p.Id == id); if (product == null) return Results.NotFound(); string html = "<h1>" + product.Name + "</h1><p>" + product.Price + " €</p>"; if (product.Category == "kaffee") html += "<p>Frisch im Laden gemahlen.</p>"; return Results.Content(html, "text/html; charset=utf-8");});<h1>Ethiopia Yirgacheffe</h1><p>14.90 €</p><p>Frisch im Laden gemahlen.</p>Mit fünfzig solchen Handlern wächst die Datei auf Tausende Zeilen. Eine Designerin, die eine Überschrift ändern will, muss sich durch C#-Code wühlen. Wenn der Preis an zwei Stellen berechnet wird, sind sich die beiden Stellen früher oder später nicht mehr einig.
Die Idee
Drei Teile mit eigenen Aufgaben
Webanwendungen werden seit Langem in drei Teile geteilt. Die Aufteilung heißt MVC (Model-View-Controller):
- Model: die Daten und die Regeln dafür. Die Klasse
Product, die Liste der Produkte, die Summe einer Bestellung. Das Model weiß nichts von HTML oder HTTP. - View: wie die Daten für einen Menschen aussehen. Ein HTML-Template mit Lücken für die Werte. In ASP.NET Core werden Views in Razor geschrieben: HTML mit C# darin (die nächste Lektion).
- Controller: nimmt die Anfrage an, fragt das Model und entscheidet, was zurückgegeben wird. Seine Methoden heißen Actions.
Jeder Teil hat im Projekt seinen eigenen Ordner:
Shop/├── Controllers/│ ├── HomeController.cs│ └── ProductsController.cs├── Models/│ └── Product.cs├── Views/│ ├── Home/│ │ └── Index.cshtml│ └── Products/│ ├── Index.cshtml│ └── Details.cshtml├── wwwroot/ ← CSS, JS und Bilder, so ausgeliefert, wie sie sind├── appsettings.json ← Einstellungen (Log-Level, Adresse der Datenbank)└── Program.csdotnet new mvc -n Shop erstellt ein Projekt mit diesem Aufbau.
Genauer, für Neugierige
- Die Aufteilung nach Aufgaben heißt Separation of Concerns (Trennung der Zuständigkeiten). Der Nutzen ist nicht nur Ordnung: Ein Model, das nichts von HTTP weiß, lässt sich mit einem gewöhnlichen Unit-Test testen, ohne Server.
- Die Ordner sind eine Konvention. Views müssen wirklich in
Views/<controller>/liegen, weilView()sie dort sucht. Controller und Models können überall sein, aber alle erwarten sie inControllers/undModels/. - In den Beispielen dieser Lektion steht alles in einer Datei, damit sich der Code in einem Schritt prüfen lässt. In einem echten Projekt steht jede Klasse in ihrer eigenen Datei.
Sieh es dir an
Controller und Actions
Program.cs einer MVC-Anwendung sagt zwei Dinge: „Ich verwende Controller“ (AddControllersWithViews) und „so kommt die Adresse zum Controller“ (MapControllerRoute):
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder();builder.Logging.ClearProviders();builder.Services.AddControllersWithViews();
var app = builder.Build();app.Urls.Add("http://localhost:7222");app.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");await app.StartAsync();Ein Controller ist eine Klasse, die von Controller erbt. Jede ihrer public-Methoden ist eine Action:
public class HomeController : Controller{ public IActionResult Index() => Content("Kaffee und Tee: Startseite");}
public class ProductsController : Controller{ private readonly ProductService products = new ProductService();
public IActionResult Index() { List<string> names = products.All().Select(p => p.Name).ToList(); return Content(string.Join(", ", names)); }
public IActionResult Details(int id) { Product product = products.Find(id); if (product == null) return NotFound(); return Content(product.Name + ": " + product.Price + " €"); }}Content gibt Text zurück und NotFound eine 404. Beide sind Action Results (IActionResult); gleich mehr dazu. Das => nach der Methode ist eine kurze Schreibweise für einen Rumpf aus einem Ausdruck: Index() => Content(...) ist dasselbe wie { return Content(...); }.
Der Client fragt nach mehreren Adressen:
var http = new HttpClient();foreach (string path in new string[] { "/", "/Products", "/Products/Details/2", "/Products/Details/9", "/products/details/3" }){ HttpResponseMessage response = await http.GetAsync("http://localhost:7222" + path); string body = await response.Content.ReadAsStringAsync(); Console.WriteLine($"{path} -> {(int)response.StatusCode} {body}".TrimEnd());}await app.StopAsync();/ -> 200 Kaffee und Tee: Startseite/Products -> 200 Ethiopia Yirgacheffe, Colombia Supremo, Sencha, Earl Grey/Products/Details/2 -> 200 Colombia Supremo: 12.50 €/Products/Details/9 -> 404/products/details/3 -> 200 Sencha: 8.40 €Das Muster {controller=Home}/{action=Index}/{id?} zerlegt den Pfad:
/Products/Details/2ist der ControllerProducts(die KlasseProductsController), die ActionDetailsundid= 2. Der Parameterint idder Methode bekommt 2 von selbst, über seinen Namen./hat keinen der Teile, also werden die Werte nach=genommen:HomeundIndex.- Das
?nachidbedeutet, dass sie fehlen darf. Groß- und Kleinschreibung im Pfad spielen keine Rolle.
Genauer, für Neugierige
- Nach Konvention endet der Name der Klasse auf
Controller, und in der Adresse schreibst du ihn ohne diese Endung. - In älteren Projekten (bis .NET 5) stand die Einrichtung in einer eigenen Klasse
Startupmit zwei Methoden:ConfigureServicesregistriert die Services, undConfigurelegt fest, wie Anfragen bearbeitet werden. Du triffst sie in vielen Artikeln und alten Projekten:
public class Startup{ public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration) { Configuration = configuration; }
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); }}Gestartet wird sie so:
IHost host = Host.CreateDefaultBuilder() .ConfigureLogging(logging => logging.ClearProviders()) .ConfigureWebHostDefaults(web => web.UseStartup<Startup>().UseUrls("http://localhost:7229")) .Build();await host.StartAsync();Die heutigen Vorlagen legen alles in Program.cs, wie in dieser Lektion. Die Aufgabe ist dieselbe: builder.Services ist ConfigureServices, und app.Map... ist Configure.
appsettings.jsonhält die Einstellungen, die du ohne Kompilieren änderst: die Log-Level, die Adresse der Datenbank, die erlaubten Hosts.
Probier es aus
Welche Adresse wohin führt
Die Route ist dieselbe, {controller=Home}/{action=Index}/{id?}, aber die Anwendung hat nur diesen Controller:
public class OrdersController : Controller{ public IActionResult Index() => Content("alle Bestellungen");
public IActionResult Details(int id) => Content("Bestellung " + id);}
var http = new HttpClient();foreach (string path in new string[] { "/Orders", "/Orders/Details/7", "/Orders/Details", "/" }){ HttpResponseMessage response = await http.GetAsync("http://localhost:7231" + path); string body = await response.Content.ReadAsStringAsync(); Console.WriteLine($"{path} -> {(int)response.StatusCode} {body}".TrimEnd());}/Orders geht zu Index, weil die Action fehlt und Index genommen wird. In /Orders/Details fehlt die id, und das ? erlaubt das, also bekommt int id den Standardwert 0. / sucht einen HomeController, und den gibt es nicht: 404.
Die Idee
Attribute: die Route direkt an der Methode
Das konventionelle Muster ist praktisch für Seiten. Manchmal willst du eine genaue Adresse oder für jede HTTP-Methode eine andere Action. Dann schreibst du die Route in eckigen Klammern über die Klasse und über die Methode:
[Route("catalog")]public class CatalogController : Controller{ private static readonly ProductService products = new ProductService();
[HttpGet("")] public IActionResult List() => Content(string.Join(", ", products.All().Select(p => p.Id + " " + p.Name)));
[HttpGet("{id:int}")] public IActionResult Show(int id) { Product product = products.Find(id); if (product == null) return NotFound(); return Content(product.Name + ": " + product.Price + " €"); }}[Route("catalog")] und [HttpGet("{id:int}")] sind Attribute, wie dein eigenes [Label] aus der Lektion über Reflection. Von sich aus tun sie nichts, aber das Framework liest sie und handelt danach. Hier versteht ASP.NET Core: „Dieser Controller ist unter /catalog, und Show beantwortet ein GET an /catalog/<zahl>.“
Für jede HTTP-Methode gibt es ein Attribut: [HttpGet] liest, [HttpPost] erstellt, [HttpPut] ändert, [HttpDelete] löscht. Einen Preis ändern:
[HttpPut("{id:int}")] public IActionResult ChangePrice(int id, decimal price) { Product product = products.Find(id); if (product == null) return NotFound(); product.Price = price; return NoContent(); }NoContent() ist 204: „erledigt, aber ich habe dir nichts zu zeigen“. Der Client schickt Anfragen mit verschiedenen Methoden. Send ist eine Hilfsmethode, die eine HttpRequestMessage schickt (die Anfrage als Objekt: Methode, Adresse, Body) und die Antwort ausgibt:
var form = new FormUrlEncodedContent(new Dictionary<string, string> { ["name"] = "Matcha", ["price"] = "18" });
await Send(HttpMethod.Get, "/catalog");await Send(HttpMethod.Post, "/catalog", form);await Send(HttpMethod.Put, "/catalog/2?price=11.90");await Send(HttpMethod.Get, "/catalog/2");await Send(HttpMethod.Delete, "/catalog/4");await Send(HttpMethod.Delete, "/catalog/4");await Send(HttpMethod.Get, "/catalog");await Send(HttpMethod.Delete, "/catalog");GET /catalog -> 200 1 Ethiopia Yirgacheffe, 2 Colombia Supremo, 3 Sencha, 4 Earl GreyPOST /catalog -> 200 hinzugefügt 5 MatchaPUT /catalog/2?price=11.90 -> 204GET /catalog/2 -> 200 Colombia Supremo: 11.90 €DELETE /catalog/4 -> 204DELETE /catalog/4 -> 404GET /catalog -> 200 1 Ethiopia Yirgacheffe, 2 Colombia Supremo, 3 Sencha, 5 MatchaDELETE /catalog -> 405price=11.90kommt aus der Adresse, und beim POST kommennameundpriceaus dem Body des Formulars. In beiden Fällen werden die Parameter über den Namen gefüllt.- Die letzte Anfrage ist ein DELETE an
/catalog. Diese Adresse gibt es, aber nur für GET und POST, also ist die Antwort 405, nicht 404. - Die Liste ist
static, damit sie für alle Anfragen dieselbe ist. Warum das nicht der gute Weg ist, sehen wir zwei Karten weiter.
Genauer, für Neugierige
- Wie in der Lektion über Reflection sind Attribute Klassen, die von
Attributeerben.[HttpGet("{id:int}")]erzeugt ein ObjektHttpGetAttributemit dem Argument"{id:int}". Das Framework findet sie mit Reflection, während es die Routen einrichtet. [Route("api/[controller]")]setzt den Namen des Controllers in die Adresse: FürProductsControllerist esapi/Products. So entstehen die APIs in der WebAPI-Lektion.- Konventionelle Routen und Attribut-Routen können in einer Anwendung zusammenleben. Ein Controller mit
[Route]ist über das konventionelle Muster nicht mehr erreichbar.
Die Idee
Was eine Action zurückgibt
Eine Action gibt ein IActionResult zurück: eine Beschreibung der Antwort, die ASP.NET Core in einen Status, Header und einen Body umwandelt. Die Methoden von Controller erzeugen die fertigen Arten:
| Methode | Was sie schickt | Status |
|---|---|---|
View(model) |
HTML aus einer Razor-View | 200 |
Content(text) |
reinen Text | 200 |
Json(obj) |
das Objekt als JSON | 200 |
File(bytes, type, name) |
eine Datei zum Herunterladen | 200 |
RedirectToAction("Index") |
„geh zu einer anderen Adresse“ | 302 |
NotFound() / BadRequest() / NoContent() |
eine leere Antwort | 404 / 400 / 204 |
public class ProductsController : Controller{ private readonly ProductService products = new ProductService();
public IActionResult Hours() => Content("Wir haben jeden Tag von 8 bis 20 Uhr geöffnet.");
public IActionResult Info(int id) { Product product = products.Find(id); if (product == null) return NotFound(); return Json(product); }
public IActionResult Report() { IEnumerable<string> lines = products.All().Select(p => p.Id + ";" + p.Name + ";" + p.Price); byte[] bytes = Encoding.UTF8.GetBytes(string.Join("\n", lines) + "\n"); return File(bytes, "text/csv", "products.csv"); }
[HttpPost] public IActionResult Create(string name) { if (string.IsNullOrWhiteSpace(name)) return BadRequest(); // The product is saved here (task 2 at the end of the lesson). return RedirectToAction("Index"); }}Der Client hat AllowAutoRedirect = false, damit wir die Weiterleitung selbst sehen. Sonst geht HttpClient wie ein Browser von selbst zur neuen Adresse:
Wir haben jeden Tag von 8 bis 20 Uhr geöffnet.{"id":3,"name":"Sencha","price":8.40,"category":"tee"}Info/9: 404text/csv, products.csv1;Ethiopia Yirgacheffe;14.902;Colombia Supremo;12.503;Sencha;8.404;Earl Grey;6.90Create: 302, Location: /ProductsJsonschreibt die Eigenschaften mit kleinem Anfangsbuchstaben (name,price), wie in JSON üblich.Filefügt einen HeaderContent-Dispositionmit dem Dateinamen hinzu. Der Browser sieht ihn und bietet „Speichern unter products.csv“ an. So lieferst du Berichte als PDF oder Excel aus:File(bytes, "application/pdf", "MonthlyReport.pdf").RedirectToAction("Index")antwortet mit 302 und dem HeaderLocation: /Products. Die Adresse wird aus der Route gebaut, du schreibst sie also nicht von Hand.
Genauer, für Neugierige
- ASP.NET Core schreibt JSON mit
System.Text.Json. Mit den Standardeinstellungen ersetztJson(...)jedes Zeichen außerhalb von ASCII durch einen Code: „Café“ wird zuCaf\u00E9. Das ist gültiges JSON, und jedes Programm liest es richtig, aber ein Mensch kann es nicht lesen. Deshalb stellt das Programm es ausdrücklich ein, wie in der vorigen Lektion mitConfigureHttpJsonOptions: Alle Buchstaben bleiben, und die für HTML gefährlichen Zeichen (<,>) werden kodiert. Für Controller heißt die EinstellungAddJsonOptions:
var builder = WebApplication.CreateBuilder();builder.Logging.ClearProviders();builder.Services.AddControllersWithViews() .AddJsonOptions(o => o.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All));
var app = builder.Build();app.Urls.Add("http://localhost:7224");app.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");await app.StartAsync();- Dasselbe gilt für Razor-Views: Sie kodieren Buchstaben wie
éalsé. Der Browser zeigt sie richtig an, aber in der Konsolenausgabe sind sie unlesbar.AddWebEncodersmitUnicodeRanges.All(auf der DI-Karte) lässt die Buchstaben, wie sie sind. Keine der beiden Einstellungen ist für eine echte Website nötig; sie sind da, damit die Ausgabe lesbar bleibt. RedirectToActionist 302 (eine vorübergehende Weiterleitung). Für „die Adresse hat sich endgültig geändert“ gibt esRedirectPermanent, 301.Redirect("https://…")leitet auf eine beliebige Adresse weiter.- Statt eines
IActionResultkann eine Action auch ein einfaches Objekt zurückgeben, etwa einProduct. Dann wird es als JSON geschickt. Das ist bei APIs üblich.
Probier es aus
Nach dem Speichern
Das Formular für ein neues Produkt wurde mit POST geschickt. Das Produkt ist gespeichert, und der Nutzer soll zur Liste gehen. Mit welcher Methode?
Was gibt die Action zurück, damit der Browser zur Liste geht?
[HttpPost]
public IActionResult Create(string name)
{
if (string.IsNullOrWhiteSpace(name)) return BadRequest();
return ("Index");
}RedirectToAction("Index") schickt eine 302 mit der Adresse von Index. Der Browser geht mit GET dorthin. Wenn der Nutzer auf „Neu laden“ drückt, wird nur das Anzeigen der Liste wiederholt, nicht das Speichern. Das ist das Muster POST, Redirect, GET, genauer in der nächsten Lektion.
Die Idee
Der Service kommt von außen
Bisher hat der Controller sein eigenes new ProductService() gemacht. ASP.NET Core erzeugt aber für jede Anfrage einen neuen Controller:
public class ProductsController : Controller{ private readonly ProductService products = new ProductService();
public ProductsController() { Console.WriteLine("neuer ProductsController und neuer ProductService"); }
public IActionResult Index() => Content(products.All().Count + " Produkte");}neuer ProductsController und neuer ProductService4 Produkteneuer ProductsController und neuer ProductService4 ProdukteMit jedem Controller entsteht auch eine neue Liste von Produkten. Ein Produkt, das bei einer Anfrage hinzugefügt wurde, ist bei der nächsten weg. static löst das, aber dann ist jeder Controller genau an diese Klasse gebunden. Wenn die Produkte in eine Datenbank umziehen, musst du jeden Controller ändern.
Der bessere Weg: Der Controller sagt, was er braucht, nicht, woher er es bekommt. Was er braucht, ist ein Interface:
public interface IProductService{ Product Find(int id); List<Product> All();}public class ProductService : IProductService{ private readonly List<Product> products = new List<Product> { new Product { Id = 1, Name = "Ethiopia Yirgacheffe", Price = 14.90m, Category = "kaffee" }, new Product { Id = 2, Name = "Colombia Supremo", Price = 12.50m, Category = "kaffee" }, new Product { Id = 3, Name = "Sencha", Price = 8.40m, Category = "tee" }, new Product { Id = 4, Name = "Earl Grey", Price = 6.90m, Category = "tee" }, };
public Product Find(int id) => products.FirstOrDefault(p => p.Id == id);
public List<Product> All() => products;}In Program.cs sagst du einmal, welche Klasse hinter dem Interface steht:
builder.Services.AddControllersWithViews();builder.Services.AddWebEncoders(o => o.TextEncoderSettings = new TextEncoderSettings(UnicodeRanges.All));builder.Services.AddSingleton<IProductService, ProductService>();AddSingleton<IProductService, ProductService>() bedeutet: „Wenn jemand nach einem IProductService fragt, gib ihm ein Objekt ProductService, dasselbe für die ganze Anwendung.“ Der Controller verlangt es als Parameter des Konstruktors, und ASP.NET Core übergibt es. Das heißt Dependency Injection (DI). Der Controller steht auf der nächsten Karte.
Genauer, für Neugierige
- Neben
AddSingletongibt es noch zwei Lebensdauern für Services.AddScopedgibt für jede Anfrage ein neues Objekt, innerhalb einer Anfrage aber dasselbe. So registriert man meist die Verbindung zur Datenbank (DbContext).AddTransientgibt bei jeder Anfrage nach dem Service ein neues Objekt. - Austauschen ist eine Zeile:
AddSingleton<IProductService, DatabaseProductService>(). In einem Test übergibst du dem Controller deine eigene Testklasse mit ein paar Produkten, ohne Server und ohne Datenbank. - Die Liste in einem Singleton wird von vielen Anfragen gleichzeitig verwendet. In einer echten Anwendung muss der Zugriff darauf geschützt werden. Wie, sehen wir in der WebAPI-Lektion.
Sieh es dir an
Eine Anfrage von Anfang bis Ende
public class ProductsController : Controller{ private readonly IProductService products;
public ProductsController(IProductService products) => this.products = products;
public IActionResult Details(int id) { Product product = products.Find(id); if (product == null) return NotFound(); return View(product); }}Schritt für Schritt, in Worten
- Eine Anfrage GET /Products/Details/1. Die Route {controller}/{action}/{id} teilt sie in drei Teile.
- Products ist der ProductsController, Details die Action, und 1 wird zum Parameter id.
- Der Controller erzeugt den Service nicht selbst: Er bekommt ihn im Konstruktor (DI).
- Die Action fragt das Model: products.Find(1) gibt das Produkt zurück.
- Es gibt ein Produkt, also nicht NotFound().
- View(product) übergibt das Produkt an die View Details.cshtml.
- Die View macht HTML daraus, das zurück zum Browser geht.
Der Browser fragt nach /Products/Details/1:
- Die Route zerlegt den Pfad: Controller
Products, ActionDetails,id= 1. - ASP.NET Core erzeugt einen
ProductsControllerund übergibt ihm den registriertenIProductService. Details(1)fragt das Model:products.Find(1)gibt das Produkt zurück. Wenn es das Produkt nicht gibt, gibt die ActionNotFound()zurück, und der Weg endet mit 404.View(product)sucht die ViewViews/Products/Details.cshtml(der Ordner ist der Name des Controllers, die Datei der Name der Action) und übergibt ihr das Produkt.- Razor füllt die Lücken aus, und das HTML kommt in die Antwort.
Die View Views/Products/Details.cshtml:
@model Product<h1>@Model.Name</h1><p>Preis: @Model.Price €</p><p>Kategorie: @Model.Category</p>@model Product sagt, welche Art von Objekt die View bekommt, und @Model.Name setzt seine Eigenschaft ein. Das Programm öffnet die beiden Adressen:
<h1>Ethiopia Yirgacheffe</h1><p>Preis: 14.90 €</p><p>Kategorie: kaffee</p>/Products/Details/99 -> 404Genauer, für Neugierige
- Das Programm ist ein Projekt mit drei Dateien. Neben
Program.csmit den Klassen undDetails.cshtmlhat es die.csproj, diedotnet new mvcerstellt. Die Views werden zusammen mit dem C#-Code beidotnet buildkompiliert, also fällt ein Fehler in@Model.Nmegenau dann auf. - Filter sind Code, der vor oder nach Actions läuft, ohne dass du ihn in jede schreibst.
[Authorize]über einem Controller lässt nur angemeldete Nutzer hinein, und ein Fehlerfilter macht aus jeder Exception eine Seite „Etwas ist schiefgelaufen“. - Areas teilen eine große Anwendung in Teile mit eigenen Controllern und Views, zum Beispiel
Areas/Admin/für den Verwaltungsbereich des Shops.
Probier es aus
Der Service ist nicht zu finden
Die Anwendung startet, aber jede Anfrage an /Products/Details/1 gibt 500 zurück. In der Konsole steht Unable to resolve service for type 'IProductService' while attempting to activate 'ProductsController'. In welcher Zeile ist der Fehler?
Wähle die Zeile mit dem Fehler.
AddSingleton<ProductService>() registriert die Klasse unter ihrem eigenen Namen. Der Controller verlangt aber IProductService, und das Framework weiß nicht, welche Klasse es ihm geben soll. Die Lösung ist AddSingleton<IProductService, ProductService>(): zuerst das Interface, nach dem gefragt wird, dann die Klasse, die es implementiert.
Übung
Der Katalog als MVC
Löse die Aufgaben in deinem Editor, in einem Projekt aus dotnet new mvc oder in einer Datei wie in der Lektion. Die Produkte sind die vier aus der Lektion, und der Service wird mit AddSingleton<IProductService, ProductService>() registriert und in den Konstruktor des Controllers übergeben.
1. Katalog. Ein ProductsController mit einem Index, der alle Produkte zurückgibt, eins pro Zeile, und einem Details(int id), der Name, Kategorie und Preis zurückgibt oder 404. Der Client fragt nach /Products, /Products/Details/3 und /Products/Details/42:
/Products -> 200 1. Ethiopia Yirgacheffe: 14.90 €2. Colombia Supremo: 12.50 €3. Sencha: 8.40 €4. Earl Grey: 6.90 €/Products/Details/3 -> 200 Sencha (tee): 8.40 €/Products/Details/42 -> 4042. Weiterleitung nach dem Speichern. Füge IProductService eine Methode void Add(Product product) hinzu, die dem Produkt die nächste freie Id gibt. Die Action [HttpPost] Create(string name, decimal price, string category) gibt ohne Namen 400 zurück, sonst speichert sie das Produkt und leitet auf Index weiter. Der Client hat AllowAutoRedirect = false und öffnet dann die Adresse aus Location:
POST /Products/Create -> 302, Location: /ProductsPOST ohne Name -> 400Ethiopia Yirgacheffe, Colombia Supremo, Sencha, Earl Grey, Matcha3. Produkte nach Kategorie. Die Action Category(string id) gibt die Namen der Produkte in der Kategorie und die Summe ihrer Preise zurück oder 404, wenn es keine gibt. Mit der konventionellen Route kommt die Kategorie als id herein. Wenn die Kategorie Buchstaben außerhalb des einfachen lateinischen Alphabets hätte, würde HttpClient sie in der Adresse von selbst kodieren:
/Products/Category/tee -> 200 Sencha, Earl Grey | gesamt 15.30 €/Products/Category/kaffee -> 200 Ethiopia Yirgacheffe, Colombia Supremo | gesamt 27.40 €/Products/Category/kakao -> 404Dann mach Index und Details mit Views statt mit Content. Wie, steht in der nächsten Lektion.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Listen, Dictionaries und LINQ · async und await · Wie das Internet funktioniert · Ein Webserver mit Minimal API
- Geht weiter in: Views mit Razor · WebAPI: Daten für andere Programme
- Aufgaben zum Thema: Web und Desktop