Middleware pipeline, urutannya, dan Options Pattern
Jalur yang dilewati setiap request, kenapa satu baris yang tertukar bisa membuka celah keamanan, dan cara Service membaca konfigurasi tanpa menyentuh secret
1. Pipeline: rangkaian middleware yang membungkus endpoint
Di lesson 0001 request "masuk" ke Controller. Sebenarnya sebelum sampai ke sana, request melewati Middleware. Microsoft: "Middleware is software that's assembled into an app pipeline to handle requests and responses. Each middleware: Chooses whether to pass the request to the next middleware in the pipeline. Can perform work before and after the next middleware in the pipeline." [1]
"The order that middleware appears in the app's Program file defines the order in which middleware are invoked on a request with the reverse order for the response."
[1]
Bayangkan bawang: masuk lapis demi lapis, keluar lewat lapis yang sama dengan urutan terbalik.
app.Use(async (context, next) =>
{
var sw = Stopwatch.StartNew(); // sebelum: request masuk
await next(context); // teruskan ke middleware berikutnya
logger.LogInformation("{Path} {Ms}ms", // sesudah: response kembali
context.Request.Path, sw.ElapsedMilliseconds);
});
| ASP.NET Core | Artinya | Padanan Express |
|---|---|---|
app.Use((context, next) => …) | middleware biasa, boleh memanggil next | app.use((req, res, next) => …) |
short-circuit: tidak memanggil next | pipeline berhenti di sini; middleware ini menjadi terminal middleware [1] | res.status(401).send() tanpa next() |
app.Run(context => …) | terminal: tidak menerima next; Use sesudahnya "never executes" [1] | handler terakhir yang selalu mengirim response |
Express juga mendefinisikan middleware sebagai fungsi dengan akses ke req, res, dan next, dan "If a middleware function does not end the request-response cycle, it must call next()".
[7]
Bedanya di ASP.NET Core: await next(context) menunggu seluruh sisa pipeline selesai, jadi kode sesudahnya bisa membaca status response.
Latihan 1 — Cara kerja pipeline
Jawab tanpa scroll ke atas. Soal yang sama muncul lagi, diacak, di review campuran 0007.
Kuis di lesson ini adalah feedback cepat, bukan bukti penguasaan; yang dicatat hanya hasil di review campuran 0007.
2. Urutan yang dipakai API Product Catalog
"The order of middleware can be critical for security, performance, and functionality." [1] Untuk Web API yang dipanggil Angular, urutan dari dokumentasi menjadi:
var app = builder.Build();
app.UseExceptionHandler("/error"); // 1. paling luar: membungkus semuanya
app.UseHttpsRedirection(); // 2. http → https
app.UseRouting(); // 3. pilih endpoint
app.UseCors("angular"); // 4. header CORS + preflight
app.UseAuthentication(); // 5. SIAPA user ini? (isi HttpContext.User)
app.UseAuthorization(); // 6. BOLEH atau tidak?
app.MapControllers(); // 7. endpoint: Controller → Service → Repository
app.Run();
- Exception handler paling awal. "Exception-handling delegates should be called early in the pipeline, so they can catch exceptions that occur in later stages of the pipeline."
[1]
Jebakan bagi developer Express: di Express "You define error-handling middleware last, after other
app.use()and routes calls". [8] Di ASP.NET Core exception handler membungkus, jadi ia harus di luar. (Detail global error handling: lesson 0017.) - CORS → Authentication → Authorization. Ketiganya "must appear in the order shown".
[1]
"The call to
UseCorsmust be placed afterUseRouting, but beforeUseAuthorization." [6] Authorization butuh tahu siapa user-nya, jadi authentication harus jalan dulu. - CORS sebelum response caching, "to add CORS headers on every request, including cached responses." [1]
Detail .NET 8 yang jarang diketahui: WebApplication menambahkan UseRouting, UseAuthentication, dan UseAuthorization otomatis bila Anda tidak memanggilnya.
Tapi kalau Anda memakai CORS, "The app needs to call UseAuthentication and UseAuthorization if UseCors is called", karena yang otomatis itu diletakkan tepat sesudah UseRouting, yaitu sebelum UseCors Anda.
[1]
Latihan 2 — Susun ulang pipeline
Baris Program.cs di bawah sudah diacak. Pindahkan dengan ↑/↓ sampai urutannya benar, lalu tekan Periksa. Coba dari ingatan dulu.
app.UseExceptionHandler("/error")app.UseHttpsRedirection()app.UseRouting()app.UseCors("angular")app.UseAuthentication()app.UseAuthorization()app.MapControllers()
Exception handler paling luar supaya menangkap exception dari semua yang sesudahnya. Routing memilih endpoint, CORS harus sesudah routing dan sebelum authorization,
authentication mengisi HttpContext.User sebelum authorization memutuskan, dan endpoint (Controller) paling dalam.
3. Service Scoped di dalam middleware
Di lesson 0004 Anda belajar captive dependency. Middleware adalah contoh nyatanya:
"Middleware is constructed at app startup and therefore has application life time."
Jadi service Scoped tidak boleh diterima lewat constructor. "To share a scoped service between middleware and other types, add these services to the InvokeAsync method's signature."
[2]
public class RequestAuditMiddleware(RequestDelegate next, ILogger<RequestAuditMiddleware> logger)
{ // constructor: hanya dependensi berumur panjang (Singleton)
public async Task InvokeAsync(HttpContext context, AppDbContext db) // Scoped: per request
{
await next(context);
db.AuditLogs.Add(new AuditLog(context.Request.Path, context.Response.StatusCode));
await db.SaveChangesAsync();
}
}
// Program.cs
app.UseMiddleware<RequestAuditMiddleware>();
Syarat middleware class: "A public constructor with a parameter of type RequestDelegate" dan "A public method named Invoke or InvokeAsync" yang mengembalikan Task dengan parameter pertama HttpContext.
[2]
Padanan Express: fungsi middleware dibuat sekali saat app.use(...), sedangkan objek per request Anda ambil dari req.
Latihan 3 — Scoped di middleware
4. Konfigurasi berlapis: siapa menimpa siapa
WebApplication.CreateBuilder menggabungkan beberapa sumber. Dari prioritas tertinggi ke terendah:
[3]
- argumen command-line
- environment variable (yang tidak berawalan
ASPNETCORE_/DOTNET_) - User Secrets, "when the app runs in the
Developmentenvironment" appsettings.{Environment}.json(mis.appsettings.Production.json)appsettings.json
Sumber yang dibaca belakangan menimpa yang sebelumnya: "key values read from the environment override values read from appsettings.json, appsettings.{ENVIRONMENT}.json, and user secrets."
[3]
Di environment variable, hierarki ditulis dengan double underscore: Jwt__Key menjadi Jwt:Key, karena "The colon (:) separator doesn't work with environment variable hierarchical keys on all platforms."
[3]
Di mana secret disimpan?
- Jangan di kode atau appsettings: "Never store passwords or other sensitive data in source code or configuration files." [5]
- Development:
dotnet user-secrets initlaludotnet user-secrets set "Jwt:Key" "…". File-nya di profil user (~/.microsoft/usersecrets/<id>/secrets.json), "separate location from the project tree", dan "aren't checked into source control". Tapi Secret Manager "doesn't encrypt the stored secrets" dan "is for development purposes only". [5] - Production: environment variable, atau "a controlled means like Azure Key Vault". [5]
Padanan Node: appsettings.json ≈ default di modul config/; User Secrets ≈ file .env yang masuk .gitignore, bedanya ia disimpan di luar folder project; environment variable sama-sama process.env.
Latihan 4 — Susun prioritas konfigurasi
Urutkan dari prioritas terendah (paling mudah ditimpa) ke tertinggi, untuk app yang jalan di Development.
appsettings.jsonappsettings.Development.json- User Secrets
- Environment variable
- Argumen command-line
Setiap sumber menimpa sumber di atasnya. User Secrets terdaftar sesudah file appsettings, sehingga "user secrets keys take precedence over keys in appsettings.json and appsettings.{ENVIRONMENT}.json."
Command-line paling akhir, jadi paling kuat.
5. Options Pattern: Service membaca class, bukan string
"The options pattern uses classes to provide strongly typed access to groups of related settings." Dua prinsip yang terpenuhi: Encapsulation ("Classes that depend on configuration settings depend only on the configuration settings that they use") dan Separation of Concerns. [4]
// appsettings.json: { "Jwt": { "Issuer": "catalog-api", "Key": "" } } ← Key diisi dari User Secrets / env
public class JwtOptions
{
public const string Section = "Jwt"; // field: tidak di-bind
[Required] public string Issuer { get; set; } = "";
[Required] public string Key { get; set; } = "";
}
// Program.cs (composition root)
builder.Services.AddOptions<JwtOptions>()
.Bind(builder.Configuration.GetSection(JwtOptions.Section))
.ValidateDataAnnotations()
.ValidateOnStart(); // gagal saat start, bukan saat request pertama
// Service
public class TokenService(IOptions<JwtOptions> options)
{
private readonly JwtOptions _jwt = options.Value;
}
Bentuk singkatnya, tanpa validasi: builder.Services.Configure<JwtOptions>(builder.Configuration.GetSection("Jwt"));
[4]
Tanpa ValidateOnStart, "Options validation runs the first time a TOption instance is created".
[4]
| Interface | Lifetime | Membaca perubahan setelah start? | Pakai untuk |
|---|---|---|---|
IOptions<T> | Singleton | tidak: "Does not support: Reading of configuration data after the app has started" | default; setting yang tetap |
IOptionsSnapshot<T> | Scoped: "so it can't be injected into a singleton service" | ya, dihitung ulang per request | Service Scoped yang butuh nilai terbaru per request |
IOptionsMonitor<T> | Singleton | ya: "Reloadable configuration", change notification | Singleton / BackgroundService yang butuh nilai terbaru |
Kutipan tabel: [4] Options interfaces. Perhatikan IOptionsSnapshot + Singleton = captive dependency lagi.
Latihan 5 — Konfigurasi dan Options
Latihan 6 — Ucapkan dengan suara keras
Pasang timer 90 detik dan jawab dengan suara keras, seolah sedang interview. Jangan buka jawaban contoh sebelum selesai.
"Jelaskan apa yang terjadi pada sebuah request di ASP.NET Core dari masuk sampai response keluar, dan kenapa urutan middleware penting."
Bandingkan dengan jawaban contoh dan checklist-nya
"Request masuk ke pipeline middleware yang disusun di Program.cs. Setiap middleware bisa mengerjakan sesuatu sebelum memanggil next,
lalu sesudahnya saat response kembali, jadi urutannya seperti bawang: masuk A-B-C, keluar C-B-A. Middleware juga bisa short-circuit,
misalnya langsung mengembalikan 401 tanpa memanggil next. Karena itu urutan menentukan perilaku: exception handler dipasang paling awal
supaya membungkus semua yang sesudahnya, kebalikan dari Express di mana error handler ditaruh paling akhir. Lalu HTTPS redirection, routing, CORS,
authentication, authorization, baru endpoint seperti MapControllers. Authentication harus sebelum authorization karena kita harus tahu siapa
user-nya dulu sebelum memutuskan boleh atau tidak, dan CORS sebelum keduanya. Middleware dibuat sekali seumur aplikasi, jadi service Scoped seperti
DbContext diterima lewat parameter InvokeAsync, bukan constructor."
Checklist: apakah Anda menyebut…
- pipeline = rangkaian middleware yang disusun di
Program.cs, dijalankan berurutan? - setiap middleware bisa bekerja sebelum dan sesudah
next(masuk A-B-C, keluar C-B-A)? - middleware bisa short-circuit (tidak memanggil
next), contoh 401? - exception handler paling awal supaya menangkap exception dari middleware sesudahnya (beda dengan Express)?
- urutan inti: routing → CORS → authentication → authorization → endpoint?
- middleware dibuat sekali, jadi service Scoped diterima di parameter
InvokeAsync?
Lulus kalau minimal 4 dari 6. Kalau kurang, ulangi besok pagi sebelum lesson berikutnya (spacing).
Bonus 60 detik
"Di mana Anda menyimpan connection string dan secret, dan bagaimana Service membacanya?"
Bandingkan dengan jawaban contoh dan checklist-nya
"Konfigurasi di ASP.NET Core berlapis. Paling bawah appsettings.json, lalu ditimpa appsettings.Development.json atau
appsettings.Production.json sesuai environment, lalu User Secrets kalau di Development, lalu environment variable, dan paling atas argumen command-line.
Secret tidak pernah saya taruh di appsettings yang di-commit. Saat development saya pakai dotnet user-secrets, yang disimpan di profil user di luar repo
tapi tidak dienkripsi. Di production pakai environment variable, misalnya ConnectionStrings__Default dengan double underscore, atau Key Vault.
Service tidak membaca IConfiguration langsung; saya buat class seperti JwtOptions, daftarkan dengan
Configure<JwtOptions>(GetSection("Jwt")), lalu inject IOptions<JwtOptions>, supaya bertipe kuat dan tiap class hanya tahu
setting miliknya. Kalau nilainya bisa berubah saat runtime saya pakai IOptionsMonitor. Padanannya di Node: process.env plus modul
config/ yang divalidasi."
Checklist: apakah Anda menyebut…
- lapisan: appsettings.json → appsettings.{Environment}.json → User Secrets → environment variable → command-line?
- sumber yang belakangan menimpa yang sebelumnya?
- secret tidak di appsettings yang di-commit; development pakai User Secrets (di luar repo, tidak terenkripsi)?
- production: environment variable (pakai
__untuk hierarki) atau secret store seperti Key Vault? - Options Pattern: class bertipe +
Configure<T>(GetSection)+ injectIOptions<T>? IOptionsvsIOptionsSnapshot(Scoped, per request) vsIOptionsMonitor(Singleton, reload)?
Lulus kalau minimal 4 dari 6. Kedua pertanyaan ini terdaftar di bank soal dan bisa muncul di review 0007.
Ringkasan untuk dicetak: Reference: Middleware dan Options Pattern · Istilah: GLOSSARY.md
Ada yang belum jelas? Tanyakan ke saya (agent) di terminal, misalnya: "kenapa preflight CORS bisa gagal kalau UseCors ditaruh sesudah UseAuthorization?" atau "kapan saya butuh IOptionsSnapshot, bukan IOptions?"
Sumber: [1] ASP.NET Core middleware (.NET 8) · [2] Write custom ASP.NET Core middleware · [3] Configuration in ASP.NET Core · [4] Options pattern in ASP.NET Core · [5] Safe storage of app secrets in development · [6] Enable CORS in ASP.NET Core · [7] Express: Using middleware · [8] Express: Error handling