Zum Inhalt
wannacode

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");
});
Ausgabe
<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.cs

dotnet 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, weil View() sie dort sucht. Controller und Models können überall sein, aber alle erwarten sie in Controllers/ und Models/.
  • 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();
Ausgabe
/ -> 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/2 ist der Controller Products (die Klasse ProductsController), die Action Details und id = 2. Der Parameter int id der Methode bekommt 2 von selbst, über seinen Namen.
  • / hat keinen der Teile, also werden die Werte nach = genommen: Home und Index.
  • Das ? nach id bedeutet, 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 Startup mit zwei Methoden: ConfigureServices registriert die Services, und Configure legt 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.json hä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());
}
Antwort

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");
Ausgabe
GET /catalog -> 200 1 Ethiopia Yirgacheffe, 2 Colombia Supremo, 3 Sencha, 4 Earl Grey
POST /catalog -> 200 hinzugefügt 5 Matcha
PUT /catalog/2?price=11.90 -> 204
GET /catalog/2 -> 200 Colombia Supremo: 11.90 €
DELETE /catalog/4 -> 204
DELETE /catalog/4 -> 404
GET /catalog -> 200 1 Ethiopia Yirgacheffe, 2 Colombia Supremo, 3 Sencha, 5 Matcha
DELETE /catalog -> 405
  • price=11.90 kommt aus der Adresse, und beim POST kommen name und price aus 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 Attribute erben. [HttpGet("{id:int}")] erzeugt ein Objekt HttpGetAttribute mit 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ür ProductsController ist es api/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:

Ausgabe
Wir haben jeden Tag von 8 bis 20 Uhr geöffnet.
{"id":3,"name":"Sencha","price":8.40,"category":"tee"}
Info/9: 404
text/csv, products.csv
1;Ethiopia Yirgacheffe;14.90
2;Colombia Supremo;12.50
3;Sencha;8.40
4;Earl Grey;6.90
Create: 302, Location: /Products
  • Json schreibt die Eigenschaften mit kleinem Anfangsbuchstaben (name, price), wie in JSON üblich.
  • File fügt einen Header Content-Disposition mit 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 Header Location: /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 ersetzt Json(...) jedes Zeichen außerhalb von ASCII durch einen Code: „Café“ wird zu Caf\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 mit ConfigureHttpJsonOptions: Alle Buchstaben bleiben, und die für HTML gefährlichen Zeichen (<, >) werden kodiert. Für Controller heißt die Einstellung AddJsonOptions:
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 &#xE9;. Der Browser zeigt sie richtig an, aber in der Konsolenausgabe sind sie unlesbar. AddWebEncoders mit UnicodeRanges.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.
  • RedirectToAction ist 302 (eine vorübergehende Weiterleitung). Für „die Adresse hat sich endgültig geändert“ gibt es RedirectPermanent, 301. Redirect("https://…") leitet auf eine beliebige Adresse weiter.
  • Statt eines IActionResult kann eine Action auch ein einfaches Objekt zurückgeben, etwa ein Product. 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");
}

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");
}
Ausgabe
neuer ProductsController und neuer ProductService
4 Produkte
neuer ProductsController und neuer ProductService
4 Produkte

Mit 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 AddSingleton gibt es noch zwei Lebensdauern für Services. AddScoped gibt für jede Anfrage ein neues Objekt, innerhalb einer Anfrage aber dasselbe. So registriert man meist die Verbindung zur Datenbank (DbContext). AddTransient gibt 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
  1. Eine Anfrage GET /Products/Details/1. Die Route {controller}/{action}/{id} teilt sie in drei Teile.
  2. Products ist der ProductsController, Details die Action, und 1 wird zum Parameter id.
  3. Der Controller erzeugt den Service nicht selbst: Er bekommt ihn im Konstruktor (DI).
  4. Die Action fragt das Model: products.Find(1) gibt das Produkt zurück.
  5. Es gibt ein Produkt, also nicht NotFound().
  6. View(product) übergibt das Produkt an die View Details.cshtml.
  7. Die View macht HTML daraus, das zurück zum Browser geht.

Der Browser fragt nach /Products/Details/1:

  1. Die Route zerlegt den Pfad: Controller Products, Action Details, id = 1.
  2. ASP.NET Core erzeugt einen ProductsController und übergibt ihm den registrierten IProductService.
  3. Details(1) fragt das Model: products.Find(1) gibt das Produkt zurück. Wenn es das Produkt nicht gibt, gibt die Action NotFound() zurück, und der Weg endet mit 404.
  4. View(product) sucht die View Views/Products/Details.cshtml (der Ordner ist der Name des Controllers, die Datei der Name der Action) und übergibt ihr das Produkt.
  5. 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:

Ausgabe
<h1>Ethiopia Yirgacheffe</h1>
<p>Preis: 14.90 €</p>
<p>Kategorie: kaffee</p>
/Products/Details/99 -> 404
Genauer, für Neugierige
  • Das Programm ist ein Projekt mit drei Dateien. Neben Program.cs mit den Klassen und Details.cshtml hat es die .csproj, die dotnet new mvc erstellt. Die Views werden zusammen mit dem C#-Code bei dotnet build kompiliert, also fällt ein Fehler in @Model.Nme genau 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.

Ü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:

Ausgabe
/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 -> 404

2. 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:

Ausgabe
POST /Products/Create -> 302, Location: /Products
POST ohne Name -> 400
Ethiopia Yirgacheffe, Colombia Supremo, Sencha, Earl Grey, Matcha

3. 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:

Ausgabe
/Products/Category/tee -> 200 Sencha, Earl Grey | gesamt 15.30 €
/Products/Category/kaffee -> 200 Ethiopia Yirgacheffe, Colombia Supremo | gesamt 27.40 €
/Products/Category/kakao -> 404

Dann mach Index und Details mit Views statt mit Content. Wie, steht in der nächsten Lektion.

Test

Teste dich

1. Welcher Teil von MVC entscheidet, was bei einer Anfrage passiert und welche View gezeigt wird?
2. Die Route ist {controller=Home}/{action=Index}/{id?}. Welche Action bearbeitet /Products/Details/5?
3. Warum gibt ein erfolgreiches POST RedirectToAction zurück und nicht direkt eine Seite?
4. Der Controller verlangt in seinem Konstruktor einen IProductService. Woher bekommt er ihn?
5. Eine Action mit [HttpPost] bekommt eine GET-Anfrage an dieselbe Adresse. Was ist die Antwort?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·