Entity Framework Core Bounded Contexts
This guide explains how to configure and use bounded contexts with Entity Framework Core in Raiqub Expressions.
Prerequisites
Before working with bounded contexts, ensure you have:
- Entity Framework Core configured in your project
- Raiqub.Expressions.EntityFrameworkCore package installed
- DbContext classes defined for your contexts
Registering Bounded Contexts
Single DbContext per Context
The most common approach is to have separate DbContext classes for each bounded context:
// Define bounded context interfaces
public interface ICatalogContext { }
public interface ISalesContext { }
public interface IReportingContext { }
// Define DbContext classes
public class CatalogDbContext : DbContext, ICatalogContext
{
public CatalogDbContext(DbContextOptions<CatalogDbContext> options)
: base(options)
{
}
public DbSet<Product> Products { get; set; }
public DbSet<Category> Categories { get; set; }
}
public class SalesDbContext : DbContext, ISalesContext
{
public SalesDbContext(DbContextOptions<SalesDbContext> options)
: base(options)
{
}
public DbSet<Order> Orders { get; set; }
public DbSet<Customer> Customers { get; set; }
}
public class ReportingDbContext : DbContext, IReportingContext
{
public ReportingDbContext(DbContextOptions<ReportingDbContext> options)
: base(options)
{
}
// Read-only views or entities for reporting
public DbSet<OrderSummary> OrderSummaries { get; set; }
}
Register with Dependency Injection
// Register DbContext factories
services.AddDbContextFactory<CatalogDbContext>(options =>
options.UseSqlServer(catalogConnectionString));
services.AddDbContextFactory<SalesDbContext>(options =>
options.UseSqlServer(salesConnectionString));
services.AddDbContextFactory<ReportingDbContext>(options =>
options.UseSqlServer(reportingConnectionString)
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));
// Register bounded context sessions
services.AddEntityFrameworkExpressions()
.AddContext<ICatalogContext, CatalogDbContext>()
.AddContext<ISalesContext, SalesDbContext>()
.AddContext<IReportingContext, ReportingDbContext>(ChangeTracking.Disabled);
Multiple Contexts Sharing Single DbContext
In some scenarios, you might want multiple logical contexts sharing a single DbContext:
public class ApplicationDbContext : DbContext,
ICatalogContext,
ISalesContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
// Catalog entities
public DbSet<Product> Products { get; set; }
public DbSet<Category> Categories { get; set; }
// Sales entities
public DbSet<Order> Orders { get; set; }
public DbSet<Customer> Customers { get; set; }
}
// Register
services.AddDbContextFactory<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
services.AddEntityFrameworkExpressions()
.AddContext<ICatalogContext, ApplicationDbContext>()
.AddContext<ISalesContext, ApplicationDbContext>();
Configuration Options
Change Tracking
Configure change tracking behavior per context:
services.AddEntityFrameworkExpressions()
// Write context with tracking enabled
.AddContext<ICatalogContext, CatalogDbContext>(ChangeTracking.Enabled)
// Read-only context with tracking disabled
.AddContext<IReportingContext, ReportingDbContext>(ChangeTracking.Disabled);
Query Configuration
Configure query-specific options per entity type within a context:
services.AddEntityFrameworkExpressions()
.Configure<Product>(options =>
{
options.UseSplitQuery = true; // Use split queries for Product
})
.Configure<Order>(options =>
{
options.UseSplitQuery = true; // Use split queries for Order
})
.AddContext<ICatalogContext, CatalogDbContext>()
.AddContext<ISalesContext, SalesDbContext>();
Using Bounded Context Sessions
Injecting Sessions
Inject the typed session interfaces into your services:
public class CatalogService
{
private readonly IDbSession<ICatalogContext> _session;
private readonly ILogger<CatalogService> _logger;
public CatalogService(
IDbSession<ICatalogContext> session,
ILogger<CatalogService> logger)
{
_session = session;
_logger = logger;
}
public async Task<Product> CreateProductAsync(
Product product,
CancellationToken cancellationToken = default)
{
_session.Add(product);
await _session.SaveChangesAsync(cancellationToken);
return product;
}
public async Task<IReadOnlyList<Product>> GetProductsAsync(
CancellationToken cancellationToken = default)
{
var query = _session.Query<Product>();
return await query.ToListAsync(cancellationToken);
}
}
Using Session Factories
For scenarios requiring explicit session creation:
public class OrderService
{
private readonly IDbSessionFactory<ISalesContext> _sessionFactory;
private readonly ILogger<OrderService> _logger;
public OrderService(
IDbSessionFactory<ISalesContext> sessionFactory,
ILogger<OrderService> logger)
{
_sessionFactory = sessionFactory;
_logger = logger;
}
public async Task ProcessOrderAsync(
Guid orderId,
CancellationToken cancellationToken = default)
{
await using var session = _sessionFactory.Create();
var query = session.Query<Order>().Where(o => o.Id == orderId);
var order = await query.FirstAsync(cancellationToken);
order.Status = OrderStatus.Processed;
session.Update(order);
await session.SaveChangesAsync(cancellationToken);
}
}
Connection Strings and Database Separation
Separate Databases
Use different connection strings for complete database isolation:
services.AddDbContextFactory<CatalogDbContext>(options =>
options.UseSqlServer("Server=localhost;Database=Catalog;..."));
services.AddDbContextFactory<SalesDbContext>(options =>
options.UseSqlServer("Server=localhost;Database=Sales;..."));
services.AddEntityFrameworkExpressions()
.AddContext<ICatalogContext, CatalogDbContext>()
.AddContext<ISalesContext, SalesDbContext>();
Same Database, Different Schemas
Use database schemas to separate contexts within a single database:
public class CatalogDbContext : DbContext, ICatalogContext
{
public CatalogDbContext(DbContextOptions<CatalogDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("catalog");
modelBuilder.Entity<Product>().ToTable("Products", "catalog");
modelBuilder.Entity<Category>().ToTable("Categories", "catalog");
}
public DbSet<Product> Products { get; set; }
public DbSet<Category> Categories { get; set; }
}
public class SalesDbContext : DbContext, ISalesContext
{
public SalesDbContext(DbContextOptions<SalesDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("sales");
modelBuilder.Entity<Order>().ToTable("Orders", "sales");
modelBuilder.Entity<Customer>().ToTable("Customers", "sales");
}
public DbSet<Order> Orders { get; set; }
public DbSet<Customer> Customers { get; set; }
}
// Both use same connection string but different schemas
var connectionString = "Server=localhost;Database=MyApp;...";
services.AddDbContextFactory<CatalogDbContext>(options =>
options.UseSqlServer(connectionString));
services.AddDbContextFactory<SalesDbContext>(options =>
options.UseSqlServer(connectionString));
Cross-Context Queries
Querying Across Contexts
When you need data from multiple contexts, coordinate at the application level:
public class OrderFulfillmentService
{
private readonly IDbSession<ISalesContext> _salesSession;
private readonly IDbQuerySession<ICatalogContext> _catalogSession;
public OrderFulfillmentService(
IDbSession<ISalesContext> salesSession,
IDbQuerySession<ICatalogContext> catalogSession)
{
_salesSession = salesSession;
_catalogSession = catalogSession;
}
public async Task<OrderFulfillmentResult> FulfillOrderAsync(
Guid orderId,
CancellationToken cancellationToken = default)
{
// Get order from sales context
var orderQuery = _salesSession.Query<Order>()
.Where(o => o.Id == orderId);
var order = await orderQuery.FirstAsync(cancellationToken);
var result = new OrderFulfillmentResult { OrderId = orderId };
// Check product availability in catalog context
foreach (var item in order.Items)
{
var productQuery = _catalogSession.Query<Product>()
.Where(p => p.Id == item.ProductId);
var product = await productQuery.FirstOrDefaultAsync(cancellationToken);
if (product == null || product.AvailableQuantity < item.Quantity)
{
result.Errors.Add($"Product {item.ProductId} not available");
}
}
if (result.Errors.Any())
{
return result;
}
// Update order status
order.Status = OrderStatus.Fulfilled;
_salesSession.Update(order);
await _salesSession.SaveChangesAsync(cancellationToken);
result.Success = true;
return result;
}
}
Transaction Considerations
Important: Each DbContext has its own transaction scope. For operations spanning multiple contexts:
public class CrossContextService
{
private readonly IDbSession<ICatalogContext> _catalogSession;
private readonly IDbSession<ISalesContext> _salesSession;
public CrossContextService(
IDbSession<ICatalogContext> catalogSession,
IDbSession<ISalesContext> salesSession)
{
_catalogSession = catalogSession;
_salesSession = salesSession;
}
public async Task ProcessOrderWithInventoryAsync(
Order order,
CancellationToken cancellationToken = default)
{
// Note: These are separate transactions!
// Consider eventual consistency patterns instead of distributed transactions
try
{
// Update inventory first
foreach (var item in order.Items)
{
var productQuery = _catalogSession.Query<Product>()
.Where(p => p.Id == item.ProductId);
var product = await productQuery.FirstAsync(cancellationToken);
product.AvailableQuantity -= item.Quantity;
_catalogSession.Update(product);
}
await _catalogSession.SaveChangesAsync(cancellationToken);
// Then save order
_salesSession.Add(order);
await _salesSession.SaveChangesAsync(cancellationToken);
}
catch
{
// Implement compensation logic or use Saga pattern
throw;
}
}
}
Migrations
Manage migrations separately for each bounded context:
# Create migration for catalog context
dotnet ef migrations add InitialCreate --context CatalogDbContext --output-dir Migrations/Catalog
# Create migration for sales context
dotnet ef migrations add InitialCreate --context SalesDbContext --output-dir Migrations/Sales
# Apply migrations
dotnet ef database update --context CatalogDbContext
dotnet ef database update --context SalesDbContext
Testing
Integration Tests with Bounded Contexts
public class BoundedContextIntegrationTests : IAsyncLifetime
{
private readonly ServiceProvider _serviceProvider;
private readonly IDbSession<ICatalogContext> _catalogSession;
private readonly IDbSession<ISalesContext> _salesSession;
public BoundedContextIntegrationTests()
{
var services = new ServiceCollection();
// Register test databases
services.AddDbContextFactory<CatalogDbContext>(options =>
options.UseInMemoryDatabase("CatalogTest"));
services.AddDbContextFactory<SalesDbContext>(options =>
options.UseInMemoryDatabase("SalesTest"));
services.AddEntityFrameworkExpressions()
.AddContext<ICatalogContext, CatalogDbContext>()
.AddContext<ISalesContext, SalesDbContext>();
_serviceProvider = services.BuildServiceProvider();
_catalogSession = _serviceProvider.GetRequiredService<IDbSession<ICatalogContext>>();
_salesSession = _serviceProvider.GetRequiredService<IDbSession<ISalesContext>>();
}
public async Task InitializeAsync()
{
// Seed catalog context
_catalogSession.Add(new Product { Id = Guid.NewGuid(), Name = "Test Product" });
await _catalogSession.SaveChangesAsync();
// Seed sales context
_salesSession.Add(new Customer { Id = Guid.NewGuid(), Name = "Test Customer" });
await _salesSession.SaveChangesAsync();
}
[Fact]
public async Task Should_Isolate_Contexts()
{
// Catalog context has products
var catalogQuery = _catalogSession.Query<Product>();
var products = await catalogQuery.ToListAsync();
Assert.NotEmpty(products);
// Sales context has customers
var salesQuery = _salesSession.Query<Customer>();
var customers = await salesQuery.ToListAsync();
Assert.NotEmpty(customers);
}
public async Task DisposeAsync()
{
await _serviceProvider.DisposeAsync();
}
}
Best Practices
Do's
✅ Use separate DbContexts - One DbContext per bounded context
✅ Configure separately - Each context can have its own configuration
✅ Use schemas - Separate contexts using database schemas
✅ Manage migrations separately - Each context has independent migrations
✅ Test isolation - Verify contexts don't interfere with each other
Don'ts
❌ Don't share DbContext - Avoid using a single DbContext for unrelated contexts
❌ Don't use distributed transactions - Prefer eventual consistency
❌ Don't couple contexts - Keep context implementations independent
❌ Don't share connection strings - Unless using schema separation
❌ Don't mix entity definitions - Each context owns its entities
See Also
- Database Session Bounded Contexts - General bounded contexts concepts
- Entity Framework Core - EF Core integration basics
- Custom SQL Query - Custom SQL in EF Core
- Split Query - Split query configuration
- EF Core Documentation - Official EF Core docs