[{"data":1,"prerenderedAt":1131},["ShallowReactive",2],{"blog-\u002Fblog\u002Fdotnet-ef-dbcontext-dbset":3},{"id":4,"title":5,"body":6,"description":1116,"difficulty":1117,"extension":1118,"framework":1119,"frameworkSlug":1120,"meta":1121,"navigation":108,"order":75,"path":1122,"qaPath":1123,"seo":1124,"stem":1125,"subtopic":1126,"topic":1127,"topicSlug":1128,"updated":1129,"__hash__":1130},"blog\u002Fblog\u002Fdotnet-ef-dbcontext-dbset.md","EF Core DbContext, DbSet, and Change Tracking",{"type":7,"value":8,"toc":1101},"minimark",[9,14,22,26,31,62,168,172,177,259,263,266,342,390,396,400,414,511,515,524,532,535,597,608,612,619,684,688,695,770,774,780,809,816,885,889,895,995,999,1002,1050,1054,1061,1076,1080,1097],[10,11,13],"h2",{"id":12},"why-dbcontext-knowledge-matters-in-interviews","Why DbContext knowledge matters in interviews",[15,16,17,21],"p",{},[18,19,20],"code",{},"DbContext"," is the single most important class in EF Core. Interviewers ask about it because\nmisconfiguring its lifetime is one of the most common causes of production data corruption in\nASP.NET Core — and the bugs it produces are notoriously hard to reproduce.",[10,23,25],{"id":24},"what-dbcontext-is","What DbContext is",[15,27,28,30],{},[18,29,20],{}," combines three patterns in one class:",[32,33,34,46,56],"ul",{},[35,36,37,41,42,45],"li",{},[38,39,40],"strong",{},"Unit of Work"," — batches all changes into one ",[18,43,44],{},"SaveChanges"," call",[35,47,48,51,52,55],{},[38,49,50],{},"Repository"," — exposes ",[18,53,54],{},"DbSet\u003CT>"," as the query surface for each entity",[35,57,58,61],{},[38,59,60],{},"Identity Map"," — returns the same object instance for the same primary key within one scope",[63,64,69],"pre",{"className":65,"code":66,"language":67,"meta":68,"style":68},"language-csharp shiki shiki-themes github-light github-dark","public class AppDbContext : DbContext\n{\n    public DbSet\u003COrder>    Orders    { get; set; }\n    public DbSet\u003CProduct>  Products  { get; set; }\n    public DbSet\u003CCustomer> Customers { get; set; }\n\n    public AppDbContext(DbContextOptions\u003CAppDbContext> options)\n        : base(options) { }\n\n    protected override void OnModelCreating(ModelBuilder mb)\n        => mb.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);\n}\n\n\u002F\u002F Register as Scoped (the default from AddDbContext):\nbuilder.Services.AddDbContext\u003CAppDbContext>(options =>\n    options.UseSqlServer(builder.Configuration.GetConnectionString(\"Default\")));\n","csharp","",[18,70,71,79,85,91,97,103,110,116,122,127,133,139,145,150,156,162],{"__ignoreMap":68},[72,73,76],"span",{"class":74,"line":75},"line",1,[72,77,78],{},"public class AppDbContext : DbContext\n",[72,80,82],{"class":74,"line":81},2,[72,83,84],{},"{\n",[72,86,88],{"class":74,"line":87},3,[72,89,90],{},"    public DbSet\u003COrder>    Orders    { get; set; }\n",[72,92,94],{"class":74,"line":93},4,[72,95,96],{},"    public DbSet\u003CProduct>  Products  { get; set; }\n",[72,98,100],{"class":74,"line":99},5,[72,101,102],{},"    public DbSet\u003CCustomer> Customers { get; set; }\n",[72,104,106],{"class":74,"line":105},6,[72,107,109],{"emptyLinePlaceholder":108},true,"\n",[72,111,113],{"class":74,"line":112},7,[72,114,115],{},"    public AppDbContext(DbContextOptions\u003CAppDbContext> options)\n",[72,117,119],{"class":74,"line":118},8,[72,120,121],{},"        : base(options) { }\n",[72,123,125],{"class":74,"line":124},9,[72,126,109],{"emptyLinePlaceholder":108},[72,128,130],{"class":74,"line":129},10,[72,131,132],{},"    protected override void OnModelCreating(ModelBuilder mb)\n",[72,134,136],{"class":74,"line":135},11,[72,137,138],{},"        => mb.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);\n",[72,140,142],{"class":74,"line":141},12,[72,143,144],{},"}\n",[72,146,148],{"class":74,"line":147},13,[72,149,109],{"emptyLinePlaceholder":108},[72,151,153],{"class":74,"line":152},14,[72,154,155],{},"\u002F\u002F Register as Scoped (the default from AddDbContext):\n",[72,157,159],{"class":74,"line":158},15,[72,160,161],{},"builder.Services.AddDbContext\u003CAppDbContext>(options =>\n",[72,163,165],{"class":74,"line":164},16,[72,166,167],{},"    options.UseSqlServer(builder.Configuration.GetConnectionString(\"Default\")));\n",[10,169,171],{"id":170},"dbset-the-query-and-change-surface","DbSet — the query and change surface",[15,173,174,176],{},[18,175,54],{}," is the entry point for querying and staging changes on one entity type.",[63,178,180],{"className":65,"code":179,"language":67,"meta":68,"style":68},"\u002F\u002F LINQ queries build SQL — nothing executes until ToListAsync:\nvar orders = await _db.Orders\n    .Where(o => o.CustomerId == id)\n    .OrderByDescending(o => o.CreatedAt)\n    .Take(20)\n    .ToListAsync(); \u002F\u002F SELECT TOP 20 ... ORDER BY CreatedAt DESC\n\n\u002F\u002F FindAsync checks the identity map before hitting the DB:\nvar order = await _db.Orders.FindAsync(42);\n\n\u002F\u002F Stage changes — no DB call until SaveChanges:\n_db.Orders.Add(new Order { CustomerId = 1 });\n_db.Orders.Update(existingOrder);\n_db.Orders.Remove(existingOrder);\n\nawait _db.SaveChangesAsync(); \u002F\u002F one transaction, all staged ops\n",[18,181,182,187,192,197,202,207,212,216,221,226,230,235,240,245,250,254],{"__ignoreMap":68},[72,183,184],{"class":74,"line":75},[72,185,186],{},"\u002F\u002F LINQ queries build SQL — nothing executes until ToListAsync:\n",[72,188,189],{"class":74,"line":81},[72,190,191],{},"var orders = await _db.Orders\n",[72,193,194],{"class":74,"line":87},[72,195,196],{},"    .Where(o => o.CustomerId == id)\n",[72,198,199],{"class":74,"line":93},[72,200,201],{},"    .OrderByDescending(o => o.CreatedAt)\n",[72,203,204],{"class":74,"line":99},[72,205,206],{},"    .Take(20)\n",[72,208,209],{"class":74,"line":105},[72,210,211],{},"    .ToListAsync(); \u002F\u002F SELECT TOP 20 ... ORDER BY CreatedAt DESC\n",[72,213,214],{"class":74,"line":112},[72,215,109],{"emptyLinePlaceholder":108},[72,217,218],{"class":74,"line":118},[72,219,220],{},"\u002F\u002F FindAsync checks the identity map before hitting the DB:\n",[72,222,223],{"class":74,"line":124},[72,224,225],{},"var order = await _db.Orders.FindAsync(42);\n",[72,227,228],{"class":74,"line":129},[72,229,109],{"emptyLinePlaceholder":108},[72,231,232],{"class":74,"line":135},[72,233,234],{},"\u002F\u002F Stage changes — no DB call until SaveChanges:\n",[72,236,237],{"class":74,"line":141},[72,238,239],{},"_db.Orders.Add(new Order { CustomerId = 1 });\n",[72,241,242],{"class":74,"line":147},[72,243,244],{},"_db.Orders.Update(existingOrder);\n",[72,246,247],{"class":74,"line":152},[72,248,249],{},"_db.Orders.Remove(existingOrder);\n",[72,251,252],{"class":74,"line":158},[72,253,109],{"emptyLinePlaceholder":108},[72,255,256],{"class":74,"line":164},[72,257,258],{},"await _db.SaveChangesAsync(); \u002F\u002F one transaction, all staged ops\n",[10,260,262],{"id":261},"the-change-tracker","The change tracker",[15,264,265],{},"The change tracker watches every entity loaded through the context and records mutations.\nFive states map to SQL operations:",[267,268,269,282],"table",{},[270,271,272],"thead",{},[273,274,275,279],"tr",{},[276,277,278],"th",{},"State",[276,280,281],{},"SQL",[283,284,285,298,310,322,332],"tbody",{},[273,286,287,293],{},[288,289,290],"td",{},[18,291,292],{},"Added",[288,294,295],{},[18,296,297],{},"INSERT",[273,299,300,305],{},[288,301,302],{},[18,303,304],{},"Modified",[288,306,307],{},[18,308,309],{},"UPDATE",[273,311,312,317],{},[288,313,314],{},[18,315,316],{},"Deleted",[288,318,319],{},[18,320,321],{},"DELETE",[273,323,324,329],{},[288,325,326],{},[18,327,328],{},"Unchanged",[288,330,331],{},"(none)",[273,333,334,339],{},[288,335,336],{},[18,337,338],{},"Detached",[288,340,341],{},"(not tracked)",[63,343,345],{"className":65,"code":344,"language":67,"meta":68,"style":68},"var order = await _db.Orders.FindAsync(42); \u002F\u002F state: Unchanged\norder.Status = \"Shipped\";                   \u002F\u002F state: Modified (auto-detected)\nawait _db.SaveChangesAsync();               \u002F\u002F UPDATE Orders SET Status='Shipped' WHERE Id=42\n\n\u002F\u002F Skip the change tracker for read-only queries:\nvar orders = await _db.Orders\n    .AsNoTracking()\n    .Where(o => o.Status == \"Pending\")\n    .ToListAsync(); \u002F\u002F faster — no snapshot, no identity map registration\n",[18,346,347,352,357,362,366,371,375,380,385],{"__ignoreMap":68},[72,348,349],{"class":74,"line":75},[72,350,351],{},"var order = await _db.Orders.FindAsync(42); \u002F\u002F state: Unchanged\n",[72,353,354],{"class":74,"line":81},[72,355,356],{},"order.Status = \"Shipped\";                   \u002F\u002F state: Modified (auto-detected)\n",[72,358,359],{"class":74,"line":87},[72,360,361],{},"await _db.SaveChangesAsync();               \u002F\u002F UPDATE Orders SET Status='Shipped' WHERE Id=42\n",[72,363,364],{"class":74,"line":93},[72,365,109],{"emptyLinePlaceholder":108},[72,367,368],{"class":74,"line":99},[72,369,370],{},"\u002F\u002F Skip the change tracker for read-only queries:\n",[72,372,373],{"class":74,"line":105},[72,374,191],{},[72,376,377],{"class":74,"line":112},[72,378,379],{},"    .AsNoTracking()\n",[72,381,382],{"class":74,"line":118},[72,383,384],{},"    .Where(o => o.Status == \"Pending\")\n",[72,386,387],{"class":74,"line":124},[72,388,389],{},"    .ToListAsync(); \u002F\u002F faster — no snapshot, no identity map registration\n",[15,391,392,395],{},[18,393,394],{},"AsNoTracking()"," is 10–20% faster on large result sets and uses less memory. Use it on\nevery read-only path.",[10,397,399],{"id":398},"savechanges-and-transactions","SaveChanges and transactions",[15,401,402,405,406,408,409,408,411,413],{},[18,403,404],{},"SaveChangesAsync"," collects all ",[18,407,292],{},"\u002F",[18,410,304],{},[18,412,316],{}," entries and executes them in\none implicit transaction.",[63,415,417],{"className":65,"code":416,"language":67,"meta":68,"style":68},"\u002F\u002F One unit of work — three operations, one transaction:\n_db.Customers.Add(customer);\nproduct.Stock -= 1;\n_db.Orders.Add(order);\nawait _db.SaveChangesAsync(); \u002F\u002F INSERT Customer + UPDATE Product + INSERT Order in one tx\n\n\u002F\u002F Multiple SaveChanges calls need an explicit transaction:\nawait using var tx = await _db.Database.BeginTransactionAsync();\ntry\n{\n    _db.Orders.Add(order);\n    await _db.SaveChangesAsync();\n\n    _db.Invoices.Add(invoice);\n    await _db.SaveChangesAsync();\n\n    await tx.CommitAsync();\n}\ncatch { await tx.RollbackAsync(); throw; }\n",[18,418,419,424,429,434,439,444,448,453,458,463,467,472,477,481,486,490,494,500,505],{"__ignoreMap":68},[72,420,421],{"class":74,"line":75},[72,422,423],{},"\u002F\u002F One unit of work — three operations, one transaction:\n",[72,425,426],{"class":74,"line":81},[72,427,428],{},"_db.Customers.Add(customer);\n",[72,430,431],{"class":74,"line":87},[72,432,433],{},"product.Stock -= 1;\n",[72,435,436],{"class":74,"line":93},[72,437,438],{},"_db.Orders.Add(order);\n",[72,440,441],{"class":74,"line":99},[72,442,443],{},"await _db.SaveChangesAsync(); \u002F\u002F INSERT Customer + UPDATE Product + INSERT Order in one tx\n",[72,445,446],{"class":74,"line":105},[72,447,109],{"emptyLinePlaceholder":108},[72,449,450],{"class":74,"line":112},[72,451,452],{},"\u002F\u002F Multiple SaveChanges calls need an explicit transaction:\n",[72,454,455],{"class":74,"line":118},[72,456,457],{},"await using var tx = await _db.Database.BeginTransactionAsync();\n",[72,459,460],{"class":74,"line":124},[72,461,462],{},"try\n",[72,464,465],{"class":74,"line":129},[72,466,84],{},[72,468,469],{"class":74,"line":135},[72,470,471],{},"    _db.Orders.Add(order);\n",[72,473,474],{"class":74,"line":141},[72,475,476],{},"    await _db.SaveChangesAsync();\n",[72,478,479],{"class":74,"line":147},[72,480,109],{"emptyLinePlaceholder":108},[72,482,483],{"class":74,"line":152},[72,484,485],{},"    _db.Invoices.Add(invoice);\n",[72,487,488],{"class":74,"line":158},[72,489,476],{},[72,491,492],{"class":74,"line":164},[72,493,109],{"emptyLinePlaceholder":108},[72,495,497],{"class":74,"line":496},17,[72,498,499],{},"    await tx.CommitAsync();\n",[72,501,503],{"class":74,"line":502},18,[72,504,144],{},[72,506,508],{"class":74,"line":507},19,[72,509,510],{},"catch { await tx.RollbackAsync(); throw; }\n",[10,512,514],{"id":513},"dbcontext-lifetime-why-scoped-is-the-only-right-answer","DbContext lifetime — why Scoped is the only right answer",[15,516,517,519,520,523],{},[18,518,20],{}," is ",[38,521,522],{},"stateful"," — the change tracker is not thread-safe and holds open a\ndatabase connection. The wrong lifetime causes production bugs.",[63,525,530],{"className":526,"code":528,"language":529},[527],"language-text","Scoped   — one context per HTTP request. Default from AddDbContext.\nSingleton — shared across all concurrent requests. Change tracker corruption.\nTransient — new context per injection. Changes in ServiceA invisible to ServiceB.\n","text",[18,531,528],{"__ignoreMap":68},[15,533,534],{},"Background services (hosted services, workers) must create their own scope:",[63,536,538],{"className":65,"code":537,"language":67,"meta":68,"style":68},"public class DataSyncWorker : BackgroundService\n{\n    private readonly IServiceScopeFactory _scopeFactory;\n    public DataSyncWorker(IServiceScopeFactory f) => _scopeFactory = f;\n\n    protected override async Task ExecuteAsync(CancellationToken ct)\n    {\n        using var scope = _scopeFactory.CreateScope();\n        var db = scope.ServiceProvider.GetRequiredService\u003CAppDbContext>();\n        \u002F\u002F db is a properly scoped context for this job\n    }\n}\n",[18,539,540,545,549,554,559,563,568,573,578,583,588,593],{"__ignoreMap":68},[72,541,542],{"class":74,"line":75},[72,543,544],{},"public class DataSyncWorker : BackgroundService\n",[72,546,547],{"class":74,"line":81},[72,548,84],{},[72,550,551],{"class":74,"line":87},[72,552,553],{},"    private readonly IServiceScopeFactory _scopeFactory;\n",[72,555,556],{"class":74,"line":93},[72,557,558],{},"    public DataSyncWorker(IServiceScopeFactory f) => _scopeFactory = f;\n",[72,560,561],{"class":74,"line":99},[72,562,109],{"emptyLinePlaceholder":108},[72,564,565],{"class":74,"line":105},[72,566,567],{},"    protected override async Task ExecuteAsync(CancellationToken ct)\n",[72,569,570],{"class":74,"line":112},[72,571,572],{},"    {\n",[72,574,575],{"class":74,"line":118},[72,576,577],{},"        using var scope = _scopeFactory.CreateScope();\n",[72,579,580],{"class":74,"line":124},[72,581,582],{},"        var db = scope.ServiceProvider.GetRequiredService\u003CAppDbContext>();\n",[72,584,585],{"class":74,"line":129},[72,586,587],{},"        \u002F\u002F db is a properly scoped context for this job\n",[72,589,590],{"class":74,"line":135},[72,591,592],{},"    }\n",[72,594,595],{"class":74,"line":141},[72,596,144],{},[15,598,599,600,603,604,607],{},"Never inject ",[18,601,602],{},"AppDbContext"," directly into a Singleton service. Use ",[18,605,606],{},"IServiceScopeFactory",".",[10,609,611],{"id":610},"dbcontext-configuration-di-vs-onconfiguring","DbContext configuration — DI vs OnConfiguring",[15,613,614,615,618],{},"Always pass ",[18,616,617],{},"DbContextOptions"," through the constructor so tests can inject a different provider:",[63,620,622],{"className":65,"code":621,"language":67,"meta":68,"style":68},"\u002F\u002F Constructor options — testable:\npublic class AppDbContext : DbContext\n{\n    public AppDbContext(DbContextOptions\u003CAppDbContext> options) : base(options) { }\n}\nbuilder.Services.AddDbContext\u003CAppDbContext>(o => o.UseSqlServer(connectionString));\n\n\u002F\u002F Test: swap to SQLite in-memory without changing the context class:\nservices.AddDbContext\u003CAppDbContext>(o => o.UseSqlite(\"Data Source=:memory:\"));\n\n\u002F\u002F OnConfiguring — hardcoded; can't swap in tests:\nprotected override void OnConfiguring(DbContextOptionsBuilder b)\n    => b.UseSqlServer(\"Server=prod;...\"); \u002F\u002F untestable\n",[18,623,624,629,633,637,642,646,651,655,660,665,669,674,679],{"__ignoreMap":68},[72,625,626],{"class":74,"line":75},[72,627,628],{},"\u002F\u002F Constructor options — testable:\n",[72,630,631],{"class":74,"line":81},[72,632,78],{},[72,634,635],{"class":74,"line":87},[72,636,84],{},[72,638,639],{"class":74,"line":93},[72,640,641],{},"    public AppDbContext(DbContextOptions\u003CAppDbContext> options) : base(options) { }\n",[72,643,644],{"class":74,"line":99},[72,645,144],{},[72,647,648],{"class":74,"line":105},[72,649,650],{},"builder.Services.AddDbContext\u003CAppDbContext>(o => o.UseSqlServer(connectionString));\n",[72,652,653],{"class":74,"line":112},[72,654,109],{"emptyLinePlaceholder":108},[72,656,657],{"class":74,"line":118},[72,658,659],{},"\u002F\u002F Test: swap to SQLite in-memory without changing the context class:\n",[72,661,662],{"class":74,"line":124},[72,663,664],{},"services.AddDbContext\u003CAppDbContext>(o => o.UseSqlite(\"Data Source=:memory:\"));\n",[72,666,667],{"class":74,"line":129},[72,668,109],{"emptyLinePlaceholder":108},[72,670,671],{"class":74,"line":135},[72,672,673],{},"\u002F\u002F OnConfiguring — hardcoded; can't swap in tests:\n",[72,675,676],{"class":74,"line":141},[72,677,678],{},"protected override void OnConfiguring(DbContextOptionsBuilder b)\n",[72,680,681],{"class":74,"line":147},[72,682,683],{},"    => b.UseSqlServer(\"Server=prod;...\"); \u002F\u002F untestable\n",[10,685,687],{"id":686},"entity-configuration-fluent-api-vs-data-annotations","Entity configuration — Fluent API vs data annotations",[15,689,690,691,694],{},"Fluent API via ",[18,692,693],{},"IEntityTypeConfiguration\u003CT>"," is the preferred approach for production codebases.\nIt keeps entity classes free of EF-specific attributes:",[63,696,698],{"className":65,"code":697,"language":67,"meta":68,"style":68},"public class OrderConfiguration : IEntityTypeConfiguration\u003COrder>\n{\n    public void Configure(EntityTypeBuilder\u003COrder> b)\n    {\n        b.ToTable(\"Orders\");\n        b.HasKey(o => o.Id);\n        b.Property(o => o.Status).IsRequired().HasMaxLength(50);\n        b.Property(o => o.Total).HasColumnType(\"decimal(18,2)\");\n        b.HasIndex(o => o.CustomerId);\n    }\n}\n\n\u002F\u002F Register all configurations automatically:\nprotected override void OnModelCreating(ModelBuilder mb)\n    => mb.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);\n",[18,699,700,705,709,714,718,723,728,733,738,743,747,751,755,760,765],{"__ignoreMap":68},[72,701,702],{"class":74,"line":75},[72,703,704],{},"public class OrderConfiguration : IEntityTypeConfiguration\u003COrder>\n",[72,706,707],{"class":74,"line":81},[72,708,84],{},[72,710,711],{"class":74,"line":87},[72,712,713],{},"    public void Configure(EntityTypeBuilder\u003COrder> b)\n",[72,715,716],{"class":74,"line":93},[72,717,572],{},[72,719,720],{"class":74,"line":99},[72,721,722],{},"        b.ToTable(\"Orders\");\n",[72,724,725],{"class":74,"line":105},[72,726,727],{},"        b.HasKey(o => o.Id);\n",[72,729,730],{"class":74,"line":112},[72,731,732],{},"        b.Property(o => o.Status).IsRequired().HasMaxLength(50);\n",[72,734,735],{"class":74,"line":118},[72,736,737],{},"        b.Property(o => o.Total).HasColumnType(\"decimal(18,2)\");\n",[72,739,740],{"class":74,"line":124},[72,741,742],{},"        b.HasIndex(o => o.CustomerId);\n",[72,744,745],{"class":74,"line":129},[72,746,592],{},[72,748,749],{"class":74,"line":135},[72,750,144],{},[72,752,753],{"class":74,"line":141},[72,754,109],{"emptyLinePlaceholder":108},[72,756,757],{"class":74,"line":147},[72,758,759],{},"\u002F\u002F Register all configurations automatically:\n",[72,761,762],{"class":74,"line":152},[72,763,764],{},"protected override void OnModelCreating(ModelBuilder mb)\n",[72,766,767],{"class":74,"line":158},[72,768,769],{},"    => mb.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);\n",[10,771,773],{"id":772},"dbcontext-pooling-for-high-throughput","DbContext pooling for high throughput",[15,775,776,779],{},[18,777,778],{},"AddDbContextPool"," reuses context instances from a pool, removing per-request allocation cost:",[63,781,783],{"className":65,"code":782,"language":67,"meta":68,"style":68},"builder.Services.AddDbContextPool\u003CAppDbContext>(options =>\n    options.UseSqlServer(connectionString), poolSize: 128);\n\n\u002F\u002F Constraints: context constructor must accept only DbContextOptions.\n\u002F\u002F EF resets the change tracker between uses — custom fields are NOT reset.\n",[18,784,785,790,795,799,804],{"__ignoreMap":68},[72,786,787],{"class":74,"line":75},[72,788,789],{},"builder.Services.AddDbContextPool\u003CAppDbContext>(options =>\n",[72,791,792],{"class":74,"line":81},[72,793,794],{},"    options.UseSqlServer(connectionString), poolSize: 128);\n",[72,796,797],{"class":74,"line":87},[72,798,109],{"emptyLinePlaceholder":108},[72,800,801],{"class":74,"line":93},[72,802,803],{},"\u002F\u002F Constraints: context constructor must accept only DbContextOptions.\n",[72,805,806],{"class":74,"line":99},[72,807,808],{},"\u002F\u002F EF resets the change tracker between uses — custom fields are NOT reset.\n",[15,810,811,812,815],{},"Use ",[18,813,814],{},"IDbContextFactory\u003CT>"," instead when you need fine-grained control over context lifetime\n(Blazor Server components, background jobs):",[63,817,819],{"className":65,"code":818,"language":67,"meta":68,"style":68},"builder.Services.AddDbContextFactory\u003CAppDbContext>(o => o.UseSqlServer(connectionString));\n\npublic class ProductImporter\n{\n    private readonly IDbContextFactory\u003CAppDbContext> _factory;\n    public ProductImporter(IDbContextFactory\u003CAppDbContext> f) => _factory = f;\n\n    public async Task ImportAsync(IEnumerable\u003CProduct> products)\n    {\n        await using var db = await _factory.CreateDbContextAsync();\n        db.Products.AddRange(products);\n        await db.SaveChangesAsync();\n    }\n}\n",[18,820,821,826,830,835,839,844,849,853,858,862,867,872,877,881],{"__ignoreMap":68},[72,822,823],{"class":74,"line":75},[72,824,825],{},"builder.Services.AddDbContextFactory\u003CAppDbContext>(o => o.UseSqlServer(connectionString));\n",[72,827,828],{"class":74,"line":81},[72,829,109],{"emptyLinePlaceholder":108},[72,831,832],{"class":74,"line":87},[72,833,834],{},"public class ProductImporter\n",[72,836,837],{"class":74,"line":93},[72,838,84],{},[72,840,841],{"class":74,"line":99},[72,842,843],{},"    private readonly IDbContextFactory\u003CAppDbContext> _factory;\n",[72,845,846],{"class":74,"line":105},[72,847,848],{},"    public ProductImporter(IDbContextFactory\u003CAppDbContext> f) => _factory = f;\n",[72,850,851],{"class":74,"line":112},[72,852,109],{"emptyLinePlaceholder":108},[72,854,855],{"class":74,"line":118},[72,856,857],{},"    public async Task ImportAsync(IEnumerable\u003CProduct> products)\n",[72,859,860],{"class":74,"line":124},[72,861,572],{},[72,863,864],{"class":74,"line":129},[72,865,866],{},"        await using var db = await _factory.CreateDbContextAsync();\n",[72,868,869],{"class":74,"line":135},[72,870,871],{},"        db.Products.AddRange(products);\n",[72,873,874],{"class":74,"line":141},[72,875,876],{},"        await db.SaveChangesAsync();\n",[72,878,879],{"class":74,"line":147},[72,880,592],{},[72,882,883],{"class":74,"line":152},[72,884,144],{},[10,886,888],{"id":887},"interceptors-for-auditing","Interceptors for auditing",[15,890,891,894],{},[18,892,893],{},"SaveChangesInterceptor"," stamps audit fields on every save without touching entity classes:",[63,896,898],{"className":65,"code":897,"language":67,"meta":68,"style":68},"public class AuditInterceptor : SaveChangesInterceptor\n{\n    public override ValueTask\u003CInterceptionResult\u003Cint>> SavingChangesAsync(\n        DbContextEventData data, InterceptionResult\u003Cint> result, CancellationToken ct = default)\n    {\n        var now = DateTime.UtcNow;\n        foreach (var entry in data.Context!.ChangeTracker.Entries\u003CIAuditable>())\n        {\n            if (entry.State == EntityState.Added)   entry.Entity.CreatedAt = now;\n            if (entry.State is EntityState.Added or EntityState.Modified)\n                                                    entry.Entity.UpdatedAt = now;\n        }\n        return base.SavingChangesAsync(data, result, ct);\n    }\n}\n\n\u002F\u002F Register:\nbuilder.Services.AddDbContext\u003CAppDbContext>(options =>\n    options.UseSqlServer(connectionString)\n           .AddInterceptors(new AuditInterceptor()));\n",[18,899,900,905,909,914,919,923,928,933,938,943,948,953,958,963,967,971,975,980,984,989],{"__ignoreMap":68},[72,901,902],{"class":74,"line":75},[72,903,904],{},"public class AuditInterceptor : SaveChangesInterceptor\n",[72,906,907],{"class":74,"line":81},[72,908,84],{},[72,910,911],{"class":74,"line":87},[72,912,913],{},"    public override ValueTask\u003CInterceptionResult\u003Cint>> SavingChangesAsync(\n",[72,915,916],{"class":74,"line":93},[72,917,918],{},"        DbContextEventData data, InterceptionResult\u003Cint> result, CancellationToken ct = default)\n",[72,920,921],{"class":74,"line":99},[72,922,572],{},[72,924,925],{"class":74,"line":105},[72,926,927],{},"        var now = DateTime.UtcNow;\n",[72,929,930],{"class":74,"line":112},[72,931,932],{},"        foreach (var entry in data.Context!.ChangeTracker.Entries\u003CIAuditable>())\n",[72,934,935],{"class":74,"line":118},[72,936,937],{},"        {\n",[72,939,940],{"class":74,"line":124},[72,941,942],{},"            if (entry.State == EntityState.Added)   entry.Entity.CreatedAt = now;\n",[72,944,945],{"class":74,"line":129},[72,946,947],{},"            if (entry.State is EntityState.Added or EntityState.Modified)\n",[72,949,950],{"class":74,"line":135},[72,951,952],{},"                                                    entry.Entity.UpdatedAt = now;\n",[72,954,955],{"class":74,"line":141},[72,956,957],{},"        }\n",[72,959,960],{"class":74,"line":147},[72,961,962],{},"        return base.SavingChangesAsync(data, result, ct);\n",[72,964,965],{"class":74,"line":152},[72,966,592],{},[72,968,969],{"class":74,"line":158},[72,970,144],{},[72,972,973],{"class":74,"line":164},[72,974,109],{"emptyLinePlaceholder":108},[72,976,977],{"class":74,"line":496},[72,978,979],{},"\u002F\u002F Register:\n",[72,981,982],{"class":74,"line":502},[72,983,161],{},[72,985,986],{"class":74,"line":507},[72,987,988],{},"    options.UseSqlServer(connectionString)\n",[72,990,992],{"class":74,"line":991},20,[72,993,994],{},"           .AddInterceptors(new AuditInterceptor()));\n",[10,996,998],{"id":997},"value-conversions","Value conversions",[15,1000,1001],{},"Value conversions let EF Core store rich domain types in simple column types:",[63,1003,1005],{"className":65,"code":1004,"language":67,"meta":68,"style":68},"\u002F\u002F Enum as string (readable in the DB):\nbuilder.Property(o => o.Status).HasConversion\u003Cstring>().HasMaxLength(20);\n\n\u002F\u002F Strongly typed ID:\nbuilder.Property(o => o.Id)\n       .HasConversion(id => id.Value, val => new OrderId(val));\n\n\u002F\u002F JSON column (EF Core 7+):\nbuilder.OwnsOne(o => o.ShippingAddress, a => a.ToJson());\n",[18,1006,1007,1012,1017,1021,1026,1031,1036,1040,1045],{"__ignoreMap":68},[72,1008,1009],{"class":74,"line":75},[72,1010,1011],{},"\u002F\u002F Enum as string (readable in the DB):\n",[72,1013,1014],{"class":74,"line":81},[72,1015,1016],{},"builder.Property(o => o.Status).HasConversion\u003Cstring>().HasMaxLength(20);\n",[72,1018,1019],{"class":74,"line":87},[72,1020,109],{"emptyLinePlaceholder":108},[72,1022,1023],{"class":74,"line":93},[72,1024,1025],{},"\u002F\u002F Strongly typed ID:\n",[72,1027,1028],{"class":74,"line":99},[72,1029,1030],{},"builder.Property(o => o.Id)\n",[72,1032,1033],{"class":74,"line":105},[72,1034,1035],{},"       .HasConversion(id => id.Value, val => new OrderId(val));\n",[72,1037,1038],{"class":74,"line":112},[72,1039,109],{"emptyLinePlaceholder":108},[72,1041,1042],{"class":74,"line":118},[72,1043,1044],{},"\u002F\u002F JSON column (EF Core 7+):\n",[72,1046,1047],{"class":74,"line":124},[72,1048,1049],{},"builder.OwnsOne(o => o.ShippingAddress, a => a.ToJson());\n",[10,1051,1053],{"id":1052},"global-query-filters","Global query filters",[15,1055,1056,1057,1060],{},"Apply automatic ",[18,1058,1059],{},"WHERE"," clauses for soft delete and multi-tenancy:",[63,1062,1064],{"className":65,"code":1063,"language":67,"meta":68,"style":68},"modelBuilder.Entity\u003COrder>().HasQueryFilter(o => !o.IsDeleted);\n\u002F\u002F Every query: WHERE IsDeleted = 0 — bypass with .IgnoreQueryFilters()\n",[18,1065,1066,1071],{"__ignoreMap":68},[72,1067,1068],{"class":74,"line":75},[72,1069,1070],{},"modelBuilder.Entity\u003COrder>().HasQueryFilter(o => !o.IsDeleted);\n",[72,1072,1073],{"class":74,"line":81},[72,1074,1075],{},"\u002F\u002F Every query: WHERE IsDeleted = 0 — bypass with .IgnoreQueryFilters()\n",[10,1077,1079],{"id":1078},"recap","Recap",[15,1081,1082,1084,1085,1087,1088,1090,1091,1093,1094,1096],{},[18,1083,20],{}," is a Unit of Work + Repository + Identity Map in one. Register it Scoped.\nUse ",[18,1086,394],{}," on read-only queries. Pass ",[18,1089,617],{}," through the constructor\nso tests can swap providers. Use ",[18,1092,778],{}," for high-throughput APIs. Use\n",[18,1095,893],{}," for auditing. Use value conversions and global query filters for\ncross-cutting concerns.",[1098,1099,1100],"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":68,"searchDepth":81,"depth":81,"links":1102},[1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115],{"id":12,"depth":81,"text":13},{"id":24,"depth":81,"text":25},{"id":170,"depth":81,"text":171},{"id":261,"depth":81,"text":262},{"id":398,"depth":81,"text":399},{"id":513,"depth":81,"text":514},{"id":610,"depth":81,"text":611},{"id":686,"depth":81,"text":687},{"id":772,"depth":81,"text":773},{"id":887,"depth":81,"text":888},{"id":997,"depth":81,"text":998},{"id":1052,"depth":81,"text":1053},{"id":1078,"depth":81,"text":1079},"How EF Core tracks changes, manages entity state, and persists data — including why Singleton lifetime is a bug, when to pool contexts, and how to add audit logging with interceptors.","medium","md",".NET Core","dotnet",{},"\u002Fblog\u002Fdotnet-ef-dbcontext-dbset","\u002Fdotnet\u002Fentity-framework\u002Fdbcontext-dbset",{"title":5,"description":1116},"blog\u002Fdotnet-ef-dbcontext-dbset","DbContext & DbSet","Entity Framework Core","entity-framework","2026-06-23","u7alB-7mJjhdVQYQfPRuOlBdNdgP3JIP1XAzxQWXsHQ",1782244083928]