[{"data":1,"prerenderedAt":1285},["ShallowReactive",2],{"blog-\u002Fblog\u002Fdotnet-options-pattern":3},{"id":4,"title":5,"body":6,"description":1270,"difficulty":1271,"extension":1272,"framework":1273,"frameworkSlug":1274,"meta":1275,"navigation":155,"order":59,"path":1276,"qaPath":1277,"seo":1278,"stem":1279,"subtopic":1280,"topic":1281,"topicSlug":1282,"updated":1283,"__hash__":1284},"blog\u002Fblog\u002Fdotnet-options-pattern.md","The .NET Options Pattern: IOptions, IOptionsSnapshot, and IOptionsMonitor",{"type":7,"value":8,"toc":1253},"minimark",[9,14,27,31,34,105,108,223,227,230,235,276,282,286,327,338,342,422,428,432,502,506,509,570,573,577,588,782,788,792,805,909,921,925,928,1072,1076,1083,1146,1152,1156,1210,1221,1225,1249],[10,11,13],"h2",{"id":12},"why-the-options-pattern-matters-for-net-interviews","Why the options pattern matters for .NET interviews",[15,16,17,18,22,23,26],"p",{},"Interviewers ask about the options pattern because the wrong approach — injecting raw\n",[19,20,21],"code",{},"IConfiguration"," everywhere, reading settings without validation, or using ",[19,24,25],{},"IOptions\u003CT>"," in\na service that needs live reloads — produces hard-to-debug runtime failures. This article\nexplains every variant and when to reach for each one.",[10,28,30],{"id":29},"the-problem-with-iconfiguration-in-application-code","The problem with IConfiguration in application code",[15,32,33],{},"Without the options pattern, services reach into raw configuration by string key:",[35,36,41],"pre",{"className":37,"code":38,"language":39,"meta":40,"style":40},"language-csharp shiki shiki-themes github-light github-dark","\u002F\u002F Fragile: magic strings, no type safety, no null checking, not testable:\npublic class EmailService\n{\n    public EmailService(IConfiguration config)\n    {\n        var host    = config[\"Smtp:Host\"];           \u002F\u002F null if key missing\n        var port    = int.Parse(config[\"Smtp:Port\"]); \u002F\u002F FormatException if malformed\n        var apiKey  = config[\"Smtp:ApiKey\"];          \u002F\u002F wrong case → silent null\n    }\n}\n","csharp","",[19,42,43,51,57,63,69,75,81,87,93,99],{"__ignoreMap":40},[44,45,48],"span",{"class":46,"line":47},"line",1,[44,49,50],{},"\u002F\u002F Fragile: magic strings, no type safety, no null checking, not testable:\n",[44,52,54],{"class":46,"line":53},2,[44,55,56],{},"public class EmailService\n",[44,58,60],{"class":46,"line":59},3,[44,61,62],{},"{\n",[44,64,66],{"class":46,"line":65},4,[44,67,68],{},"    public EmailService(IConfiguration config)\n",[44,70,72],{"class":46,"line":71},5,[44,73,74],{},"    {\n",[44,76,78],{"class":46,"line":77},6,[44,79,80],{},"        var host    = config[\"Smtp:Host\"];           \u002F\u002F null if key missing\n",[44,82,84],{"class":46,"line":83},7,[44,85,86],{},"        var port    = int.Parse(config[\"Smtp:Port\"]); \u002F\u002F FormatException if malformed\n",[44,88,90],{"class":46,"line":89},8,[44,91,92],{},"        var apiKey  = config[\"Smtp:ApiKey\"];          \u002F\u002F wrong case → silent null\n",[44,94,96],{"class":46,"line":95},9,[44,97,98],{},"    }\n",[44,100,102],{"class":46,"line":101},10,[44,103,104],{},"}\n",[15,106,107],{},"The options pattern wraps configuration in a typed, validated, injectable class:",[35,109,111],{"className":37,"code":110,"language":39,"meta":40,"style":40},"\u002F\u002F Settings class — plain C# POCO:\npublic class SmtpSettings\n{\n    public string Host   { get; set; } = \"\";\n    public int    Port   { get; set; } = 587;\n    public bool   UseTls { get; set; } = true;\n    public string ApiKey { get; set; } = \"\";\n}\n\n\u002F\u002F Registration:\nbuilder.Services.Configure\u003CSmtpSettings>(\n    builder.Configuration.GetSection(\"Smtp\"));\n\n\u002F\u002F Injection — strongly typed, testable, no magic strings:\npublic class EmailService\n{\n    private readonly SmtpSettings _settings;\n\n    public EmailService(IOptions\u003CSmtpSettings> options)\n        => _settings = options.Value;\n}\n",[19,112,113,118,123,127,132,137,142,147,151,157,162,168,174,179,185,190,195,201,206,212,218],{"__ignoreMap":40},[44,114,115],{"class":46,"line":47},[44,116,117],{},"\u002F\u002F Settings class — plain C# POCO:\n",[44,119,120],{"class":46,"line":53},[44,121,122],{},"public class SmtpSettings\n",[44,124,125],{"class":46,"line":59},[44,126,62],{},[44,128,129],{"class":46,"line":65},[44,130,131],{},"    public string Host   { get; set; } = \"\";\n",[44,133,134],{"class":46,"line":71},[44,135,136],{},"    public int    Port   { get; set; } = 587;\n",[44,138,139],{"class":46,"line":77},[44,140,141],{},"    public bool   UseTls { get; set; } = true;\n",[44,143,144],{"class":46,"line":83},[44,145,146],{},"    public string ApiKey { get; set; } = \"\";\n",[44,148,149],{"class":46,"line":89},[44,150,104],{},[44,152,153],{"class":46,"line":95},[44,154,156],{"emptyLinePlaceholder":155},true,"\n",[44,158,159],{"class":46,"line":101},[44,160,161],{},"\u002F\u002F Registration:\n",[44,163,165],{"class":46,"line":164},11,[44,166,167],{},"builder.Services.Configure\u003CSmtpSettings>(\n",[44,169,171],{"class":46,"line":170},12,[44,172,173],{},"    builder.Configuration.GetSection(\"Smtp\"));\n",[44,175,177],{"class":46,"line":176},13,[44,178,156],{"emptyLinePlaceholder":155},[44,180,182],{"class":46,"line":181},14,[44,183,184],{},"\u002F\u002F Injection — strongly typed, testable, no magic strings:\n",[44,186,188],{"class":46,"line":187},15,[44,189,56],{},[44,191,193],{"class":46,"line":192},16,[44,194,62],{},[44,196,198],{"class":46,"line":197},17,[44,199,200],{},"    private readonly SmtpSettings _settings;\n",[44,202,204],{"class":46,"line":203},18,[44,205,156],{"emptyLinePlaceholder":155},[44,207,209],{"class":46,"line":208},19,[44,210,211],{},"    public EmailService(IOptions\u003CSmtpSettings> options)\n",[44,213,215],{"class":46,"line":214},20,[44,216,217],{},"        => _settings = options.Value;\n",[44,219,221],{"class":46,"line":220},21,[44,222,104],{},[10,224,226],{"id":225},"the-three-options-interfaces","The three options interfaces",[15,228,229],{},"This is the question interviewers ask most often about the options pattern.",[231,232,234],"h3",{"id":233},"ioptionst-singleton-frozen-at-startup","IOptions\u003CT> — Singleton, frozen at startup",[35,236,238],{"className":37,"code":237,"language":39,"meta":40,"style":40},"\u002F\u002F Registered as Singleton; reads config once at startup; never reloads:\npublic class PaymentService\n{\n    public PaymentService(IOptions\u003CStripeSettings> options)\n    {\n        var settings = options.Value; \u002F\u002F same object for entire app lifetime\n    }\n}\n",[19,239,240,245,250,254,259,263,268,272],{"__ignoreMap":40},[44,241,242],{"class":46,"line":47},[44,243,244],{},"\u002F\u002F Registered as Singleton; reads config once at startup; never reloads:\n",[44,246,247],{"class":46,"line":53},[44,248,249],{},"public class PaymentService\n",[44,251,252],{"class":46,"line":59},[44,253,62],{},[44,255,256],{"class":46,"line":65},[44,257,258],{},"    public PaymentService(IOptions\u003CStripeSettings> options)\n",[44,260,261],{"class":46,"line":71},[44,262,74],{},[44,264,265],{"class":46,"line":77},[44,266,267],{},"        var settings = options.Value; \u002F\u002F same object for entire app lifetime\n",[44,269,270],{"class":46,"line":83},[44,271,98],{},[44,273,274],{"class":46,"line":89},[44,275,104],{},[15,277,278,279,281],{},"Use ",[19,280,25],{}," for configuration that never changes after startup (connection strings,\nAPI endpoints, feature flags set at deploy time). It can be safely injected into Singleton,\nScoped, or Transient services.",[231,283,285],{"id":284},"ioptionssnapshott-scoped-re-reads-per-request","IOptionsSnapshot\u003CT> — Scoped, re-reads per request",[35,287,289],{"className":37,"code":288,"language":39,"meta":40,"style":40},"\u002F\u002F New snapshot per HTTP request — reflects file changes between requests:\npublic class PricingService\n{\n    public PricingService(IOptionsSnapshot\u003CPricingSettings> options)\n    {\n        var settings = options.Value; \u002F\u002F fresh per request\n    }\n}\n",[19,290,291,296,301,305,310,314,319,323],{"__ignoreMap":40},[44,292,293],{"class":46,"line":47},[44,294,295],{},"\u002F\u002F New snapshot per HTTP request — reflects file changes between requests:\n",[44,297,298],{"class":46,"line":53},[44,299,300],{},"public class PricingService\n",[44,302,303],{"class":46,"line":59},[44,304,62],{},[44,306,307],{"class":46,"line":65},[44,308,309],{},"    public PricingService(IOptionsSnapshot\u003CPricingSettings> options)\n",[44,311,312],{"class":46,"line":71},[44,313,74],{},[44,315,316],{"class":46,"line":77},[44,317,318],{},"        var settings = options.Value; \u002F\u002F fresh per request\n",[44,320,321],{"class":46,"line":83},[44,322,98],{},[44,324,325],{"class":46,"line":89},[44,326,104],{},[15,328,278,329,332,333,337],{},[19,330,331],{},"IOptionsSnapshot\u003CT>"," when config can change at runtime and you need each request to see\nthe current values. ",[334,335,336],"strong",{},"Cannot"," be injected into Singleton services — it's Scoped.",[231,339,341],{"id":340},"ioptionsmonitort-singleton-with-change-callbacks","IOptionsMonitor\u003CT> — Singleton, with change callbacks",[35,343,345],{"className":37,"code":344,"language":39,"meta":40,"style":40},"\u002F\u002F Singleton; provides CurrentValue and fires OnChange when config is reloaded:\npublic class FeatureFlagService\n{\n    private FeatureFlags _current;\n    private readonly IDisposable? _registration;\n\n    public FeatureFlagService(IOptionsMonitor\u003CFeatureFlags> monitor)\n    {\n        _current = monitor.CurrentValue;\n        _registration = monitor.OnChange(updated =>\n        {\n            _current = updated;\n            Console.WriteLine($\"Feature flags updated at {DateTime.UtcNow:O}\");\n        });\n    }\n}\n",[19,346,347,352,357,361,366,371,375,380,384,389,394,399,404,409,414,418],{"__ignoreMap":40},[44,348,349],{"class":46,"line":47},[44,350,351],{},"\u002F\u002F Singleton; provides CurrentValue and fires OnChange when config is reloaded:\n",[44,353,354],{"class":46,"line":53},[44,355,356],{},"public class FeatureFlagService\n",[44,358,359],{"class":46,"line":59},[44,360,62],{},[44,362,363],{"class":46,"line":65},[44,364,365],{},"    private FeatureFlags _current;\n",[44,367,368],{"class":46,"line":71},[44,369,370],{},"    private readonly IDisposable? _registration;\n",[44,372,373],{"class":46,"line":77},[44,374,156],{"emptyLinePlaceholder":155},[44,376,377],{"class":46,"line":83},[44,378,379],{},"    public FeatureFlagService(IOptionsMonitor\u003CFeatureFlags> monitor)\n",[44,381,382],{"class":46,"line":89},[44,383,74],{},[44,385,386],{"class":46,"line":95},[44,387,388],{},"        _current = monitor.CurrentValue;\n",[44,390,391],{"class":46,"line":101},[44,392,393],{},"        _registration = monitor.OnChange(updated =>\n",[44,395,396],{"class":46,"line":164},[44,397,398],{},"        {\n",[44,400,401],{"class":46,"line":170},[44,402,403],{},"            _current = updated;\n",[44,405,406],{"class":46,"line":176},[44,407,408],{},"            Console.WriteLine($\"Feature flags updated at {DateTime.UtcNow:O}\");\n",[44,410,411],{"class":46,"line":181},[44,412,413],{},"        });\n",[44,415,416],{"class":46,"line":187},[44,417,98],{},[44,419,420],{"class":46,"line":192},[44,421,104],{},[15,423,278,424,427],{},[19,425,426],{},"IOptionsMonitor\u003CT>"," for Singleton services that need to react to config changes without\nrestarting the app.",[231,429,431],{"id":430},"quick-comparison","Quick comparison",[433,434,435,454],"table",{},[436,437,438],"thead",{},[439,440,441,445,448,451],"tr",{},[442,443,444],"th",{},"Interface",[442,446,447],{},"Lifetime",[442,449,450],{},"Reloads",[442,452,453],{},"Use when",[455,456,457,473,488],"tbody",{},[439,458,459,464,467,470],{},[460,461,462],"td",{},[19,463,25],{},[460,465,466],{},"Singleton",[460,468,469],{},"Never",[460,471,472],{},"Static config; inject into any lifetime",[439,474,475,479,482,485],{},[460,476,477],{},[19,478,331],{},[460,480,481],{},"Scoped",[460,483,484],{},"Per request",[460,486,487],{},"Per-request fresh values; not in singletons",[439,489,490,494,496,499],{},[460,491,492],{},[19,493,426],{},[460,495,466],{},[460,497,498],{},"On change + callback",[460,500,501],{},"Live reloads in singletons",[10,503,505],{"id":504},"binding-from-configuration","Binding from configuration",[15,507,508],{},"Three equivalent patterns:",[35,510,512],{"className":37,"code":511,"language":39,"meta":40,"style":40},"\u002F\u002F Pattern 1 — Configure (most common):\nbuilder.Services.Configure\u003CSmtpSettings>(\n    builder.Configuration.GetSection(\"Smtp\"));\n\n\u002F\u002F Pattern 2 — BindConfiguration (AddOptions builder style):\nbuilder.Services.AddOptions\u003CSmtpSettings>()\n    .BindConfiguration(\"Smtp\");\n\n\u002F\u002F Pattern 3 — Bind to an instance (inject as a concrete type):\nvar settings = new SmtpSettings();\nbuilder.Configuration.GetSection(\"Smtp\").Bind(settings);\nbuilder.Services.AddSingleton(settings);\n",[19,513,514,519,523,527,531,536,541,546,550,555,560,565],{"__ignoreMap":40},[44,515,516],{"class":46,"line":47},[44,517,518],{},"\u002F\u002F Pattern 1 — Configure (most common):\n",[44,520,521],{"class":46,"line":53},[44,522,167],{},[44,524,525],{"class":46,"line":59},[44,526,173],{},[44,528,529],{"class":46,"line":65},[44,530,156],{"emptyLinePlaceholder":155},[44,532,533],{"class":46,"line":71},[44,534,535],{},"\u002F\u002F Pattern 2 — BindConfiguration (AddOptions builder style):\n",[44,537,538],{"class":46,"line":77},[44,539,540],{},"builder.Services.AddOptions\u003CSmtpSettings>()\n",[44,542,543],{"class":46,"line":83},[44,544,545],{},"    .BindConfiguration(\"Smtp\");\n",[44,547,548],{"class":46,"line":89},[44,549,156],{"emptyLinePlaceholder":155},[44,551,552],{"class":46,"line":95},[44,553,554],{},"\u002F\u002F Pattern 3 — Bind to an instance (inject as a concrete type):\n",[44,556,557],{"class":46,"line":101},[44,558,559],{},"var settings = new SmtpSettings();\n",[44,561,562],{"class":46,"line":164},[44,563,564],{},"builder.Configuration.GetSection(\"Smtp\").Bind(settings);\n",[44,566,567],{"class":46,"line":170},[44,568,569],{},"builder.Services.AddSingleton(settings);\n",[15,571,572],{},"Pattern 2 (AddOptions builder) is preferred when you also need validation — it chains cleanly.",[10,574,576],{"id":575},"validation-at-startup","Validation at startup",[15,578,579,580,583,584,587],{},"Options validation catches misconfigured environments immediately. Without ",[19,581,582],{},"ValidateOnStart()",",\nvalidation only runs when ",[19,585,586],{},"options.Value"," is first accessed — potentially on the first\nproduction request.",[35,589,591],{"className":37,"code":590,"language":39,"meta":40,"style":40},"\u002F\u002F Data Annotations — simplest approach:\npublic class SmtpSettings\n{\n    [Required]\n    public string Host { get; set; } = \"\";\n\n    [Range(1, 65535)]\n    public int Port { get; set; } = 587;\n\n    [Required, MinLength(20)]\n    public string ApiKey { get; set; } = \"\";\n}\n\nbuilder.Services.AddOptions\u003CSmtpSettings>()\n    .BindConfiguration(\"Smtp\")\n    .ValidateDataAnnotations()\n    .ValidateOnStart(); \u002F\u002F throw at startup, not on first request\n\n\u002F\u002F Custom cross-property validation via IValidateOptions\u003CT>:\npublic class SmtpSettingsValidator : IValidateOptions\u003CSmtpSettings>\n{\n    public ValidateOptionsResult Validate(string? name, SmtpSettings opts)\n    {\n        if (opts.UseTls && opts.Port == 25)\n            return ValidateOptionsResult.Fail(\n                \"TLS enabled but port 25 is for unencrypted SMTP. Use 587 or 465.\");\n\n        return ValidateOptionsResult.Success;\n    }\n}\n\nbuilder.Services.AddSingleton\u003CIValidateOptions\u003CSmtpSettings>, SmtpSettingsValidator>();\n\n\u002F\u002F Inline delegate validation:\nbuilder.Services.AddOptions\u003CSmtpSettings>()\n    .BindConfiguration(\"Smtp\")\n    .Validate(s => s.Host.Contains('.'), \"Smtp:Host must be a valid hostname\")\n    .ValidateOnStart();\n",[19,592,593,598,602,606,611,616,620,625,630,634,639,643,647,651,655,660,665,670,674,679,684,688,694,699,705,711,717,722,728,733,738,743,749,754,760,765,770,776],{"__ignoreMap":40},[44,594,595],{"class":46,"line":47},[44,596,597],{},"\u002F\u002F Data Annotations — simplest approach:\n",[44,599,600],{"class":46,"line":53},[44,601,122],{},[44,603,604],{"class":46,"line":59},[44,605,62],{},[44,607,608],{"class":46,"line":65},[44,609,610],{},"    [Required]\n",[44,612,613],{"class":46,"line":71},[44,614,615],{},"    public string Host { get; set; } = \"\";\n",[44,617,618],{"class":46,"line":77},[44,619,156],{"emptyLinePlaceholder":155},[44,621,622],{"class":46,"line":83},[44,623,624],{},"    [Range(1, 65535)]\n",[44,626,627],{"class":46,"line":89},[44,628,629],{},"    public int Port { get; set; } = 587;\n",[44,631,632],{"class":46,"line":95},[44,633,156],{"emptyLinePlaceholder":155},[44,635,636],{"class":46,"line":101},[44,637,638],{},"    [Required, MinLength(20)]\n",[44,640,641],{"class":46,"line":164},[44,642,146],{},[44,644,645],{"class":46,"line":170},[44,646,104],{},[44,648,649],{"class":46,"line":176},[44,650,156],{"emptyLinePlaceholder":155},[44,652,653],{"class":46,"line":181},[44,654,540],{},[44,656,657],{"class":46,"line":187},[44,658,659],{},"    .BindConfiguration(\"Smtp\")\n",[44,661,662],{"class":46,"line":192},[44,663,664],{},"    .ValidateDataAnnotations()\n",[44,666,667],{"class":46,"line":197},[44,668,669],{},"    .ValidateOnStart(); \u002F\u002F throw at startup, not on first request\n",[44,671,672],{"class":46,"line":203},[44,673,156],{"emptyLinePlaceholder":155},[44,675,676],{"class":46,"line":208},[44,677,678],{},"\u002F\u002F Custom cross-property validation via IValidateOptions\u003CT>:\n",[44,680,681],{"class":46,"line":214},[44,682,683],{},"public class SmtpSettingsValidator : IValidateOptions\u003CSmtpSettings>\n",[44,685,686],{"class":46,"line":220},[44,687,62],{},[44,689,691],{"class":46,"line":690},22,[44,692,693],{},"    public ValidateOptionsResult Validate(string? name, SmtpSettings opts)\n",[44,695,697],{"class":46,"line":696},23,[44,698,74],{},[44,700,702],{"class":46,"line":701},24,[44,703,704],{},"        if (opts.UseTls && opts.Port == 25)\n",[44,706,708],{"class":46,"line":707},25,[44,709,710],{},"            return ValidateOptionsResult.Fail(\n",[44,712,714],{"class":46,"line":713},26,[44,715,716],{},"                \"TLS enabled but port 25 is for unencrypted SMTP. Use 587 or 465.\");\n",[44,718,720],{"class":46,"line":719},27,[44,721,156],{"emptyLinePlaceholder":155},[44,723,725],{"class":46,"line":724},28,[44,726,727],{},"        return ValidateOptionsResult.Success;\n",[44,729,731],{"class":46,"line":730},29,[44,732,98],{},[44,734,736],{"class":46,"line":735},30,[44,737,104],{},[44,739,741],{"class":46,"line":740},31,[44,742,156],{"emptyLinePlaceholder":155},[44,744,746],{"class":46,"line":745},32,[44,747,748],{},"builder.Services.AddSingleton\u003CIValidateOptions\u003CSmtpSettings>, SmtpSettingsValidator>();\n",[44,750,752],{"class":46,"line":751},33,[44,753,156],{"emptyLinePlaceholder":155},[44,755,757],{"class":46,"line":756},34,[44,758,759],{},"\u002F\u002F Inline delegate validation:\n",[44,761,763],{"class":46,"line":762},35,[44,764,540],{},[44,766,768],{"class":46,"line":767},36,[44,769,659],{},[44,771,773],{"class":46,"line":772},37,[44,774,775],{},"    .Validate(s => s.Host.Contains('.'), \"Smtp:Host must be a valid hostname\")\n",[44,777,779],{"class":46,"line":778},38,[44,780,781],{},"    .ValidateOnStart();\n",[15,783,784,785,787],{},"Always pair validation with ",[19,786,582],{}," — fail fast at startup, not under load.",[10,789,791],{"id":790},"configure-vs-postconfigure","Configure vs PostConfigure",[15,793,794,797,798,801,802,804],{},[19,795,796],{},"Configure\u003CT>"," sets values. ",[19,799,800],{},"PostConfigure\u003CT>"," runs after all ",[19,803,796],{}," calls and wins:",[35,806,808],{"className":37,"code":807,"language":39,"meta":40,"style":40},"\u002F\u002F App configuration from appsettings.json:\nbuilder.Services.Configure\u003CCacheSettings>(builder.Configuration.GetSection(\"Cache\"));\n\u002F\u002F MaxSize = 500 from appsettings\n\n\u002F\u002F Library code enforces a hard cap regardless of what app configured:\nbuilder.Services.PostConfigure\u003CCacheSettings>(settings =>\n{\n    if (settings.MaxSize > 10_000)\n        settings.MaxSize = 10_000; \u002F\u002F enforced regardless of appsettings\n\n    \u002F\u002F Ensure derived values stay consistent:\n    settings.EvictionBatchSize = Math.Min(\n        settings.EvictionBatchSize, settings.MaxSize \u002F 10);\n});\n\n\u002F\u002F Apply PostConfigure to ALL named instances of a type:\nbuilder.Services.PostConfigureAll\u003CApiClientSettings>(settings =>\n{\n    if (!settings.BaseUrl.EndsWith('\u002F'))\n        settings.BaseUrl += '\u002F';\n});\n",[19,809,810,815,820,825,829,834,839,843,848,853,857,862,867,872,877,881,886,891,895,900,905],{"__ignoreMap":40},[44,811,812],{"class":46,"line":47},[44,813,814],{},"\u002F\u002F App configuration from appsettings.json:\n",[44,816,817],{"class":46,"line":53},[44,818,819],{},"builder.Services.Configure\u003CCacheSettings>(builder.Configuration.GetSection(\"Cache\"));\n",[44,821,822],{"class":46,"line":59},[44,823,824],{},"\u002F\u002F MaxSize = 500 from appsettings\n",[44,826,827],{"class":46,"line":65},[44,828,156],{"emptyLinePlaceholder":155},[44,830,831],{"class":46,"line":71},[44,832,833],{},"\u002F\u002F Library code enforces a hard cap regardless of what app configured:\n",[44,835,836],{"class":46,"line":77},[44,837,838],{},"builder.Services.PostConfigure\u003CCacheSettings>(settings =>\n",[44,840,841],{"class":46,"line":83},[44,842,62],{},[44,844,845],{"class":46,"line":89},[44,846,847],{},"    if (settings.MaxSize > 10_000)\n",[44,849,850],{"class":46,"line":95},[44,851,852],{},"        settings.MaxSize = 10_000; \u002F\u002F enforced regardless of appsettings\n",[44,854,855],{"class":46,"line":101},[44,856,156],{"emptyLinePlaceholder":155},[44,858,859],{"class":46,"line":164},[44,860,861],{},"    \u002F\u002F Ensure derived values stay consistent:\n",[44,863,864],{"class":46,"line":170},[44,865,866],{},"    settings.EvictionBatchSize = Math.Min(\n",[44,868,869],{"class":46,"line":176},[44,870,871],{},"        settings.EvictionBatchSize, settings.MaxSize \u002F 10);\n",[44,873,874],{"class":46,"line":181},[44,875,876],{},"});\n",[44,878,879],{"class":46,"line":187},[44,880,156],{"emptyLinePlaceholder":155},[44,882,883],{"class":46,"line":192},[44,884,885],{},"\u002F\u002F Apply PostConfigure to ALL named instances of a type:\n",[44,887,888],{"class":46,"line":197},[44,889,890],{},"builder.Services.PostConfigureAll\u003CApiClientSettings>(settings =>\n",[44,892,893],{"class":46,"line":203},[44,894,62],{},[44,896,897],{"class":46,"line":208},[44,898,899],{},"    if (!settings.BaseUrl.EndsWith('\u002F'))\n",[44,901,902],{"class":46,"line":214},[44,903,904],{},"        settings.BaseUrl += '\u002F';\n",[44,906,907],{"class":46,"line":220},[44,908,876],{},[15,910,911,914,915,917,918,920],{},[334,912,913],{},"Rule:"," Use ",[19,916,796],{}," in application code. Use ",[19,919,800],{}," in library\u002Fframework\ncode to enforce invariants that must hold regardless of what the consuming app configured.",[10,922,924],{"id":923},"named-options","Named options",[15,926,927],{},"When the same settings shape covers multiple logical configurations:",[35,929,931],{"className":37,"code":930,"language":39,"meta":40,"style":40},"\u002F\u002F appsettings.json:\n\u002F\u002F {\n\u002F\u002F \"Stripe\": { \"BaseUrl\": \"https:\u002F\u002Fapi.stripe.com\", \"ApiKey\": \"sk_live_...\" },\n\u002F\u002F \"Sendgrid\": { \"BaseUrl\": \"https:\u002F\u002Fapi.sendgrid.com\", \"ApiKey\": \"SG.xxx\" }\n\u002F\u002F }\n\npublic class ApiClientSettings\n{\n    public string BaseUrl   { get; set; } = \"\";\n    public string ApiKey    { get; set; } = \"\";\n    public int    TimeoutMs { get; set; } = 5000;\n}\n\nbuilder.Services.Configure\u003CApiClientSettings>(\"Stripe\",\n    builder.Configuration.GetSection(\"Stripe\"));\nbuilder.Services.Configure\u003CApiClientSettings>(\"Sendgrid\",\n    builder.Configuration.GetSection(\"Sendgrid\"));\n\n\u002F\u002F Resolve by name:\npublic class PaymentService\n{\n    public PaymentService(IOptionsSnapshot\u003CApiClientSettings> options)\n    {\n        var stripe   = options.Get(\"Stripe\");   \u002F\u002F or IOptionsMonitor.Get(\"Stripe\")\n        var sendgrid = options.Get(\"Sendgrid\");\n    }\n}\n\n\u002F\u002F IOptions\u003CT>.Value always returns the unnamed (default) instance:\n\u002F\u002F options.Value == options.Get(Options.DefaultName) == options.Get(\"\")\n",[19,932,933,938,943,948,953,958,962,967,971,976,981,986,990,994,999,1004,1009,1014,1018,1023,1027,1031,1036,1040,1045,1050,1054,1058,1062,1067],{"__ignoreMap":40},[44,934,935],{"class":46,"line":47},[44,936,937],{},"\u002F\u002F appsettings.json:\n",[44,939,940],{"class":46,"line":53},[44,941,942],{},"\u002F\u002F {\n",[44,944,945],{"class":46,"line":59},[44,946,947],{},"\u002F\u002F \"Stripe\": { \"BaseUrl\": \"https:\u002F\u002Fapi.stripe.com\", \"ApiKey\": \"sk_live_...\" },\n",[44,949,950],{"class":46,"line":65},[44,951,952],{},"\u002F\u002F \"Sendgrid\": { \"BaseUrl\": \"https:\u002F\u002Fapi.sendgrid.com\", \"ApiKey\": \"SG.xxx\" }\n",[44,954,955],{"class":46,"line":71},[44,956,957],{},"\u002F\u002F }\n",[44,959,960],{"class":46,"line":77},[44,961,156],{"emptyLinePlaceholder":155},[44,963,964],{"class":46,"line":83},[44,965,966],{},"public class ApiClientSettings\n",[44,968,969],{"class":46,"line":89},[44,970,62],{},[44,972,973],{"class":46,"line":95},[44,974,975],{},"    public string BaseUrl   { get; set; } = \"\";\n",[44,977,978],{"class":46,"line":101},[44,979,980],{},"    public string ApiKey    { get; set; } = \"\";\n",[44,982,983],{"class":46,"line":164},[44,984,985],{},"    public int    TimeoutMs { get; set; } = 5000;\n",[44,987,988],{"class":46,"line":170},[44,989,104],{},[44,991,992],{"class":46,"line":176},[44,993,156],{"emptyLinePlaceholder":155},[44,995,996],{"class":46,"line":181},[44,997,998],{},"builder.Services.Configure\u003CApiClientSettings>(\"Stripe\",\n",[44,1000,1001],{"class":46,"line":187},[44,1002,1003],{},"    builder.Configuration.GetSection(\"Stripe\"));\n",[44,1005,1006],{"class":46,"line":192},[44,1007,1008],{},"builder.Services.Configure\u003CApiClientSettings>(\"Sendgrid\",\n",[44,1010,1011],{"class":46,"line":197},[44,1012,1013],{},"    builder.Configuration.GetSection(\"Sendgrid\"));\n",[44,1015,1016],{"class":46,"line":203},[44,1017,156],{"emptyLinePlaceholder":155},[44,1019,1020],{"class":46,"line":208},[44,1021,1022],{},"\u002F\u002F Resolve by name:\n",[44,1024,1025],{"class":46,"line":214},[44,1026,249],{},[44,1028,1029],{"class":46,"line":220},[44,1030,62],{},[44,1032,1033],{"class":46,"line":690},[44,1034,1035],{},"    public PaymentService(IOptionsSnapshot\u003CApiClientSettings> options)\n",[44,1037,1038],{"class":46,"line":696},[44,1039,74],{},[44,1041,1042],{"class":46,"line":701},[44,1043,1044],{},"        var stripe   = options.Get(\"Stripe\");   \u002F\u002F or IOptionsMonitor.Get(\"Stripe\")\n",[44,1046,1047],{"class":46,"line":707},[44,1048,1049],{},"        var sendgrid = options.Get(\"Sendgrid\");\n",[44,1051,1052],{"class":46,"line":713},[44,1053,98],{},[44,1055,1056],{"class":46,"line":719},[44,1057,104],{},[44,1059,1060],{"class":46,"line":724},[44,1061,156],{"emptyLinePlaceholder":155},[44,1063,1064],{"class":46,"line":730},[44,1065,1066],{},"\u002F\u002F IOptions\u003CT>.Value always returns the unnamed (default) instance:\n",[44,1068,1069],{"class":46,"line":735},[44,1070,1071],{},"\u002F\u002F options.Value == options.Get(Options.DefaultName) == options.Get(\"\")\n",[10,1073,1075],{"id":1074},"testing-options-without-a-config-file","Testing options without a config file",[15,1077,1078,1079,1082],{},"No container or ",[19,1080,1081],{},"appsettings.json"," needed in unit tests:",[35,1084,1086],{"className":37,"code":1085,"language":39,"meta":40,"style":40},"\u002F\u002F Options.Create wraps an in-memory instance as IOptions\u003CT>:\nvar sut = new EmailService(Options.Create(new SmtpSettings\n{\n    Host   = \"smtp.test.com\",\n    Port   = 587,\n    ApiKey = \"test-key\"\n}));\n\n\u002F\u002F For IOptionsSnapshot\u003CT>, use a mock or TestOptionsManager:\nvar snapshot = Substitute.For\u003CIOptionsSnapshot\u003CSmtpSettings>>();\nsnapshot.Value.Returns(new SmtpSettings { Host = \"smtp.test.com\" });\nsnapshot.Get(\"Primary\").Returns(new SmtpSettings { Host = \"primary.smtp.com\" });\n",[19,1087,1088,1093,1098,1102,1107,1112,1117,1122,1126,1131,1136,1141],{"__ignoreMap":40},[44,1089,1090],{"class":46,"line":47},[44,1091,1092],{},"\u002F\u002F Options.Create wraps an in-memory instance as IOptions\u003CT>:\n",[44,1094,1095],{"class":46,"line":53},[44,1096,1097],{},"var sut = new EmailService(Options.Create(new SmtpSettings\n",[44,1099,1100],{"class":46,"line":59},[44,1101,62],{},[44,1103,1104],{"class":46,"line":65},[44,1105,1106],{},"    Host   = \"smtp.test.com\",\n",[44,1108,1109],{"class":46,"line":71},[44,1110,1111],{},"    Port   = 587,\n",[44,1113,1114],{"class":46,"line":77},[44,1115,1116],{},"    ApiKey = \"test-key\"\n",[44,1118,1119],{"class":46,"line":83},[44,1120,1121],{},"}));\n",[44,1123,1124],{"class":46,"line":89},[44,1125,156],{"emptyLinePlaceholder":155},[44,1127,1128],{"class":46,"line":95},[44,1129,1130],{},"\u002F\u002F For IOptionsSnapshot\u003CT>, use a mock or TestOptionsManager:\n",[44,1132,1133],{"class":46,"line":101},[44,1134,1135],{},"var snapshot = Substitute.For\u003CIOptionsSnapshot\u003CSmtpSettings>>();\n",[44,1137,1138],{"class":46,"line":164},[44,1139,1140],{},"snapshot.Value.Returns(new SmtpSettings { Host = \"smtp.test.com\" });\n",[44,1142,1143],{"class":46,"line":170},[44,1144,1145],{},"snapshot.Get(\"Primary\").Returns(new SmtpSettings { Host = \"primary.smtp.com\" });\n",[15,1147,1148,1151],{},[19,1149,1150],{},"Options.Create(new T { ... })"," is the fastest path to a testable options injection.",[10,1153,1155],{"id":1154},"the-addoptions-builder-the-preferred-registration-style","The AddOptions builder — the preferred registration style",[35,1157,1159],{"className":37,"code":1158,"language":39,"meta":40,"style":40},"builder.Services\n    .AddOptions\u003CJwtSettings>()\n    .BindConfiguration(\"Jwt\")\n    .Validate(s => s.Secret.Length >= 32,\n        \"Jwt:Secret must be at least 32 characters\")\n    .Validate(s => Uri.TryCreate(s.Issuer, UriKind.Absolute, out _),\n        \"Jwt:Issuer must be a valid absolute URI\")\n    .ValidateDataAnnotations()\n    .ValidateOnStart()\n    .PostConfigure(s => s.Issuer = s.Issuer.TrimEnd('\u002F').ToLower());\n",[19,1160,1161,1166,1171,1176,1181,1186,1191,1196,1200,1205],{"__ignoreMap":40},[44,1162,1163],{"class":46,"line":47},[44,1164,1165],{},"builder.Services\n",[44,1167,1168],{"class":46,"line":53},[44,1169,1170],{},"    .AddOptions\u003CJwtSettings>()\n",[44,1172,1173],{"class":46,"line":59},[44,1174,1175],{},"    .BindConfiguration(\"Jwt\")\n",[44,1177,1178],{"class":46,"line":65},[44,1179,1180],{},"    .Validate(s => s.Secret.Length >= 32,\n",[44,1182,1183],{"class":46,"line":71},[44,1184,1185],{},"        \"Jwt:Secret must be at least 32 characters\")\n",[44,1187,1188],{"class":46,"line":77},[44,1189,1190],{},"    .Validate(s => Uri.TryCreate(s.Issuer, UriKind.Absolute, out _),\n",[44,1192,1193],{"class":46,"line":83},[44,1194,1195],{},"        \"Jwt:Issuer must be a valid absolute URI\")\n",[44,1197,1198],{"class":46,"line":89},[44,1199,664],{},[44,1201,1202],{"class":46,"line":95},[44,1203,1204],{},"    .ValidateOnStart()\n",[44,1206,1207],{"class":46,"line":101},[44,1208,1209],{},"    .PostConfigure(s => s.Issuer = s.Issuer.TrimEnd('\u002F').ToLower());\n",[15,1211,1212,1213,1216,1217,1220],{},"One chain replaces scattered ",[19,1214,1215],{},"Configure"," + validator registration + ",[19,1218,1219],{},"PostConfigure"," calls and\nmakes the full intent visible in one place.",[10,1222,1224],{"id":1223},"recap","Recap",[15,1226,1227,1228,1230,1231,1233,1234,1236,1237,1239,1240,1242,1243,1245,1246,1248],{},"The options pattern replaces raw ",[19,1229,21],{}," access with strongly typed, injectable,\nvalidated settings objects. Use ",[19,1232,25],{}," for static config (any lifetime), ",[19,1235,331],{},"\nfor per-request fresh values (Scoped only), and ",[19,1238,426],{}," for reactive live reloads\nin singletons. Always add ",[19,1241,582],{}," so misconfigured settings fail at startup.\nUse ",[19,1244,800],{}," for invariants that must hold regardless of what application code\nconfigured. Test with ",[19,1247,1150],{}," — no container required.",[1250,1251,1252],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":40,"searchDepth":53,"depth":53,"links":1254},[1255,1256,1257,1263,1264,1265,1266,1267,1268,1269],{"id":12,"depth":53,"text":13},{"id":29,"depth":53,"text":30},{"id":225,"depth":53,"text":226,"children":1258},[1259,1260,1261,1262],{"id":233,"depth":59,"text":234},{"id":284,"depth":59,"text":285},{"id":340,"depth":59,"text":341},{"id":430,"depth":59,"text":431},{"id":504,"depth":53,"text":505},{"id":575,"depth":53,"text":576},{"id":790,"depth":53,"text":791},{"id":923,"depth":53,"text":924},{"id":1074,"depth":53,"text":1075},{"id":1154,"depth":53,"text":1155},{"id":1223,"depth":53,"text":1224},"How the .NET Options pattern works — binding configuration to typed classes, picking between IOptions, IOptionsSnapshot, and IOptionsMonitor, and validating settings before the app starts.","medium","md",".NET Core","dotnet",{},"\u002Fblog\u002Fdotnet-options-pattern","\u002Fdotnet\u002Fdependency-injection\u002Foptions-pattern",{"title":5,"description":1270},"blog\u002Fdotnet-options-pattern","Options Pattern","Dependency Injection","dependency-injection","2026-06-23","JH90S_ocYPhh40SfyTQ7hbG0jbPXBWUd34BcHGrs17s",1782244086534]