ASP.NET Core čte konfiguraci ze zásobníku zdrojů a vyhrává ten, který klíč nastaví jako poslední: appsettings.json, pak appsettings.{Environment}.json, pak user secrets (jen ve Development), pak proměnné prostředí a nakonec argumenty příkazové řádky. Na notebooku se tajemství berou z user secrets. Na serveru z proměnných prostředí, psaných s dvojitým podtržítkem tam, kde měl JSON vnoření - ConnectionStrings__Default, Smtp__Password. Každou sekci naváž na typovanou třídu pomocí vzoru options, při startu aplikace ji zvaliduj a chybějící heslo se stane srozumitelnou chybou hned v první vteřině, ne null reference při prvním požadavku.
To je celý model. Zbytek příspěvku jsou detaily, které rozhodují, jestli to funguje: jak se pojmenovávají klíče, které rozhraní načítá změny znovu, co user secrets vlastně jsou a chyby, kvůli kterým produkční aplikace běží s vývojářským nastavením.
Odkud konfigurace pochází#
WebApplication.CreateBuilder(args) nastaví výchozí zdroje za tebe. V pořadí od nejnižší priority po nejvyšší:
| Pořadí | Zdroj | Kdy se načte |
|---|---|---|
| 1 | appsettings.json | Vždy, pokud existuje |
| 2 | appsettings.{Environment}.json | Vždy, pokud existuje |
| 3 | User secrets | Prostředí je Development |
| 4 | Proměnné prostředí | Vždy |
| 5 | Argumenty příkazové řádky | Vždy |
Každý zdroj přispívá plochou sadou klíčů. Když dva zdroje nastaví stejný klíč, vyhraje pozdější; klíče, které nastavuje jen jeden zdroj, zůstanou nedotčené. Právě díky tomu je vrstvení užitečné: appsettings.json drží každé nastavení s bezpečnou výchozí hodnotou, soubor prostředí pár z nich změní a proměnné prostředí na serveru dodají těch pár, která jsou tajná nebo specifická pro daný stroj.
Znamená to také, že se může skrýt zastaralá hodnota. Proměnná prostředí nastavená na serveru před měsíci přebije cokoli, co dnes změníš v appsettings.Production.json, a nic ti to neřekne. Když se nastavení odmítá změnit, podívej se nejdřív na vyšší vrstvy.
Klíče, sekce a dvojité podtržítko#
Konfigurace je strom zploštělý na klíče spojené dvojtečkami. Tento JSON:
{ "ConnectionStrings": { "Default": "Host=localhost;Database=shop;Username=shop;Password=dev" }, "Smtp": { "Host": "smtp.example.com", "Port": 587, "From": "shop@example.com" }, "Cors": { "Origins": [ "https://example.com", "https://www.example.com" ] }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }}vytvoří klíče jako Smtp:Host, Cors:Origins:0 a Logging:LogLevel:Default. Pole se stanou očíslovanými potomky. Na velikosti písmen u klíčů nezáleží, takže smtp:host přečte stejnou hodnotu.
Názvy proměnných prostředí nemohou přenositelně obsahovat dvojtečku, takže provider prostředí místo ní přijímá dvojité podtržítko a převádí ho:
ConnectionStrings__Default=Host=203.0.113.10;Port=5432;Database=shop;Username=shop;Password=...Smtp__Password=...Cors__Origins__0=https://example.comCors__Origins__1=https://www.example.comLogging__LogLevel__Default=WarningDvě drobnosti tu lidi překvapí. Pole nastavené z proměnných prostředí nahrazuje prvky podle indexu, ne celé pole, takže když JSON obsahuje tři originy a prostředí nastaví __0 a __1, třetí z JSON tam pořád je. A jednoduché podtržítko je jen znak: Smtp_Password je jiný klíč než Smtp:Password a všechno, co čte ten druhý, ho tiše ignoruje.
Čtení hodnot přímo funguje pro jednorázové případy:
var connection = builder.Configuration.GetConnectionString("Default");var smtpHost = builder.Configuration["Smtp:Host"];var port = builder.Configuration.GetValue<int>("Smtp:Port", 587);GetConnectionString("Default") je zkratka za Configuration["ConnectionStrings:Default"]. Pro cokoli s víc než jedním nastavením použij vzor options níže, místo abys po kódu rozhazoval řetězcové klíče.
Prostředí a soubory pro jednotlivá prostředí#
Název prostředí pochází z ASPNETCORE_ENVIRONMENT, nebo z DOTNET_ENVIRONMENT, pokud není nastavená ta první, a pokud není ani jedna, výchozí je Production. Šablony nastavují Development v launchSettings.json, který používá dotnet run a Visual Studio, ale ne publikovaná aplikace - a právě proto server běží jako Production, aniž bys pro to něco dělal.
Prostředí rozhoduje o třech věcech:
- který
appsettings.{Environment}.jsonse načte; - jestli se načtou user secrets (ve výchozím stavu jen ve Development);
- co vrací
app.Environment.IsDevelopment(), čehož šablony využívají k zapnutí vývojářské stránky výjimek a vypnutí HSTS.
Použij Staging pro druhé nasazení, které se má chovat jako produkce, ale ukazovat na testovací data. IsStaging() existuje a appsettings.Staging.json se načte automaticky. Provoz obou vedle sebe pokrývají staging a produkce na jednom účtu.
Co patří do kterého souboru: appsettings.json dostane každý klíč, který aplikace čte, s hodnotami, které je bezpečné commitovat - hostitele, porty, feature flagy, timeouty. appsettings.Development.json dostane lokální přepisy, třeba podrobné logování. appsettings.Production.json je často zbytečný; pokud existuje, drží netajné produkční hodnoty. Žádný z nich nedrží hesla, API klíče ani connection stringy s přihlašovacími údaji, protože všechny se commitují.
User secrets při vývoji#
User secrets drží vývojářské přihlašovací údaje mimo repozitář tím, že je ukládají do tvého uživatelského profilu místo složky projektu.
$ dotnet user-secrets init$ dotnet user-secrets set "ConnectionStrings:Default" "Host=localhost;Password=dev-only"$ dotnet user-secrets set "Smtp:Password" "app-password-for-testing"$ dotnet user-secrets listinit přidá do souboru projektu GUID UserSecretsId. Hodnoty jdou do prostého souboru JSON v %APPDATA%\Microsoft\UserSecrets\<id>\secrets.json ve Windows, nebo ~/.microsoft/usersecrets/<id>/secrets.json v Linuxu a macOS.
Pochop, co to je a co není. Soubor není šifrovaný; je to obyčejný JSON ve tvém domovském adresáři. Jeho jediný úkol je držet tajemství mimo strom projektu, aby se nedala omylem commitnout. Načítá se jen v prostředí Development, takže to nikdy není mechanismus nasazení. Tým jím nesdílí nic - každý vývojář si nastaví vlastní.
Poslední bod způsobuje nejčastější selhání prvního nasazení v .NET: lokálně všechno funguje, protože connection string žije v user secrets, a na serveru ten klíč prostě neexistuje. Aplikace nastartuje, přijme požadavek a spadne, protože connection string je null. Validace při startu níže z toho udělá okamžitou a čitelnou chybu.
Tajemství na serveru#
Na serveru jsou standardním místem pro tajemství proměnné prostředí. Nastavují se mimo repozitář, čtou se normálním konfiguračním systémem bez změny kódu a přebíjejí každý soubor.
Na RE:NODE je nastavíš na záložce Startup serveru a projeví se při dalším startu kontejneru - takže změň hodnotu a pak restartuj. Tarif pro aplikace zahrnuje dva databázové sloty, vytvářené v panelu s vygenerovaným hostitelem, uživatelem a heslem; ty zkopíruj do ConnectionStrings__Default, místo abys je dával do commitovaného souboru. Obecná pravidla a to, co dělat v den, kdy se tajemství přesto dostane do historie Gitu, pokrývají proměnné prostředí a tajemství.
Čemu se vyhnout:
- Necommituj produkční `appsettings.Production.json` s přihlašovacími údaji a nepřidávej ho do `.gitignore` až později. Je v historii. Změň přihlašovací údaj.
- Neloguj svou konfiguraci.
IConfigurationRoot.GetDebugView()je užitečná metoda, která vypíše každý klíč, každou hodnotu a to, který provider je nastavil - včetně hesel. Použij ji lokálně; její výstup nikdy nezapisuj do produkčního logu. - Nepředávej tajemství jako argumenty příkazové řádky. Objevují se ve výpisech procesů a snadno se vloží do tiketu podpoře.
Pokud bys tajemství raději četl ze souborů než z prostředí - jeden soubor na klíč, jako fungují připojení tajemství v kontejnerech - balíček Microsoft.Extensions.Configuration.KeyPerFile to přidá jako zdroj pomocí builder.Configuration.AddKeyPerFile("/path/to/secrets", optional: true). Soubor pojmenovaný Smtp__Password v tom adresáři se stane klíčem Smtp:Password. Externí trezory (Azure Key Vault, HashiCorp Vault, AWS Secrets Manager) mají také konfigurační providery; dávají smysl, když už nějaký provozuješ, ne jako první krok pro jedinou aplikaci.
Jedno nastavení, sledované skrz všechny vrstvy
Pomáhá projít jednu sekci od notebooku po server. Vezmi sekci Smtp výše.
Na notebooku je prostředí Development. Smtp:Host, Smtp:Port a Smtp:From pocházejí z appsettings.json. appsettings.Development.json změní Smtp:Host na localhost a Smtp:Port na 1025, takže míří na lokální lapač pošty. Smtp:Password pochází z user secrets. Čtyři klíče, tři zdroje a v repozitáři nic tajného.
Na serveru je prostředí Production. appsettings.Development.json a user secrets se vůbec nenačtou. Smtp:Host, Smtp:Port a Smtp:From zase pocházejí z appsettings.json, takže commitované výchozí hodnoty by měly být ty produkční. Smtp:Password pochází z proměnné prostředí Smtp__Password na záložce Startup. Pokud pak budeš potřebovat nasměrovat produkci na jedno odpoledne na jiný relay, nastavení Smtp__Host v prostředí přepíše soubor bez commitu a odstranění proměnné vrátí commitovanou hodnotu.
Návyk, který z toho vyplývá: do základního souboru commituj výchozí hodnoty bezpečné pro produkci, vývojářská pohodlí dej do souboru Development a prostředí si nech pro tajemství a pro záměrné dočasné přepisy. Pokud se přistihneš, že na serveru nastavuješ tucet netajných proměnných, patří ty hodnoty pravděpodobně spíš do commitovaného souboru, kde se revidují a verzují jako zbytek kódu.
Vzor options#
Místo čtení řetězců naváž sekci na třídu:
public sealed class SmtpOptions{ public const string Section = "Smtp"; [Required] public string Host { get; set; } = ""; [Range(1, 65535)] public int Port { get; set; } = 587; [Required, EmailAddress] public string From { get; set; } = ""; [Required] public string Password { get; set; } = "";}builder.Services.AddOptions<SmtpOptions>() .BindConfiguration(SmtpOptions.Section) .ValidateDataAnnotations() .ValidateOnStart();Pak ji vstříkni tam, kde je potřeba. Rozhraní jsou tři a liší se tím, kdy se hodnoty čtou:
| Rozhraní | Životnost | Vidí změny po startu |
|---|---|---|
IOptions<T> | Singleton | Ne - čte se jednou, při prvním použití |
IOptionsSnapshot<T> | Scoped | Ano - přepočítává se pro každý požadavek |
IOptionsMonitor<T> | Singleton | Ano - CurrentValue a OnChange |
Ve výchozím stavu používej IOptions<T>. Je nejlevnější a nejpředvídatelnější: co aplikace přečetla při startu, to používá do restartu. IOptionsSnapshot<T> nejde vstříknout do singletonu, protože je scoped; tahle chyba vyvolá chybu dependency injection při startu. IOptionsMonitor<T> je pro singletony, které opravdu musí reagovat na změny, třeba služba na pozadí, jejíž interval dotazování chceš ladit za běhu.
Binder nastavuje veřejné vlastnosti se settery a páruje názvy bez ohledu na velikost písmen. Vlastnost bez odpovídajícího klíče si tiše ponechá výchozí hodnotu. Proto je validace důležitá: bez ní vyrobí překlep v názvu klíče objekt plný výchozích hodnot a žádnou chybu.
Validace při startu#
ValidateDataAnnotations() zkontroluje atributy na třídě. ValidateOnStart() zařídí, že se kontrola spustí při startu hostu, ne až když se poprvé někdo zeptá na options. Dohromady promění chybějící tajemství v tohle, hned v první vteřině logu:
Unhandled exception. Microsoft.Extensions.Options.OptionsValidationException:DataAnnotation validation failed for 'SmtpOptions' members: 'Password' with the error:'The Password field is required.'.To je chování, které na serveru chceš. Proces, který odmítne nastartovat s větou pojmenovávající chybějící klíč, je oprava na dvě minuty. Ten, který nastartuje, přijímá provoz a selže u prvního e-mailu, je incident.
Pro pravidla, která atributy nevyjádří, přidej delegáta:
builder.Services.AddOptions<SmtpOptions>() .BindConfiguration(SmtpOptions.Section) .Validate(o => o.Port != 25 || o.Host.EndsWith(".internal"), "Port 25 is only allowed for the internal relay.") .ValidateOnStart();Data annotations validují vlastnosti nejvyšší úrovně třídy options; vnořené objekty se nevalidují rekurzivně, dokud je nevalidaješ výslovně. Pokud mají tvoje options vnořené sekce, dej každé vlastní třídu options a vlastní validaci.
Proces, který při startu skončí, většina platforem restartuje, a to opakovaně. Na RE:NODE tři neočekávané restarty během hodiny vyvolají varování na stránce serveru a automaticky otevřou tiket, takže chyba konfigurace se nemůže nepozorovaně točit ve smyčce. Čti první řádky logu, ne poslední.
Znovunačtení bez redeploye#
Zdroje JSON se registrují s reloadOnChange: true, takže úprava appsettings.json na disku serveru aktualizuje konfiguraci v běžící aplikaci. Jestli to tvůj kód zaznamená, závisí na rozhraní: IOptionsMonitor<T> a IOptionsSnapshot<T> změnu vidí, IOptions<T> ne, a hodnoty, které sis při startu zkopíroval do polí, samozřejmě také ne.
Logování je jediné místo, kde je to skutečně užitečné. Úrovně logů se čtou přes monitor, takže změna Logging:LogLevel:Default v souboru na disku zvýší nebo sníží podrobnost bez restartu - šikovné, když honíš produkční problém. U všeho ostatního ber konfiguraci jako pevnou po dobu života procesu, měň ji přes prostředí a restartuj. Proměnné prostředí se čtou jednou při startu procesu a nikdy se nenačítají znovu. Soubory upravené na serveru navíc přepíše další deploy, pokud jsou sledované v repozitáři, což je dobrý důvod sledované soubory na serveru ručně vůbec neupravovat. Víc o volbě úrovní najdeš v lozích, které stojí za uchování.
Časté chyby#
Nastavení se nechce změnit. Nastavuje ho vyšší vrstva. Zkontroluj nejdřív proměnné prostředí, pak argumenty příkazové řádky.
Lokálně to funguje a na serveru je null. Hodnota žije v user secrets, které se načítají jen ve Development. Nastav ji na serveru jako proměnnou prostředí.
Server běží s vývojářským nastavením. ASPNETCORE_ENVIRONMENT=Development se do prostředí serveru zkopírovalo z nějakého tutoriálu. Odstraň to; výchozí je Production.
Proměnná prostředí se ignoruje. Jedno podtržítko místo dvou, překlep v názvu sekce, nebo se aplikace po změně nerestartovala.
Options jsou samé výchozí hodnoty. Název sekce předaný do BindConfiguration neodpovídá JSON, nebo vlastnosti nemají settery. Validace by to zachytila.
„Cannot consume scoped service IOptionsSnapshot from singleton“. V singletonech použij IOptionsMonitor<T>.
Číslo nebo boolean je špatně. Každá konfigurační hodnota je řetězec, dokud se nenaváže. "Port": "587" a "Port": 587 se navážou stejně, ale boolean zapsaný jako yes nebo port zapsaný jako 587/tcp převod nezvládne - a chyba pojmenuje klíč, tak si ji přečti.
Tajemství je přesto v logu. Něco zalogovalo celý objekt options, nebo zpráva výjimky obsahovala connection string. Přepiš ToString() u tříd options, které drží tajemství, a zkontroluj, co tvá logovací knihovna dělá s objekty předanými jako strukturované vlastnosti.
FAQ#
Je bezpečné držet tajemství v proměnných prostředí?
Pro většinu aplikací dost bezpečné a mnohem bezpečnější než commitované soubory. Kdo umí číst prostředí procesu nebo záložku Startup v panelu, přečte je, takže omez, kdo ten přístup má - rozdělení oprávnění v panelu pokrývají subuživatelé a nejmenší oprávnění.
Má se appsettings.Production.json commitovat?
Ano, pokud drží jen netajné hodnoty. Pokud potřebuje tajemství, patří to tajemství místo toho do proměnné prostředí a soubor ho vůbec obsahovat nemá.
Musí se ASPNETCORE_ENVIRONMENT na serveru nastavovat?
Ne. Výchozí je Production, což je správně. Nastav ji jen pro záměrně jiné prostředí, třeba Staging.
Jak nastavím connection string se středníkem v proměnné prostředí?
Nastav ho tak, jak je; středníky jsou součástí hodnoty a na záložce Startup nepotřebují escapování. Uvozovky by potřeboval jen shell. Funkční connection string pro každého providera ukazuje .NET s PostgreSQL, MySQL nebo SQL Serverem.
Mohu změnit konfiguraci bez restartu aplikace?
U hodnot čtených přes IOptionsMonitor<T> nebo u úrovní logů funguje úprava souboru JSON na disku. Proměnné prostředí vždy potřebují restart. Pro předvídatelnou aplikaci dávej přednost restartu.




Komentáře
Zcela anonymně: žádný účet, žádný e-mail, žádné cookie. Ukládáme jen jméno, které napíšeš, text a čas - nic dalšího. Počet odkazů je omezený a značkování se nevykresluje.