DEV_NET_CORE
GET_STARTED
.NETEntity Framework

DbContext change tracking and entity state management in EF Core

Overview

DbContext, DbSet, entity states, and the Change Tracker are central to how Entity Framework Core reads data, tracks object changes, and writes those changes back to the database.

In EF Core, a DbContext represents a short-lived unit of work. It usually lives for one business operation, such as one HTTP request, one command handler, one background job step, or one screen edit operation. During that unit of work, EF Core can query entities, track them, detect changes, and generate SQL commands when SaveChanges or SaveChangesAsync is called.

This topic matters because many production bugs in EF Core come from misunderstanding tracking behavior. Common issues include accidentally updating every column, attaching duplicate entity instances, using a long-lived DbContext, mixing tracked and untracked entities incorrectly, calling Update on disconnected DTOs, or disabling DetectChanges without understanding the consequences.

It is important for interviews because it tests more than basic EF Core syntax. A strong candidate should understand how EF Core works internally enough to make good persistence decisions, debug unexpected updates, design clean API update flows, and avoid performance and concurrency problems.

Typical real-world use cases include:

  • Loading an entity, changing a few properties, and saving only the changed columns.
  • Creating new entities with Add.
  • Updating disconnected entities from API request DTOs.
  • Attaching existing entities without querying them first.
  • Deleting entities with Remove.
  • Inspecting ChangeTracker.Entries() for auditing, domain events, debugging, or soft-delete logic.
  • Choosing between tracking queries and AsNoTracking queries.
  • Avoiding duplicate tracked entity instances in long-running workflows.
  • Understanding why an entity is Added, Modified, Deleted, Unchanged, or Detached.

Core Concepts

DbContext as a Unit of Work

DbContext is the main EF Core object used to coordinate database access. It combines several responsibilities:

  • Database connection and provider configuration.
  • Query execution.
  • Change tracking.
  • Relationship fix-up.
  • Entity state management.
  • Command generation.
  • Transaction coordination for SaveChanges.

A common mental model is:

  1. Create a DbContext.
  2. Query, add, attach, update, or remove entities.
  3. Change entity property values.
  4. Call SaveChangesAsync.
  5. Dispose the DbContext.

Example:

Code
public sealed class AppDbContext : DbContext
{
    public DbSet<Customer> Customers => Set<Customer>();
    public DbSet<Order> Orders => Set<Order>();

    public AppDbContext(DbContextOptions<AppDbContext> options)
        : base(options)
    {
    }
}

In ASP.NET Core, DbContext is commonly registered as scoped:

Code
builder.Services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
});

For a typical Web API request, this means the same context instance is used during one request scope and disposed at the end of that request.

Best practice:

Code
public sealed class UpdateCustomerHandler
{
    private readonly AppDbContext _dbContext;

    public UpdateCustomerHandler(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task HandleAsync(int customerId, string newName, CancellationToken cancellationToken)
    {
        var customer = await _dbContext.Customers
            .SingleAsync(c => c.Id == customerId, cancellationToken);

        customer.Name = newName;

        await _dbContext.SaveChangesAsync(cancellationToken);
    }
}

Avoid treating DbContext as a singleton. It is not thread-safe, and a long-lived context can accumulate tracked entities, stale state, memory usage, and identity resolution conflicts.

DbSet

DbSet<TEntity> represents a queryable and mutable set of entities of a specific type.

It is used for:

  • Querying a database table or collection-like entity source.
  • Adding new entities.
  • Attaching existing entities.
  • Updating disconnected entities.
  • Removing entities.
  • Accessing local tracked entities.

Example:

Code
var activeCustomers = await dbContext.Customers
    .Where(c => c.IsActive)
    .OrderBy(c => c.Name)
    .ToListAsync(cancellationToken);

A DbSet<Customer> does not literally hold every customer in memory. It represents the query root for Customer entities. The database is queried only when the LINQ query is executed, such as by calling ToListAsync, SingleAsync, FirstOrDefaultAsync, or iterating the query.

Entity States

EF Core assigns each tracked entity an EntityState.

StateMeaningSaved by SaveChanges?
DetachedThe entity is not tracked by the current DbContext.No
AddedThe entity is new and should be inserted.Insert
UnchangedThe entity is tracked and matches the database snapshot.No
ModifiedOne or more properties are marked as changed.Update
DeletedThe entity should be deleted.Delete

Example:

Code
var customer = await dbContext.Customers.FindAsync([1], cancellationToken);

Console.WriteLine(dbContext.Entry(customer!).State); // Unchanged

customer!.Name = "Updated Name";

Console.WriteLine(dbContext.Entry(customer).State); // Often still Unchanged until changes are detected

await dbContext.SaveChangesAsync(cancellationToken);

A key interview point is that EF Core can track modifications at the property level. If a tracked entity is loaded from the database and only one property changes, EF Core can generate an update for only that changed property.

Change Tracker

The Change Tracker is the EF Core component that tracks entity instances and their states.

It keeps information such as:

  • Which entities are being tracked.
  • Entity state.
  • Original property values.
  • Current property values.
  • Modified properties.
  • Temporary key values.
  • Relationship changes.
  • Navigation fix-up information.

Example:

Code
foreach (var entry in dbContext.ChangeTracker.Entries())
{
    Console.WriteLine($"{entry.Entity.GetType().Name}: {entry.State}");
}

The Change Tracker is what allows this simple update pattern to work:

Code
var customer = await dbContext.Customers
    .SingleAsync(c => c.Id == customerId, cancellationToken);

customer.Email = request.Email;

await dbContext.SaveChangesAsync(cancellationToken);

EF Core knows customer was loaded and tracked. It compares the current values with the original values and generates the needed database update.

Snapshot Change Tracking

By default, EF Core uses snapshot change tracking.

When an entity is first tracked, EF Core stores a snapshot of its property values. Later, EF Core compares the current values with that snapshot to detect what changed.

Example:

Code
var product = await dbContext.Products
    .SingleAsync(p => p.Id == productId, cancellationToken);

product.Price = 99.99m;

await dbContext.SaveChangesAsync(cancellationToken);

EF Core originally remembers the old Price. At save time, it detects that Price changed and generates an update.

Conceptually:

Code
Original Price: 79.99
Current Price:  99.99
State:          Modified
Modified prop:  Price

This is different from simply saying "the entity object changed." EF Core needs either tracked state, explicit state changes, or attached state to know what should be written.

DetectChanges

DetectChanges is the process EF Core uses to discover changes made to tracked entities.

It can be triggered automatically by EF Core in common operations such as:

  • SaveChanges.
  • SaveChangesAsync.
  • ChangeTracker.Entries().
  • ChangeTracker.HasChanges().
  • DbSet.Local.
  • Some Entry operations.

You can also call it manually:

Code
dbContext.ChangeTracker.DetectChanges();

Console.WriteLine(dbContext.ChangeTracker.DebugView.LongView);

Manual DetectChanges is useful when:

  • Debugging tracking state.
  • Working with low-level change-tracking APIs.
  • Temporarily disabling automatic change detection for performance.
  • Inspecting DebugView.

A common performance pattern for large imports is to temporarily disable automatic change detection:

Code
dbContext.ChangeTracker.AutoDetectChangesEnabled = false;

try
{
    foreach (var item in importedItems)
    {
        dbContext.Products.Add(item);
    }

    dbContext.ChangeTracker.DetectChanges();
    await dbContext.SaveChangesAsync(cancellationToken);
}
finally
{
    dbContext.ChangeTracker.AutoDetectChangesEnabled = true;
}

This should be used carefully. Disabling automatic change detection can improve performance in some large batch scenarios, but it can also cause incorrect behavior if the code expects EF Core to detect changes automatically.

Tracking Queries

By default, EF Core queries that return entity types are tracking queries.

Example:

Code
var customer = await dbContext.Customers
    .SingleAsync(c => c.Id == customerId, cancellationToken);

customer.Name = "New Name";

await dbContext.SaveChangesAsync(cancellationToken);

The loaded customer is tracked. Updating the property and calling SaveChangesAsync is enough.

Tracking queries are useful when:

  • You intend to update the returned entities.
  • You need identity resolution within the context.
  • You want EF Core to preserve original values for efficient updates.
  • You are working inside one unit of work.

No-Tracking Queries

AsNoTracking tells EF Core not to track returned entities.

Example:

Code
var customers = await dbContext.Customers
    .AsNoTracking()
    .Where(c => c.IsActive)
    .ToListAsync(cancellationToken);

No-tracking queries are usually better for read-only operations because they avoid the overhead of tracking.

Use AsNoTracking for:

  • Read-only API responses.
  • Reports.
  • Dropdown lists.
  • Search results.
  • Queries with many rows where no update is needed.

Do not use AsNoTracking if you plan to edit the returned entity and expect SaveChanges to detect it automatically.

Problem example:

Code
var customer = await dbContext.Customers
    .AsNoTracking()
    .SingleAsync(c => c.Id == customerId, cancellationToken);

customer.Name = "Updated Name";

await dbContext.SaveChangesAsync(cancellationToken); // No update, because customer is not tracked

To update an untracked entity, you must either query a tracked entity first or explicitly attach/update it.

Identity Resolution

A single DbContext can track only one entity instance with a given primary key value.

This means the following pattern can fail:

Code
var trackedCustomer = await dbContext.Customers
    .SingleAsync(c => c.Id == request.Id, cancellationToken);

var detachedCustomer = new Customer
{
    Id = request.Id,
    Name = request.Name
};

dbContext.Update(detachedCustomer); // Can throw because another instance with same key is already tracked

EF Core avoids tracking multiple instances with the same key because it would not know which instance represents the correct state.

Better approach:

Code
var customer = await dbContext.Customers
    .SingleAsync(c => c.Id == request.Id, cancellationToken);

customer.Name = request.Name;
customer.Email = request.Email;

await dbContext.SaveChangesAsync(cancellationToken);

Identity resolution is one reason short-lived contexts are important. A long-lived context increases the chance that stale or duplicate entity instances remain tracked.

Add

Add tells EF Core that an entity is new and should be inserted.

Code
var customer = new Customer
{
    Name = "Alice",
    Email = "[email protected]"
};

dbContext.Customers.Add(customer);

await dbContext.SaveChangesAsync(cancellationToken);

The entity state becomes Added. On save, EF Core generates an INSERT.

Important behavior:

  • Add can affect an entire object graph.
  • Entities with generated keys may receive temporary key values before save.
  • After save, generated database keys are populated back into the entity.
  • After successful save, the entity usually becomes Unchanged.

Attach

Attach starts tracking an existing entity without marking it as modified.

Code
var customer = new Customer
{
    Id = 10
};

dbContext.Customers.Attach(customer);

Console.WriteLine(dbContext.Entry(customer).State); // Unchanged

Attach is useful when:

  • You know the entity already exists.
  • You do not want to update every column.
  • You want to set a relationship using a known key.
  • You want to mark specific properties as modified manually.

Example: update one property without loading the full row:

Code
var customer = new Customer
{
    Id = request.Id
};

dbContext.Customers.Attach(customer);

customer.Email = request.Email;

dbContext.Entry(customer)
    .Property(c => c.Email)
    .IsModified = true;

await dbContext.SaveChangesAsync(cancellationToken);

This can be efficient, but it must be used carefully because:

  • You bypass database validation that a prior query might have provided.
  • You may not have original values for concurrency checks unless configured separately.
  • You must explicitly mark what changed.
  • Incorrect use can cause missing updates.

Update

Update starts tracking an entity as Modified.

Code
var customer = new Customer
{
    Id = request.Id,
    Name = request.Name,
    Email = request.Email
};

dbContext.Customers.Update(customer);

await dbContext.SaveChangesAsync(cancellationToken);

This is simple, but it can be dangerous with disconnected API models.

Update usually marks the entire entity graph as modified. That can result in updating more columns than intended.

Risky pattern:

Code
[HttpPut("{id:int}")]
public async Task<IActionResult> UpdateCustomer(int id, Customer customer)
{
    if (id != customer.Id)
    {
        return BadRequest();
    }

    _dbContext.Customers.Update(customer);
    await _dbContext.SaveChangesAsync();

    return NoContent();
}

Problems:

  • The API accepts an entity directly instead of a DTO.
  • The client might omit properties.
  • Omitted properties may overwrite database values.
  • More columns may be updated than necessary.
  • Related entities in the graph may also be marked modified.
  • It can bypass business rules.

Safer pattern:

Code
[HttpPut("{id:int}")]
public async Task<IActionResult> UpdateCustomer(int id, UpdateCustomerRequest request, CancellationToken cancellationToken)
{
    var customer = await _dbContext.Customers
        .SingleOrDefaultAsync(c => c.Id == id, cancellationToken);

    if (customer is null)
    {
        return NotFound();
    }

    customer.Name = request.Name;
    customer.Email = request.Email;

    await _dbContext.SaveChangesAsync(cancellationToken);

    return NoContent();
}

This pattern allows EF Core to track only actual changes and keeps update logic explicit.

Remove

Remove marks an entity as Deleted.

Code
var customer = await dbContext.Customers
    .SingleAsync(c => c.Id == customerId, cancellationToken);

dbContext.Customers.Remove(customer);

await dbContext.SaveChangesAsync(cancellationToken);

If the entity is not tracked, Remove first attaches it and then marks it as deleted:

Code
var customer = new Customer { Id = customerId };

dbContext.Customers.Remove(customer);

await dbContext.SaveChangesAsync(cancellationToken);

This can be useful for deleting by key without loading the entity, but it should be used carefully when business rules require checking the current database state.

Attach vs Update

Attach and Update are common interview comparison points.

APIInitial stateTypical purpose
AttachUnchangedTrack an existing entity without updating it yet
UpdateModifiedMark an existing disconnected entity as changed
AddAddedInsert a new entity
RemoveDeletedDelete an entity

Example:

Code
var customer = new Customer { Id = 1, Name = "Alice" };

dbContext.Attach(customer);
// State: Unchanged

dbContext.Update(customer);
// State: Modified

Use Attach when you want more control. Use Update only when you are comfortable treating the supplied entity or graph as the full updated state.

Graph Tracking Behavior

Add, Attach, and Update can apply to an entire graph of related entities.

Example:

Code
var order = new Order
{
    Id = 10,
    CustomerId = 1,
    Lines =
    {
        new OrderLine { Id = 100, ProductId = 5, Quantity = 2 },
        new OrderLine { ProductId = 6, Quantity = 1 }
    }
};

dbContext.Orders.Update(order);

Depending on key values and configuration, EF Core may treat some related entities as existing and others as new. With generated keys, an unset key value often indicates a new entity.

This is useful, but it can be risky for API update endpoints. A client-provided graph might unintentionally insert, update, or delete related records.

For complex aggregate updates, a safer approach is often:

  1. Load the aggregate from the database.
  2. Apply the command or DTO intentionally.
  3. Add, update, or remove child entities according to business rules.
  4. Call SaveChangesAsync.

Generated Keys and Temporary Keys

For entities with generated keys, EF Core may assign temporary key values before the database generates the real values.

Example:

Code
var customer = new Customer
{
    Name = "Alice"
};

dbContext.Customers.Add(customer);

Console.WriteLine(customer.Id); // May be temporary internally before save

await dbContext.SaveChangesAsync(cancellationToken);

Console.WriteLine(customer.Id); // Real database-generated ID

In disconnected graph scenarios, generated keys help EF Core distinguish new entities from existing entities. If a generated key has its default value, EF Core can infer that the entity is new.

Property-Level Updates

EF Core can update only selected properties when it knows exactly what changed.

Tracked entity pattern:

Code
var customer = await dbContext.Customers
    .SingleAsync(c => c.Id == request.Id, cancellationToken);

customer.Name = request.Name;

await dbContext.SaveChangesAsync(cancellationToken);

Only changed properties are marked modified.

Manual property update pattern:

Code
var customer = new Customer { Id = request.Id };

dbContext.Attach(customer);

customer.Name = request.Name;

dbContext.Entry(customer)
    .Property(c => c.Name)
    .IsModified = true;

await dbContext.SaveChangesAsync(cancellationToken);

This avoids a preliminary query but requires careful handling of validation, authorization, concurrency, and missing data.

CurrentValues and OriginalValues

Each tracked entity has an EntityEntry that exposes current and original values.

Example:

Code
var entry = dbContext.Entry(customer);

var currentName = entry.CurrentValues[nameof(Customer.Name)];
var originalName = entry.OriginalValues[nameof(Customer.Name)];

A useful DTO update pattern is SetValues:

Code
var customer = await dbContext.Customers
    .SingleAsync(c => c.Id == request.Id, cancellationToken);

dbContext.Entry(customer).CurrentValues.SetValues(request);

await dbContext.SaveChangesAsync(cancellationToken);

This works best when the DTO property names match entity property names. However, many teams prefer explicit assignment to avoid accidental updates to fields that should not be client-controlled.

Debugging Change Tracker State

ChangeTracker.DebugView can help explain what EF Core will save.

Code
dbContext.ChangeTracker.DetectChanges();

Console.WriteLine(dbContext.ChangeTracker.DebugView.LongView);

This is useful when debugging:

  • Why an update did not happen.
  • Why an unexpected insert happened.
  • Why too many columns are updated.
  • Why an entity is still Unchanged.
  • Why a duplicate tracking exception occurs.
  • Which properties are marked as modified.

You can also inspect entries:

Code
var entries = dbContext.ChangeTracker.Entries()
    .Select(e => new
    {
        Entity = e.Entity.GetType().Name,
        State = e.State
    })
    .ToList();

Tracking vs No-Tracking Performance

Tracking has overhead because EF Core must store snapshots and manage entity state.

Use tracking when:

  • You intend to modify entities.
  • You need EF Core to detect changes.
  • You need identity resolution inside the context.
  • You are working in a command/update flow.

Use no-tracking when:

  • The result is read-only.
  • The result is projected to a DTO.
  • The query is used for reports or search.
  • The result set is large and does not need to be updated.

Example projection:

Code
var customers = await dbContext.Customers
    .AsNoTracking()
    .Select(c => new CustomerListItemDto
    {
        Id = c.Id,
        Name = c.Name,
        Email = c.Email
    })
    .ToListAsync(cancellationToken);

For many read APIs, projection plus AsNoTracking is a good default.

Disconnected Entities in Web APIs

Web APIs commonly receive DTOs from clients. These DTOs are disconnected from the DbContext.

Example request:

Code
public sealed record UpdateProductRequest(
    string Name,
    decimal Price);

Recommended update flow:

Code
public async Task UpdateProductAsync(
    int productId,
    UpdateProductRequest request,
    CancellationToken cancellationToken)
{
    var product = await dbContext.Products
        .SingleOrDefaultAsync(p => p.Id == productId, cancellationToken);

    if (product is null)
    {
        throw new KeyNotFoundException("Product was not found.");
    }

    product.Name = request.Name;
    product.Price = request.Price;

    await dbContext.SaveChangesAsync(cancellationToken);
}

This approach is clear, safe, and easy to validate.

Alternative disconnected update:

Code
var product = new Product
{
    Id = productId,
    Name = request.Name,
    Price = request.Price
};

dbContext.Products.Update(product);

await dbContext.SaveChangesAsync(cancellationToken);

This is shorter but less precise. It can be acceptable for internal tools or simple full-replacement commands, but it is risky for public APIs where partial data, authorization, and overposting matter.

Overposting Risk

Overposting happens when a client can update fields that should not be client-controlled.

Risky entity binding:

Code
public sealed class User
{
    public int Id { get; set; }
    public string DisplayName { get; set; } = "";
    public bool IsAdmin { get; set; }
}

[HttpPut("{id:int}")]
public async Task<IActionResult> UpdateUser(int id, User user)
{
    _dbContext.Users.Update(user);
    await _dbContext.SaveChangesAsync();
    return NoContent();
}

A malicious client could set IsAdmin = true.

Safer DTO:

Code
public sealed record UpdateUserRequest(string DisplayName);

[HttpPut("{id:int}")]
public async Task<IActionResult> UpdateUser(
    int id,
    UpdateUserRequest request,
    CancellationToken cancellationToken)
{
    var user = await _dbContext.Users
        .SingleOrDefaultAsync(u => u.Id == id, cancellationToken);

    if (user is null)
    {
        return NotFound();
    }

    user.DisplayName = request.DisplayName;

    await _dbContext.SaveChangesAsync(cancellationToken);

    return NoContent();
}

DTOs define the request contract and prevent accidental persistence of fields that should not be writable.

Change Tracker and Relationships

EF Core also tracks relationship changes.

Example:

Code
var order = await dbContext.Orders
    .Include(o => o.Lines)
    .SingleAsync(o => o.Id == orderId, cancellationToken);

order.Lines.Add(new OrderLine
{
    ProductId = productId,
    Quantity = 2
});

await dbContext.SaveChangesAsync(cancellationToken);

EF Core detects the new child entity and inserts it.

Changing a reference can also update a foreign key:

Code
order.CustomerId = newCustomerId;

await dbContext.SaveChangesAsync(cancellationToken);

Relationship fix-up means EF Core can synchronize foreign key values and navigation properties for tracked entities. This is helpful, but it can be confusing when many entities are tracked in a long-lived context.

SaveChanges Behavior

SaveChanges and SaveChangesAsync are the point where tracked changes are converted into database commands.

Conceptually, EF Core does the following:

  1. Detect changes.
  2. Determine entity states and modified properties.
  3. Generate insert, update, and delete commands.
  4. Execute commands.
  5. Accept changes if save succeeds.
  6. Mark saved entities as Unchanged.

Example:

Code
dbContext.Customers.Add(new Customer { Name = "Alice" });

var existing = await dbContext.Products
    .SingleAsync(p => p.Id == 5, cancellationToken);

existing.Price = 100m;

await dbContext.SaveChangesAsync(cancellationToken);

The same SaveChangesAsync call can insert the new customer and update the product.

DbContext Is Not Thread-Safe

A single DbContext instance should not be used concurrently from multiple threads.

Bad pattern:

Code
await Task.WhenAll(
    ProcessCustomerAsync(dbContext, 1),
    ProcessCustomerAsync(dbContext, 2));

Better pattern:

Code
await Task.WhenAll(
    ProcessCustomerWithNewContextAsync(1),
    ProcessCustomerWithNewContextAsync(2));

For background jobs or parallel operations, use separate scopes or an IDbContextFactory<TContext>.

Example:

Code
public sealed class ProductWorker
{
    private readonly IDbContextFactory<AppDbContext> _contextFactory;

    public ProductWorker(IDbContextFactory<AppDbContext> contextFactory)
    {
        _contextFactory = contextFactory;
    }

    public async Task ProcessAsync(int productId, CancellationToken cancellationToken)
    {
        await using var dbContext = await _contextFactory.CreateDbContextAsync(cancellationToken);

        var product = await dbContext.Products
            .SingleAsync(p => p.Id == productId, cancellationToken);

        product.LastProcessedUtc = DateTime.UtcNow;

        await dbContext.SaveChangesAsync(cancellationToken);
    }
}

Common Mistakes

Common mistakes include:

  • Using a singleton DbContext.
  • Sharing one DbContext across parallel tasks.
  • Calling Update on a client-provided entity without checking overposting risk.
  • Mixing a tracked entity and a detached entity with the same key.
  • Using AsNoTracking and expecting automatic updates.
  • Attaching an entity already tracked by the same context.
  • Forgetting that Update can mark an entire graph as modified.
  • Disabling AutoDetectChangesEnabled and forgetting to re-enable it.
  • Keeping a context alive too long and seeing stale data.
  • Exposing EF Core entities directly as API request models.
  • Calling SaveChanges inside every repository method instead of coordinating one unit of work.
  • Not passing CancellationToken to async EF Core operations.
  • Ignoring concurrency control when updating disconnected entities.

Best Practices

Good EF Core tracking habits:

  • Use short-lived DbContext instances.
  • Let dependency injection manage scoped contexts in typical ASP.NET Core requests.
  • Use tracking queries for command/update flows.
  • Use AsNoTracking and DTO projections for read-only queries.
  • Prefer DTOs over binding API requests directly to EF Core entities.
  • Prefer query-then-update for business-critical updates.
  • Use Attach plus property-level IsModified only when you intentionally want a partial update without loading.
  • Use Update carefully for full replacement scenarios.
  • Inspect ChangeTracker.DebugView.LongView when behavior is confusing.
  • Avoid parallel operations on the same context.
  • Keep transaction boundaries and SaveChanges boundaries clear.
  • Understand graph behavior before using Add, Attach, or Update on aggregate roots.
  • Use concurrency tokens when multiple users or processes may update the same row.
  • Keep persistence logic explicit enough that future maintainers can see what is being changed.

Interview Practice

PreviousData Seeding Choices, Including HasData vs Runtime Seed LogicNext UpDbContext Lifetime, Thread Safety, Connection Usage, and When to Avoid Sharing a Context