Skip to content

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:

  1. Entity Framework Core configured in your project
  2. Raiqub.Expressions.EntityFrameworkCore package installed
  3. 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:

csharp
// 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

csharp
// 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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

bash
# 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

csharp
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

Released under the MIT License.