[{"data":1,"prerenderedAt":1097},["ShallowReactive",2],{"blog-\u002Fblog\u002Fdotnet-service-lifetimes":3},{"id":4,"title":5,"body":6,"description":1082,"difficulty":1083,"extension":1084,"framework":1085,"frameworkSlug":1086,"meta":1087,"navigation":51,"order":42,"path":1088,"qaPath":1089,"seo":1090,"stem":1091,"subtopic":1092,"topic":1093,"topicSlug":1094,"updated":1095,"__hash__":1096},"blog\u002Fblog\u002Fdotnet-service-lifetimes.md","Service Lifetimes in .NET Core DI",{"type":7,"value":8,"toc":1069},"minimark",[9,14,18,22,82,150,154,162,275,293,297,306,401,408,412,415,466,469,473,480,540,543,617,620,624,637,758,763,767,786,880,884,918,933,936,992,996,1003,1042,1046,1065],[10,11,13],"h2",{"id":12},"why-lifetime-bugs-are-the-hardest-di-bugs-to-catch","Why lifetime bugs are the hardest DI bugs to catch",[15,16,17],"p",{},"Lifetime mismatches in .NET Core DI are insidious. The app starts fine, unit tests pass, and\nthe bug only surfaces under concurrent load when a shared DbContext corrupts EF's change\ntracker — or a stale config value from a Singleton serves wrong prices to every customer.\nThis article explains the three lifetimes, when each is right, and how to detect mismatches\nbefore they reach production.",[10,19,21],{"id":20},"the-three-lifetimes-at-a-glance","The three lifetimes at a glance",[23,24,29],"pre",{"className":25,"code":26,"language":27,"meta":28,"style":28},"language-csharp shiki shiki-themes github-light github-dark","\u002F\u002F Singleton — one instance for the entire app lifetime:\nbuilder.Services.AddSingleton\u003CIMemoryCache, MemoryCache>();\n\n\u002F\u002F Scoped — one instance per HTTP request (or per explicit scope):\nbuilder.Services.AddScoped\u003CAppDbContext>();\n\n\u002F\u002F Transient — new instance on every resolve:\nbuilder.Services.AddTransient\u003CIEmailValidator, RegexEmailValidator>();\n","csharp","",[30,31,32,40,46,53,59,65,70,76],"code",{"__ignoreMap":28},[33,34,37],"span",{"class":35,"line":36},"line",1,[33,38,39],{},"\u002F\u002F Singleton — one instance for the entire app lifetime:\n",[33,41,43],{"class":35,"line":42},2,[33,44,45],{},"builder.Services.AddSingleton\u003CIMemoryCache, MemoryCache>();\n",[33,47,49],{"class":35,"line":48},3,[33,50,52],{"emptyLinePlaceholder":51},true,"\n",[33,54,56],{"class":35,"line":55},4,[33,57,58],{},"\u002F\u002F Scoped — one instance per HTTP request (or per explicit scope):\n",[33,60,62],{"class":35,"line":61},5,[33,63,64],{},"builder.Services.AddScoped\u003CAppDbContext>();\n",[33,66,68],{"class":35,"line":67},6,[33,69,52],{"emptyLinePlaceholder":51},[33,71,73],{"class":35,"line":72},7,[33,74,75],{},"\u002F\u002F Transient — new instance on every resolve:\n",[33,77,79],{"class":35,"line":78},8,[33,80,81],{},"builder.Services.AddTransient\u003CIEmailValidator, RegexEmailValidator>();\n",[83,84,85,104],"table",{},[86,87,88],"thead",{},[89,90,91,95,98,101],"tr",{},[92,93,94],"th",{},"Lifetime",[92,96,97],{},"Created",[92,99,100],{},"Disposed",[92,102,103],{},"Thread-safe required?",[105,106,107,122,136],"tbody",{},[89,108,109,113,116,119],{},[110,111,112],"td",{},"Singleton",[110,114,115],{},"App start",[110,117,118],{},"App shutdown",[110,120,121],{},"Yes",[89,123,124,127,130,133],{},[110,125,126],{},"Scoped",[110,128,129],{},"Per request",[110,131,132],{},"End of request",[110,134,135],{},"No (one request = one thread)",[89,137,138,141,144,147],{},[110,139,140],{},"Transient",[110,142,143],{},"Per resolve",[110,145,146],{},"End of scope",[110,148,149],{},"No (own instance)",[10,151,153],{"id":152},"singleton-shared-thread-safe-long-lived","Singleton — shared, thread-safe, long-lived",[15,155,156,157,161],{},"Use Singleton for services that are ",[158,159,160],"strong",{},"stateless or explicitly thread-safe",", expensive to\ncreate, or meant to be shared across all requests.",[23,163,165],{"className":25,"code":164,"language":27,"meta":28,"style":28},"\u002F\u002F Good singletons:\nbuilder.Services.AddSingleton\u003CIHttpClientFactory>(); \u002F\u002F pool management — thread-safe\nbuilder.Services.AddSingleton\u003CIMemoryCache, MemoryCache>(); \u002F\u002F thread-safe by design\nbuilder.Services.AddSingleton\u003CIConnectionMultiplexer>(sp =>\n    ConnectionMultiplexer.Connect(sp.GetRequiredService\u003CIConfiguration>()[\"Redis\"]));\n\n\u002F\u002F Thread-safe counter:\npublic class RequestCounter\n{\n    private long _count;\n    public void Increment() => Interlocked.Increment(ref _count);\n    public long Get()       => Interlocked.Read(ref _count);\n}\n\n\u002F\u002F Unsafe singleton — shared mutable state without synchronization:\npublic class OrderCache\n{\n    private List\u003COrder> _orders = new(); \u002F\u002F not thread-safe!\n    public void Add(Order o) => _orders.Add(o); \u002F\u002F race condition under concurrent requests\n}\n",[30,166,167,172,177,182,187,192,196,201,206,212,218,224,230,236,241,247,253,258,264,270],{"__ignoreMap":28},[33,168,169],{"class":35,"line":36},[33,170,171],{},"\u002F\u002F Good singletons:\n",[33,173,174],{"class":35,"line":42},[33,175,176],{},"builder.Services.AddSingleton\u003CIHttpClientFactory>(); \u002F\u002F pool management — thread-safe\n",[33,178,179],{"class":35,"line":48},[33,180,181],{},"builder.Services.AddSingleton\u003CIMemoryCache, MemoryCache>(); \u002F\u002F thread-safe by design\n",[33,183,184],{"class":35,"line":55},[33,185,186],{},"builder.Services.AddSingleton\u003CIConnectionMultiplexer>(sp =>\n",[33,188,189],{"class":35,"line":61},[33,190,191],{},"    ConnectionMultiplexer.Connect(sp.GetRequiredService\u003CIConfiguration>()[\"Redis\"]));\n",[33,193,194],{"class":35,"line":67},[33,195,52],{"emptyLinePlaceholder":51},[33,197,198],{"class":35,"line":72},[33,199,200],{},"\u002F\u002F Thread-safe counter:\n",[33,202,203],{"class":35,"line":78},[33,204,205],{},"public class RequestCounter\n",[33,207,209],{"class":35,"line":208},9,[33,210,211],{},"{\n",[33,213,215],{"class":35,"line":214},10,[33,216,217],{},"    private long _count;\n",[33,219,221],{"class":35,"line":220},11,[33,222,223],{},"    public void Increment() => Interlocked.Increment(ref _count);\n",[33,225,227],{"class":35,"line":226},12,[33,228,229],{},"    public long Get()       => Interlocked.Read(ref _count);\n",[33,231,233],{"class":35,"line":232},13,[33,234,235],{},"}\n",[33,237,239],{"class":35,"line":238},14,[33,240,52],{"emptyLinePlaceholder":51},[33,242,244],{"class":35,"line":243},15,[33,245,246],{},"\u002F\u002F Unsafe singleton — shared mutable state without synchronization:\n",[33,248,250],{"class":35,"line":249},16,[33,251,252],{},"public class OrderCache\n",[33,254,256],{"class":35,"line":255},17,[33,257,211],{},[33,259,261],{"class":35,"line":260},18,[33,262,263],{},"    private List\u003COrder> _orders = new(); \u002F\u002F not thread-safe!\n",[33,265,267],{"class":35,"line":266},19,[33,268,269],{},"    public void Add(Order o) => _orders.Add(o); \u002F\u002F race condition under concurrent requests\n",[33,271,273],{"class":35,"line":272},20,[33,274,235],{},[15,276,277,278,281,282,285,286,281,289,292],{},"If a Singleton holds mutable state, protect it with ",[30,279,280],{},"Interlocked",", ",[30,283,284],{},"lock",", or concurrent\ncollections (",[30,287,288],{},"ConcurrentDictionary",[30,290,291],{},"ConcurrentQueue",").",[10,294,296],{"id":295},"scoped-the-right-home-for-dbcontext","Scoped — the right home for DbContext",[15,298,299,301,302,305],{},[158,300,126],{}," services live for exactly one HTTP request. Two classes that both inject the same\nScoped service within the same request share the same instance — which is exactly what you\nwant for a ",[30,303,304],{},"DbContext"," unit of work.",[23,307,309],{"className":25,"code":308,"language":27,"meta":28,"style":28},"builder.Services.AddScoped\u003CAppDbContext>();\n\n\u002F\u002F Both services get the same AppDbContext within one request:\npublic class OrderService\n{\n    public OrderService(AppDbContext db) { _db = db; }\n}\n\npublic class InventoryService\n{\n    public InventoryService(AppDbContext db) { _db = db; }\n    \u002F\u002F Same instance as OrderService — shared transaction possible\n}\n\n\u002F\u002F Creating a scope manually (needed outside requests):\nusing (var scope = app.Services.CreateScope())\n{\n    var db = scope.ServiceProvider.GetRequiredService\u003CAppDbContext>();\n    await db.Database.MigrateAsync(); \u002F\u002F own connection; disposed at end of using\n}\n",[30,310,311,315,319,324,329,333,338,342,346,351,355,360,365,369,373,378,383,387,392,397],{"__ignoreMap":28},[33,312,313],{"class":35,"line":36},[33,314,64],{},[33,316,317],{"class":35,"line":42},[33,318,52],{"emptyLinePlaceholder":51},[33,320,321],{"class":35,"line":48},[33,322,323],{},"\u002F\u002F Both services get the same AppDbContext within one request:\n",[33,325,326],{"class":35,"line":55},[33,327,328],{},"public class OrderService\n",[33,330,331],{"class":35,"line":61},[33,332,211],{},[33,334,335],{"class":35,"line":67},[33,336,337],{},"    public OrderService(AppDbContext db) { _db = db; }\n",[33,339,340],{"class":35,"line":72},[33,341,235],{},[33,343,344],{"class":35,"line":78},[33,345,52],{"emptyLinePlaceholder":51},[33,347,348],{"class":35,"line":208},[33,349,350],{},"public class InventoryService\n",[33,352,353],{"class":35,"line":214},[33,354,211],{},[33,356,357],{"class":35,"line":220},[33,358,359],{},"    public InventoryService(AppDbContext db) { _db = db; }\n",[33,361,362],{"class":35,"line":226},[33,363,364],{},"    \u002F\u002F Same instance as OrderService — shared transaction possible\n",[33,366,367],{"class":35,"line":232},[33,368,235],{},[33,370,371],{"class":35,"line":238},[33,372,52],{"emptyLinePlaceholder":51},[33,374,375],{"class":35,"line":243},[33,376,377],{},"\u002F\u002F Creating a scope manually (needed outside requests):\n",[33,379,380],{"class":35,"line":249},[33,381,382],{},"using (var scope = app.Services.CreateScope())\n",[33,384,385],{"class":35,"line":255},[33,386,211],{},[33,388,389],{"class":35,"line":260},[33,390,391],{},"    var db = scope.ServiceProvider.GetRequiredService\u003CAppDbContext>();\n",[33,393,394],{"class":35,"line":266},[33,395,396],{},"    await db.Database.MigrateAsync(); \u002F\u002F own connection; disposed at end of using\n",[33,398,399],{"class":35,"line":272},[33,400,235],{},[15,402,403,404,407],{},"The \"scope\" in ASP.NET Core maps to an HTTP request. In background services, Blazor circuits,\nand gRPC calls, you create scopes explicitly with ",[30,405,406],{},"IServiceScopeFactory",".",[10,409,411],{"id":410},"transient-cheap-and-stateless","Transient — cheap and stateless",[15,413,414],{},"Transient services are created anew on every resolve. They're safe for non-thread-safe objects\nbecause each consumer gets its own instance. The trade-off is allocation cost.",[23,416,418],{"className":25,"code":417,"language":27,"meta":28,"style":28},"builder.Services.AddTransient\u003CIEmailValidator, RegexEmailValidator>();\n\n\u002F\u002F Demonstration:\npublic class SignupService\n{\n    public SignupService(IEmailValidator v1, IEmailValidator v2)\n    {\n        Console.WriteLine(ReferenceEquals(v1, v2)); \u002F\u002F false — different instances\n    }\n}\n",[30,419,420,424,428,433,438,442,447,452,457,462],{"__ignoreMap":28},[33,421,422],{"class":35,"line":36},[33,423,81],{},[33,425,426],{"class":35,"line":42},[33,427,52],{"emptyLinePlaceholder":51},[33,429,430],{"class":35,"line":48},[33,431,432],{},"\u002F\u002F Demonstration:\n",[33,434,435],{"class":35,"line":55},[33,436,437],{},"public class SignupService\n",[33,439,440],{"class":35,"line":61},[33,441,211],{},[33,443,444],{"class":35,"line":67},[33,445,446],{},"    public SignupService(IEmailValidator v1, IEmailValidator v2)\n",[33,448,449],{"class":35,"line":72},[33,450,451],{},"    {\n",[33,453,454],{"class":35,"line":78},[33,455,456],{},"        Console.WriteLine(ReferenceEquals(v1, v2)); \u002F\u002F false — different instances\n",[33,458,459],{"class":35,"line":208},[33,460,461],{},"    }\n",[33,463,464],{"class":35,"line":214},[33,465,235],{},[15,467,468],{},"Avoid Transient for expensive-to-construct services or services holding resources like\nconnections — you'll create and dispose them on every injection, multiplying overhead.",[10,470,472],{"id":471},"captive-dependencies-the-most-dangerous-lifetime-bug","Captive dependencies — the most dangerous lifetime bug",[15,474,475,476,479],{},"A ",[158,477,478],{},"captive dependency"," occurs when a longer-lived service holds a shorter-lived service\nin its constructor. The short-lived service is \"captured\" and effectively becomes long-lived.",[23,481,483],{"className":25,"code":482,"language":27,"meta":28,"style":28},"\u002F\u002F Singleton captures a Scoped service:\npublic class ProductCache  \u002F\u002F Singleton\n{\n    private readonly AppDbContext _db; \u002F\u002F AppDbContext is Scoped\n\n    public ProductCache(AppDbContext db)\n    {\n        _db = db;\n        \u002F\u002F _db now lives forever — request-specific EF change tracker shared across ALL requests\n        \u002F\u002F Symptoms: stale data, entity tracking conflicts, ObjectDisposedException\n    }\n}\n",[30,484,485,490,495,499,504,508,513,517,522,527,532,536],{"__ignoreMap":28},[33,486,487],{"class":35,"line":36},[33,488,489],{},"\u002F\u002F Singleton captures a Scoped service:\n",[33,491,492],{"class":35,"line":42},[33,493,494],{},"public class ProductCache  \u002F\u002F Singleton\n",[33,496,497],{"class":35,"line":48},[33,498,211],{},[33,500,501],{"class":35,"line":55},[33,502,503],{},"    private readonly AppDbContext _db; \u002F\u002F AppDbContext is Scoped\n",[33,505,506],{"class":35,"line":61},[33,507,52],{"emptyLinePlaceholder":51},[33,509,510],{"class":35,"line":67},[33,511,512],{},"    public ProductCache(AppDbContext db)\n",[33,514,515],{"class":35,"line":72},[33,516,451],{},[33,518,519],{"class":35,"line":78},[33,520,521],{},"        _db = db;\n",[33,523,524],{"class":35,"line":208},[33,525,526],{},"        \u002F\u002F _db now lives forever — request-specific EF change tracker shared across ALL requests\n",[33,528,529],{"class":35,"line":214},[33,530,531],{},"        \u002F\u002F Symptoms: stale data, entity tracking conflicts, ObjectDisposedException\n",[33,533,534],{"class":35,"line":220},[33,535,461],{},[33,537,538],{"class":35,"line":226},[33,539,235],{},[15,541,542],{},"This is silent in development and only manifests under concurrent load or across requests.",[23,544,546],{"className":25,"code":545,"language":27,"meta":28,"style":28},"\u002F\u002F Fix: inject IServiceScopeFactory; create a scope per operation:\npublic class ProductCache\n{\n    private readonly IServiceScopeFactory _factory;\n\n    public ProductCache(IServiceScopeFactory factory) => _factory = factory;\n\n    public async Task\u003CIReadOnlyList\u003CProduct>> GetAllAsync()\n    {\n        using var scope = _factory.CreateScope();\n        var db = scope.ServiceProvider.GetRequiredService\u003CAppDbContext>();\n        return await db.Products.AsNoTracking().ToListAsync();\n        \u002F\u002F scope and db disposed at end of using block\n    }\n}\n",[30,547,548,553,558,562,567,571,576,580,585,589,594,599,604,609,613],{"__ignoreMap":28},[33,549,550],{"class":35,"line":36},[33,551,552],{},"\u002F\u002F Fix: inject IServiceScopeFactory; create a scope per operation:\n",[33,554,555],{"class":35,"line":42},[33,556,557],{},"public class ProductCache\n",[33,559,560],{"class":35,"line":48},[33,561,211],{},[33,563,564],{"class":35,"line":55},[33,565,566],{},"    private readonly IServiceScopeFactory _factory;\n",[33,568,569],{"class":35,"line":61},[33,570,52],{"emptyLinePlaceholder":51},[33,572,573],{"class":35,"line":67},[33,574,575],{},"    public ProductCache(IServiceScopeFactory factory) => _factory = factory;\n",[33,577,578],{"class":35,"line":72},[33,579,52],{"emptyLinePlaceholder":51},[33,581,582],{"class":35,"line":78},[33,583,584],{},"    public async Task\u003CIReadOnlyList\u003CProduct>> GetAllAsync()\n",[33,586,587],{"class":35,"line":208},[33,588,451],{},[33,590,591],{"class":35,"line":214},[33,592,593],{},"        using var scope = _factory.CreateScope();\n",[33,595,596],{"class":35,"line":220},[33,597,598],{},"        var db = scope.ServiceProvider.GetRequiredService\u003CAppDbContext>();\n",[33,600,601],{"class":35,"line":226},[33,602,603],{},"        return await db.Products.AsNoTracking().ToListAsync();\n",[33,605,606],{"class":35,"line":232},[33,607,608],{},"        \u002F\u002F scope and db disposed at end of using block\n",[33,610,611],{"class":35,"line":238},[33,612,461],{},[33,614,615],{"class":35,"line":243},[33,616,235],{},[15,618,619],{},"The valid captive directions: Singleton can hold Singleton. Scoped can hold Scoped or\nSingleton. Transient can hold anything. The invalid direction: any longer-lived service\ncapturing a shorter-lived one.",[10,621,623],{"id":622},"iservicescopefactory-the-tool-for-background-work","IServiceScopeFactory — the tool for background work",[15,625,626,627,281,630,633,634,636],{},"Background services (",[30,628,629],{},"IHostedService",[30,631,632],{},"BackgroundService",") are registered as Singletons.\nThey can't inject Scoped services directly. ",[30,635,406],{}," creates an explicit scope\nper work unit.",[23,638,640],{"className":25,"code":639,"language":27,"meta":28,"style":28},"public class DataSyncJob : BackgroundService\n{\n    private readonly IServiceScopeFactory _scopeFactory;\n\n    public DataSyncJob(IServiceScopeFactory factory) => _scopeFactory = factory;\n\n    protected override async Task ExecuteAsync(CancellationToken ct)\n    {\n        while (!ct.IsCancellationRequested)\n        {\n            \u002F\u002F Fresh scope per sync cycle — own DbContext, own transaction:\n            using (var scope = _scopeFactory.CreateScope())\n            {\n                var db   = scope.ServiceProvider.GetRequiredService\u003CAppDbContext>();\n                var sync = scope.ServiceProvider.GetRequiredService\u003CISyncService>();\n\n                await sync.SyncPendingOrdersAsync(ct);\n                await db.SaveChangesAsync(ct);\n            } \u002F\u002F scope, db, and sync disposed here\n\n            await Task.Delay(TimeSpan.FromMinutes(5), ct);\n        }\n    }\n}\n",[30,641,642,647,651,656,660,665,669,674,678,683,688,693,698,703,708,713,717,722,727,732,736,742,748,753],{"__ignoreMap":28},[33,643,644],{"class":35,"line":36},[33,645,646],{},"public class DataSyncJob : BackgroundService\n",[33,648,649],{"class":35,"line":42},[33,650,211],{},[33,652,653],{"class":35,"line":48},[33,654,655],{},"    private readonly IServiceScopeFactory _scopeFactory;\n",[33,657,658],{"class":35,"line":55},[33,659,52],{"emptyLinePlaceholder":51},[33,661,662],{"class":35,"line":61},[33,663,664],{},"    public DataSyncJob(IServiceScopeFactory factory) => _scopeFactory = factory;\n",[33,666,667],{"class":35,"line":67},[33,668,52],{"emptyLinePlaceholder":51},[33,670,671],{"class":35,"line":72},[33,672,673],{},"    protected override async Task ExecuteAsync(CancellationToken ct)\n",[33,675,676],{"class":35,"line":78},[33,677,451],{},[33,679,680],{"class":35,"line":208},[33,681,682],{},"        while (!ct.IsCancellationRequested)\n",[33,684,685],{"class":35,"line":214},[33,686,687],{},"        {\n",[33,689,690],{"class":35,"line":220},[33,691,692],{},"            \u002F\u002F Fresh scope per sync cycle — own DbContext, own transaction:\n",[33,694,695],{"class":35,"line":226},[33,696,697],{},"            using (var scope = _scopeFactory.CreateScope())\n",[33,699,700],{"class":35,"line":232},[33,701,702],{},"            {\n",[33,704,705],{"class":35,"line":238},[33,706,707],{},"                var db   = scope.ServiceProvider.GetRequiredService\u003CAppDbContext>();\n",[33,709,710],{"class":35,"line":243},[33,711,712],{},"                var sync = scope.ServiceProvider.GetRequiredService\u003CISyncService>();\n",[33,714,715],{"class":35,"line":249},[33,716,52],{"emptyLinePlaceholder":51},[33,718,719],{"class":35,"line":255},[33,720,721],{},"                await sync.SyncPendingOrdersAsync(ct);\n",[33,723,724],{"class":35,"line":260},[33,725,726],{},"                await db.SaveChangesAsync(ct);\n",[33,728,729],{"class":35,"line":266},[33,730,731],{},"            } \u002F\u002F scope, db, and sync disposed here\n",[33,733,734],{"class":35,"line":272},[33,735,52],{"emptyLinePlaceholder":51},[33,737,739],{"class":35,"line":738},21,[33,740,741],{},"            await Task.Delay(TimeSpan.FromMinutes(5), ct);\n",[33,743,745],{"class":35,"line":744},22,[33,746,747],{},"        }\n",[33,749,751],{"class":35,"line":750},23,[33,752,461],{},[33,754,756],{"class":35,"line":755},24,[33,757,235],{},[15,759,760,762],{},[30,761,406],{}," is itself Singleton — safe to inject into any lifetime.",[10,764,766],{"id":765},"disposal-who-calls-dispose","Disposal — who calls Dispose?",[15,768,769,770,773,774,777,778,781,782,785],{},"The container automatically disposes ",[30,771,772],{},"IDisposable"," and ",[30,775,776],{},"IAsyncDisposable"," services when\ntheir scope ends. You should ",[158,779,780],{},"not"," call ",[30,783,784],{},"Dispose"," on injected services manually.",[23,787,789],{"className":25,"code":788,"language":27,"meta":28,"style":28},"\u002F\u002F The container calls DisposeAsync at request end:\npublic class ReportWriter : IAsyncDisposable\n{\n    private readonly StreamWriter _writer;\n\n    public ReportWriter() => _writer = new StreamWriter(\"report.csv\");\n\n    public async ValueTask DisposeAsync() => await _writer.DisposeAsync();\n}\n\nbuilder.Services.AddScoped\u003CReportWriter>();\n\u002F\u002F DisposeAsync called automatically when the HTTP request scope ends\n\n\u002F\u002F Root-scope transient IDisposable memory leak:\n\u002F\u002F Resolving a Transient IDisposable from app.Services (root scope) means it won't\n\u002F\u002F be disposed until app shutdown — always use a child scope for one-off resolutions.\nusing var scope = app.Services.CreateScope();\nvar writer = scope.ServiceProvider.GetRequiredService\u003CReportWriter>();\n\u002F\u002F Disposed when scope is disposed\n",[30,790,791,796,801,805,810,814,819,823,828,832,836,841,846,850,855,860,865,870,875],{"__ignoreMap":28},[33,792,793],{"class":35,"line":36},[33,794,795],{},"\u002F\u002F The container calls DisposeAsync at request end:\n",[33,797,798],{"class":35,"line":42},[33,799,800],{},"public class ReportWriter : IAsyncDisposable\n",[33,802,803],{"class":35,"line":48},[33,804,211],{},[33,806,807],{"class":35,"line":55},[33,808,809],{},"    private readonly StreamWriter _writer;\n",[33,811,812],{"class":35,"line":61},[33,813,52],{"emptyLinePlaceholder":51},[33,815,816],{"class":35,"line":67},[33,817,818],{},"    public ReportWriter() => _writer = new StreamWriter(\"report.csv\");\n",[33,820,821],{"class":35,"line":72},[33,822,52],{"emptyLinePlaceholder":51},[33,824,825],{"class":35,"line":78},[33,826,827],{},"    public async ValueTask DisposeAsync() => await _writer.DisposeAsync();\n",[33,829,830],{"class":35,"line":208},[33,831,235],{},[33,833,834],{"class":35,"line":214},[33,835,52],{"emptyLinePlaceholder":51},[33,837,838],{"class":35,"line":220},[33,839,840],{},"builder.Services.AddScoped\u003CReportWriter>();\n",[33,842,843],{"class":35,"line":226},[33,844,845],{},"\u002F\u002F DisposeAsync called automatically when the HTTP request scope ends\n",[33,847,848],{"class":35,"line":232},[33,849,52],{"emptyLinePlaceholder":51},[33,851,852],{"class":35,"line":238},[33,853,854],{},"\u002F\u002F Root-scope transient IDisposable memory leak:\n",[33,856,857],{"class":35,"line":243},[33,858,859],{},"\u002F\u002F Resolving a Transient IDisposable from app.Services (root scope) means it won't\n",[33,861,862],{"class":35,"line":249},[33,863,864],{},"\u002F\u002F be disposed until app shutdown — always use a child scope for one-off resolutions.\n",[33,866,867],{"class":35,"line":255},[33,868,869],{},"using var scope = app.Services.CreateScope();\n",[33,871,872],{"class":35,"line":260},[33,873,874],{},"var writer = scope.ServiceProvider.GetRequiredService\u003CReportWriter>();\n",[33,876,877],{"class":35,"line":266},[33,878,879],{},"\u002F\u002F Disposed when scope is disposed\n",[10,881,883],{"id":882},"validatescopes-catch-bugs-at-startup","ValidateScopes — catch bugs at startup",[23,885,887],{"className":25,"code":886,"language":27,"meta":28,"style":28},"builder.Host.UseDefaultServiceProvider((ctx, options) =>\n{\n    bool isDev = ctx.HostingEnvironment.IsDevelopment();\n    options.ValidateScopes  = isDev; \u002F\u002F throws if Scoped is captured by Singleton\n    options.ValidateOnBuild = isDev; \u002F\u002F throws if any dependency can't be resolved\n});\n",[30,888,889,894,898,903,908,913],{"__ignoreMap":28},[33,890,891],{"class":35,"line":36},[33,892,893],{},"builder.Host.UseDefaultServiceProvider((ctx, options) =>\n",[33,895,896],{"class":35,"line":42},[33,897,211],{},[33,899,900],{"class":35,"line":48},[33,901,902],{},"    bool isDev = ctx.HostingEnvironment.IsDevelopment();\n",[33,904,905],{"class":35,"line":55},[33,906,907],{},"    options.ValidateScopes  = isDev; \u002F\u002F throws if Scoped is captured by Singleton\n",[33,909,910],{"class":35,"line":61},[33,911,912],{},"    options.ValidateOnBuild = isDev; \u002F\u002F throws if any dependency can't be resolved\n",[33,914,915],{"class":35,"line":67},[33,916,917],{},"});\n",[15,919,920,921,924,925,928,929,932],{},"With ",[30,922,923],{},"ValidateScopes = true",", the example captive dependency throws\n",[30,926,927],{},"InvalidOperationException"," at ",[30,930,931],{},"app.Build()"," — not silently on the 10th concurrent request.",[15,934,935],{},"Add an integration test that builds the real container with these options enabled to make\nscope validation part of CI:",[23,937,939],{"className":25,"code":938,"language":27,"meta":28,"style":28},"[Fact]\npublic void ServiceGraph_IsValid()\n{\n    var services = new ServiceCollection();\n    Startup.ConfigureServices(services);\n\n    using var provider = services.BuildServiceProvider(\n        new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true });\n\n    Assert.NotNull(provider); \u002F\u002F build succeeds = graph is valid\n}\n",[30,940,941,946,951,955,960,965,969,974,979,983,988],{"__ignoreMap":28},[33,942,943],{"class":35,"line":36},[33,944,945],{},"[Fact]\n",[33,947,948],{"class":35,"line":42},[33,949,950],{},"public void ServiceGraph_IsValid()\n",[33,952,953],{"class":35,"line":48},[33,954,211],{},[33,956,957],{"class":35,"line":55},[33,958,959],{},"    var services = new ServiceCollection();\n",[33,961,962],{"class":35,"line":61},[33,963,964],{},"    Startup.ConfigureServices(services);\n",[33,966,967],{"class":35,"line":67},[33,968,52],{"emptyLinePlaceholder":51},[33,970,971],{"class":35,"line":72},[33,972,973],{},"    using var provider = services.BuildServiceProvider(\n",[33,975,976],{"class":35,"line":78},[33,977,978],{},"        new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true });\n",[33,980,981],{"class":35,"line":208},[33,982,52],{"emptyLinePlaceholder":51},[33,984,985],{"class":35,"line":214},[33,986,987],{},"    Assert.NotNull(provider); \u002F\u002F build succeeds = graph is valid\n",[33,989,990],{"class":35,"line":220},[33,991,235],{},[10,993,995],{"id":994},"lifetimes-in-blazor-server","Lifetimes in Blazor Server",[15,997,998,999,1002],{},"Blazor Server changes what \"Scoped\" means. A Scoped service in Blazor Server lives for the\n",[158,1000,1001],{},"SignalR circuit"," (browser tab session), not for an HTTP request. This makes Scoped\neffectively per-user-session — much longer than ASP.NET Core Scoped.",[23,1004,1006],{"className":25,"code":1005,"language":27,"meta":28,"style":28},"\u002F\u002F In Blazor Server:\nbuilder.Services.AddScoped\u003CIUserState, UserState>();\n\u002F\u002F One instance per browser tab — persists across component navigations\n\n\u002F\u002F Singleton in Blazor Server = shared across ALL connected browser tabs:\nbuilder.Services.AddSingleton\u003CISharedCache, MemoryCache>();\n\u002F\u002F All users share this — only safe for truly thread-safe, shared state\n",[30,1007,1008,1013,1018,1023,1027,1032,1037],{"__ignoreMap":28},[33,1009,1010],{"class":35,"line":36},[33,1011,1012],{},"\u002F\u002F In Blazor Server:\n",[33,1014,1015],{"class":35,"line":42},[33,1016,1017],{},"builder.Services.AddScoped\u003CIUserState, UserState>();\n",[33,1019,1020],{"class":35,"line":48},[33,1021,1022],{},"\u002F\u002F One instance per browser tab — persists across component navigations\n",[33,1024,1025],{"class":35,"line":55},[33,1026,52],{"emptyLinePlaceholder":51},[33,1028,1029],{"class":35,"line":61},[33,1030,1031],{},"\u002F\u002F Singleton in Blazor Server = shared across ALL connected browser tabs:\n",[33,1033,1034],{"class":35,"line":67},[33,1035,1036],{},"builder.Services.AddSingleton\u003CISharedCache, MemoryCache>();\n",[33,1038,1039],{"class":35,"line":72},[33,1040,1041],{},"\u002F\u002F All users share this — only safe for truly thread-safe, shared state\n",[10,1043,1045],{"id":1044},"recap","Recap",[15,1047,1048,1049,1051,1052,1054,1055,1057,1058,773,1061,1064],{},"Singleton lives for the app, Scoped lives for the request, and Transient lives per resolve.\nCaptive dependencies — Singletons capturing Scoped or Transient services — are the most\ndangerous lifetime bug: silent in dev, corrupting in production. Use ",[30,1050,406],{},"\nin Singletons and background services that need Scoped resources. The container owns disposal\nfor ",[30,1053,772],{}," services — never call ",[30,1056,784],{}," on injected services yourself. Enable\n",[30,1059,1060],{},"ValidateScopes",[30,1062,1063],{},"ValidateOnBuild"," in development to turn lifetime mismatches into startup\nerrors rather than production incidents.",[1066,1067,1068],"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":28,"searchDepth":42,"depth":42,"links":1070},[1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081],{"id":12,"depth":42,"text":13},{"id":20,"depth":42,"text":21},{"id":152,"depth":42,"text":153},{"id":295,"depth":42,"text":296},{"id":410,"depth":42,"text":411},{"id":471,"depth":42,"text":472},{"id":622,"depth":42,"text":623},{"id":765,"depth":42,"text":766},{"id":882,"depth":42,"text":883},{"id":994,"depth":42,"text":995},{"id":1044,"depth":42,"text":1045},"When to use Singleton, Scoped, or Transient in .NET Core DI — how captive dependencies cause subtle bugs, how disposal interacts with lifetime, and using ValidateScopes to catch registration mistakes before they reach production.","medium","md",".NET Core","dotnet",{},"\u002Fblog\u002Fdotnet-service-lifetimes","\u002Fdotnet\u002Fdependency-injection\u002Fservice-lifetimes",{"title":5,"description":1082},"blog\u002Fdotnet-service-lifetimes","Service Lifetimes","Dependency Injection","dependency-injection","2026-06-23","ORdZZXMNFN-3DDXXYQkzuTMTjcvLgC3UbXznYlxJ4lk",1782244086119]