Using Multiple DbContexts in a Single Query Execution
Tags: csharp, dotnet, microservices, oop
Published: 2024-06-30 09:34:05

Introduction to DbContexts

This article delves into how to manage and use multiple DbContexts in a single query execution efficiently. We will explore various techniques, their pros and cons, and provide ample code examples to illustrate the concepts.

One well-liked Object-Relational Mapping (ORM) framework for.NET applications is called Entity Framework (EF). It removes the requirement for the majority of the data access code that developers typically have to write by enabling developers to interface with databases using.NET objects. The DbContext, a session with the database that can be used to query and save data, is the main idea behind Entity Framework.

In many applications, you might find yourself needing to interact with multiple databases. This can be due to various reasons such as microservices architecture, multiple data sources, or legacy systems. The challenge arises when you need to perform a single operation that spans multiple databases, each represented by its own DbContext.

Why Use Multiple DbContexts?

Before diving into the implementation details, let's understand the scenarios where multiple DbContexts might be necessary:

Microservices Architecture: In a microservices architecture, each service typically has its own database. When building a service that aggregates data from multiple services, you need to handle multiple DbContexts.

Multiple Data Sources: Sometimes, applications need to aggregate data from different databases, possibly even different types of databases (SQL Server, MySQL, PostgreSQL, etc.).

Legacy Systems: In scenarios where legacy systems are still in use, and you need to integrate them with newer systems, multiple DbContexts might be necessary to handle the different data sources.

Modular Applications: Large applications are often broken into modules, each with its own database for better maintainability and scalability.

Setting Up Multiple DbContexts

To work with multiple DbContexts, you first need to set up your Entity Framework models and contexts. Let's consider an example where we have two databases: SalesDb and HRDb.

Step 1: Define Your Entity Models

First, define the entities for each context. For example, SalesDb might have Customer and Order entities, while HRDb might have Employee and Department entities.

```
// SalesDb Entities

public class Customer
{
    public int CustomerId { get; set; }
    public string Name { get; set; }
    public ICollection<Order> Orders { get; set; }
}

public class Order
{
    public int OrderId { get; set; }
    public int CustomerId { get; set; }
    public DateTime OrderDate { get; set; }
    public Customer Customer { get; set; }
}

// HRDb Entities

public class Employee
{
    public int EmployeeId { get; set; }
    public string Name { get; set; }
    public int DepartmentId { get; set; }
    public Department Department { get; set; }
}

public class Department
{
    public int DepartmentId { get; set; }
    public string DepartmentName { get; set; }
    public ICollection<Employee> Employees { get; set; }
}
```

Step 2: Define Your DbContexts

Next, define the DbContexts for each database.

```
// SalesDbContext

public class SalesDbContext : DbContext
{
    public DbSet<Customer> Customers { get; set; }
    public DbSet<Order> Orders { get; set; }

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

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Fluent API configurations
    }
}

// HRDbContext

public class HRDbContext : DbContext
{
    public DbSet<Employee> Employees { get; set; }
    public DbSet<Department> Departments { get; set; }

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

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Fluent API configurations
    }
}

```
Configuring Dependency Injection

To use these DbContexts in your application, configure them in the Startup.cs (for ASP.NET Core applications).

```
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Configure SalesDbContext
        services.AddDbContext<SalesDbContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("SalesDbConnection")));

        // Configure HRDbContext
        services.AddDbContext<HRDbContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("HRDbConnection")));

        // Add other services
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}
```

Querying with Multiple DbContexts

Let's explore different ways to query data using multiple DbContexts.

Method 1: Using Separate Context Instances

The simplest way to handle multiple DbContexts is to use them separately. This method involves querying each context independently and then combining the results.

```
public class MultiContextService
{
    private readonly SalesDbContext _salesDbContext;
    private readonly HRDbContext _hrDbContext;

    public MultiContextService(SalesDbContext salesDbContext, HRDbContext hrDbContext)
    {
        _salesDbContext = salesDbContext;
        _hrDbContext = hrDbContext;
    }

    public async Task<IEnumerable<EmployeeOrderInfo>> GetEmployeeOrderInfoAsync()
    {
        // Query SalesDbContext
        var orders = await _salesDbContext.Orders.ToListAsync();

        // Query HRDbContext
        var employees = await _hrDbContext.Employees.Include(e => e.Department).ToListAsync();

        // Combine the results
        var employeeOrderInfo = from e in employees
                                join o in orders on e.EmployeeId equals o.CustomerId into eo
                                from order in eo.DefaultIfEmpty()
                                select new EmployeeOrderInfo
                                {
                                    EmployeeName = e.Name,
                                    Department = e.Department.DepartmentName,
                                    OrderDate = order?.OrderDate
                                };

        return employeeOrderInfo;
    }
}

public class EmployeeOrderInfo
{
    public string EmployeeName { get; set; }
    public string Department { get; set; }
    public DateTime? OrderDate { get; set; }
}
```

Method 2: Using a TransactionScope

When you need to ensure data consistency across multiple DbContexts, using a TransactionScope is a good approach. This allows you to wrap multiple database operations in a single transaction.

```
public async Task PerformTransactionalOperationAsync()
{
    using (var transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
    {
        try
        {
            // Perform operations on SalesDbContext
            var customer = new Customer { Name = "John Doe" };
            _salesDbContext.Customers.Add(customer);
            await _salesDbContext.SaveChangesAsync();

            // Perform operations on HRDbContext
            var employee = new Employee { Name = "John Doe", DepartmentId = 1 };
            _hrDbContext.Employees.Add(employee);
            await _hrDbContext.SaveChangesAsync();

            // Complete the transaction
            transaction.Complete();
        }
        catch (Exception)
        {
            // Handle exceptions
            throw;
        }
    }
}

```
Handling Complex Scenarios

Cross-Context Data Consistency

Ensuring data consistency across multiple DbContexts can be challenging. Using TransactionScope is one approach, but it might not always be sufficient, especially in distributed systems. In such cases, you might need to implement a two-phase commit or use a distributed transaction coordinator (DTC).

Performance Considerations

Querying multiple databases can have performance implications. It's essential to consider the performance impact and optimize your queries and data access patterns. Techniques such as caching, asynchronous operations, and minimizing the number of database calls can help improve performance.

Error Handling

Error handling becomes more complex when dealing with multiple DbContexts. Ensure you have robust error handling mechanisms in place to manage partial failures and maintain data consistency.

Advanced Techniques

Using Repository and Unit of Work Patterns

Implementing the repository and unit of work patterns can help manage multiple DbContexts more effectively. These patterns provide a clean abstraction over the data access layer and help in managing transactions and data consistency.

```
public interface IUnitOfWork : IDisposable
{
    ISalesRepository SalesRepository { get; }
    IHRRepository HRRepository { get; }
    Task<int> SaveChangesAsync();
}

public class UnitOfWork : IUnitOfWork
{
    private readonly SalesDbContext _salesDbContext;
    private readonly HRDbContext _hrDbContext;
    private ISalesRepository _salesRepository;
    private IHRRepository _hrRepository;

    public UnitOfWork(SalesDbContext salesDbContext, HRDbContext hrDbContext)
    {
        _salesDbContext = salesDbContext;
        _hrDbContext = hrDbContext;
    }

    public ISalesRepository SalesRepository =>
        _salesRepository ??= new SalesRepository(_salesDbContext);

    public IHRRepository HRRepository =>
        _hrRepository ??= new HRRepository(_hrDbContext);

    public async Task<int> SaveChangesAsync()
    {
        using (var transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
        {
            try
            {
                var salesResult = await _salesDbContext.SaveChangesAsync();
                var

 hrResult = await _hrDbContext.SaveChangesAsync();
                transaction.Complete();
                return salesResult + hrResult;
            }
            catch
            {
                // Handle exceptions
                throw;
            }
        }
    }

    public void Dispose()
    {
        _salesDbContext?.Dispose();
        _hrDbContext?.Dispose();
    }
}
```

CQRS Pattern

The Command Query Responsibility Segregation (CQRS) pattern can also be beneficial when working with multiple DbContexts. It separates the read and write operations, which can be particularly useful for complex scenarios with multiple databases.

Conclusion

Using multiple DbContexts in a single query execution can be complex but is often necessary for modern applications dealing with multiple data sources. By understanding the scenarios where multiple DbContexts are needed and implementing the techniques discussed in this article, you can effectively manage and query data across multiple databases.

Key takeaways include:

Understanding the scenarios where multiple DbContexts are necessary.

Setting up and configuring multiple DbContexts.

Querying data using separate context instances and using TransactionScope for data consistency.

Handling complex scenarios and optimizing performance.

Implementing advanced techniques like repository and unit of work patterns and the CQRS pattern.

With these strategies, you can build robust and scalable applications that handle multiple data sources efficiently.