27

I have an Asp.Net Core app with Entity Framework Core that I initialize as follows:

services.AddDbContext<ApplicationDbContext>(options => 
         options.UseSqlServer(sqlConnectionString));

This works fine, but I have a scenario where I need to read/write from the primary database for normal operations but for some operations I need to read from an alternate server (a replication target that's read only that we use for reporting).

With the way the new Core API does everything via Dependency Injection and configuration in StartUp.cs, how do I switch connection strings, but use the same ApplicationDbContext class?

I know that one option would be to have a duplicate of ApplicationDbContext class that I register with the DI system using a different connection string, but I'd like to avoid maintaining two identical DBContext objects just because sometimes I need to read from a different database server (but with the exact same schema).

Thanks in advance for any pointers!

3
  • I'm a bit confused, but you would just like to be able to choose what database to connect right? And this wouldn't change once the app ran? If so then would a condition before options.UseSqlServer suffice? e.g. if(Environment.GetEnvironmentVariable("FIRSTENVIRONMENT") options.UseSqlServer("firstEnvironementConnectionString") else options.UseSqlServer("secondEnvironementConnectionString") Commented Mar 6, 2017 at 2:31
  • Ugghh that 5 mins edit, anyway fixed code here. if(Environment.GetEnvironmentVariable("FIRSTENVIRONMENT") == "environmentString") options.UseSqlServer("firstEnvironementConnectionString") else options.UseSqlServer("secondEnvironementConnectionString") Commented Mar 6, 2017 at 2:37
  • That would set the Connection string at the application level, I need it set at the Method level. In other words, one line of code writes to database A and the next line of code reads from database B. Both use the same schema, just operating on different database. Commented Mar 6, 2017 at 2:39

7 Answers 7

43

You'll need two DbContexts.

public class BloggingContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
}

public class MyBloggingContext : BloggingContext
{

}

public class MyBackupBloggingContext : BloggingContext
{

}

And you can register them like this:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<MyBloggingContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddDbContext<MyBackupBloggingContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("BackupConnection")));

}
Sign up to request clarification or add additional context in comments.

6 Comments

Dangit! So obvious. I was trying to solve it through the EF API instead of just using inheritance. Thanks!
The above approach never works, as we need to pass the parameterized constructor for MyBloggingContext and MyBackUpBloggingContext.
Above approach works with generic T added to base context where T is the new subcontext.
@Siddharth if the parameter is DbContextOptions<xxx> you can use DbContextOptions options in the Base Context an then it should not be a problem. If you need order Parameter how do you handle Dependency Injection?
@Siddharth is right. To make it work we need to do this: use only DbContextOptions for the base (BloggingContext) constructor (not DbContextOptions<BloggingContext>) and DbContextOptions<MyBloggingContext> for MyBloggingContext constructor and DbContextOptions<MyBackupBloggingContext> for MyBackupBloggingContext constructor. Hope this helps everyone.
|
13

Can be done like this(tested with .net core 3.1):

public abstract partial class BloggingContext<T> : DbContext where T : DbContext
{
    private readonly string _connectionString;
    protected BloggingContext(string connectionString) { _connectionString = connectionString; }
    protected BloggingContext(DbContextOptions<T> options) : base(options) { }

    public virtual DbSet<Blog> Blogs { get; set; }
    public virtual DbSet<Post> Posts { get; set; } 

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (!optionsBuilder.IsConfigured)
        {
            optionsBuilder.UseSqlServer(_connectionString);
        }
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
    ...
    }
}

public class MyBloggingContext : BloggingContext<MyBloggingContext>
{
    public MyBloggingContext(string connectionString) : base(connectionString) { }
    public MyBloggingContext(DbContextOptions<MyBloggingContext> options) : base(options) { }
}

public class MyBackupBloggingContext : BloggingContext<MyBackupBloggingContext>
{
    public MyBackupBloggingContext(string connectionString) : base(connectionString) { }
    public MyBackupBloggingContext(DbContextOptions<MyBackupBloggingContext> options) : base(options) { }
}

And in Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<MyBloggingContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
    
    services.AddDbContext<MyBackupBloggingContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("BackupConnection")));

}

1 Comment

On the public class calls , I'm getting _connection string is inaccessible due to its protection level, method must have a return type, does not contain a constructor that takes 0 arguments, and can not resolve symbol T errors
10

Connection string can be resolved using IServiceProvider. In the example below I map query parameter to configuration from appsettings.json, but you could inject any other logic you want.

services.AddDbContext<ApplicationDbContext>((services, optionsBuilder) =>
{
    var httpContextAccessor = services.GetService<IHttpContextAccessor>();
    var requestParam = httpContextAccessor.HttpContext.Request.Query["database"];

    var connStr = Configuration.GetConnectionString(requestParam);

    optionsBuilder.UseSqlServer(connStr);
});

?database=Connection1 and ?database=Connection2 in query will lead to using different connection strings. It is worth to provide default value, when parameter is missing.

Comments

2

It can be resolved in this way

public class AppDbContext : DbContext
{
    private string _connectionString { get; }

    public AppDbContext(string connectionString, DbContextOptions<AppDbContext> options) : base(options)
    {
        _connectionString = connectionString;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(_connectionString);
    }
}

Then create the DbContext manually

var appDbContext = new AppDbContext("server=localhost;database=TestDB;Trusted_Connection=true", new DbContextOptions<AppDbContext>());

Instead of hard coding the connection, read from the connection string factory.

Comments

0

While the answers to this question work properly, they introduce a write-delay given that both contexts have to write data to two servers. An alternative solution is to use database replication. For instance, postgresql both has streaming and logical replication (setup article can be found here), and SQLite has LiteFS and Marmot.

Feel free to search for the replication configuration for your databse of choice, and ideally follow that solution.

1 Comment

Your point only applies when someone wants to write the exact same data to both servers. But if you read the OP's question, data is being written only to the primary server, and only some data is being read from the alternate server. In fact, the OP mentions that the alternate server is a replication target already!
0

First I created the DbContext and I have added 2 DbSets

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

    public virtual DbSet<Employee> Employees { get; set; }
    public virtual DbSet<Department> Departments { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
    }
}

The Models I've been created Employee

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }

    public int DepartmentId { get; set; }
    public Department Department { get; set; }
}

Department

public class Department
{
    public int Id { get; set; }
    public string DepartmentName { get; set; }

    public virtual List<Employee> Employees { get; set; } = new();
}

Go to the controller and create an endpoint that will take connection string name that is needed to switch as a parameter then it will go and check if it exists then it will be changed from the controller

[Route("api/[controller]")]
[ApiController]
public class DbController : ControllerBase
{
    private readonly IConfiguration configuration;
    private readonly IServiceProvider serviceProvider;
    private readonly IConnectionString connectionString;

    public DbController(IConfiguration configuration, IServiceProvider serviceProvider,IConnectionString connectionString)
    {
        this.configuration = configuration;
        this.serviceProvider = serviceProvider;
        this.connectionString = connectionString;
    }
    [HttpGet("change-db")]
    public IActionResult ChangeDB(string connectionStringName)
    {
        string? conString = configuration.GetConnectionString(connectionStringName);

        // Validate the connection string
        if (string.IsNullOrEmpty(conString))
            return BadRequest("Can't find this database");

        var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>();
        optionsBuilder.UseSqlServer(conString);

        var dbContext = new AppDbContext(optionsBuilder.Options);
        connectionString.CurrentConnectionString = dbContext is not null ? connectionStringName : string.Empty;
        
        var initialCatalog = conString.Split(';')
                                      .FirstOrDefault(part => part.StartsWith("Initial Catalog", StringComparison.OrdinalIgnoreCase))?
                                      .Split('=')[1].Trim();

        return Ok($"Switched to {initialCatalog} database.");
    }

This will return "Switched to {Database name} database." if it exists else it will return a Bad request

The IConnectionString is only used to save my connection string name to be used in another controllers The interface

public interface IConnectionString
{
    string CurrentConnectionString { get; set; }
}

The Class

public class ConnectionString : IConnectionString
{
    public string CurrentConnectionString { get; set; } = string.Empty;
}

It must be injected at the Program.cs

builder.Services.AddSingleton<IConnectionString, ConnectionString>();

The Connection string must be the default here and it will be changed after calling the endpoint

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

Comments

-1

This code worked for me but I'm not sure if it is bad design or not.

builder.Services.AddDbContext<MyDbContext>((sp, options) =>
{
    var httpAccessor = sp.GetRequiredService<IHttpContextAccessor>();
    var connectionString = builder.Configuration.GetConnectionString("EntityConnection");
    if (httpAccessor.HttpContext.GetEndpoint().DisplayName.Contains("SomeEndpoint"))
    {
        connectionString = builder.Configuration.GetConnectionString("OtherEntityConnection");
    }
    options.UseSqlServer(connectionString,
        options => options.EnableRetryOnFailure());
    ...etc...

2 Comments

Please don't post answers as questions. You should know if it works. If you're not sure, please don't post it.
@GertArnold I'll rephrase my response

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.