Optionally allow a different connection string to be used for SaveChanges
See original GitHub issueAsk a question
Is there an option in EF Core to define two connection strings to the same SQL Cluster?
- one connection string for ReadWrite queries
- one connection string for Read queries
This can be very efficient, because for example in azure SQL you can access your read-only replicas to increase your query performance without scaling to the next SKU. (a detailed description of the use-case can be found here)
In the include code
section, I document my current solution. In the conclusion section I also documented some downsides of this solution. It would be cool if there is a solution that works with a single DBContext.
Include your code
My current solution looks like this:
define a simple model
public class SomeEntity{
public int Id { get; set; }
public string Title { get; set; }
}
Define a DBContext Interface
This implements just IQueryable because it’s readonly
public interface IReadSampleDbContext{
IQueryable<SomeEntity> SomeEntities { get;}
}
Create write Context
Only implements IReadSampleDbContext to has a complete implementation.
public class SampleDbContext : DbContext, IReadSampleDbContext{
public SampleDbContext([NotNullAttribute] DbContextOptions<SampleDbContext> options) : base(options)
{
}
/// <summary>
/// For ReadWrite Actions
/// </summary>
public DbSet<SomeEntity> SomeEntities { get; set; }
/// <summary>
/// For Read actions only. This is maybe never used. Use the readonly context if you need just query stuff.
/// </summary>
IQueryable<SomeEntity> IReadSampleDbContext.SomeEntities { get => this.SomeEntities; }
}
Create a read only context
public class ReadOnlySampleDbContext : DbContext, IReadSampleDbContext{
public ReadOnlySampleDbContext([NotNullAttribute] DbContextOptions<ReadOnlySampleDbContext> options) : base(options)
{
}
private DbSet<SomeEntity> SomeEntities { get; set; }
IQueryable<SomeEntity> IReadSampleDbContext.SomeEntities => this.SomeEntities;
}
Register both contextes in the startup
services.AddDbContext<SampleDbContext>(s =>
{
s.UseSqlServer(Configuration.GetConnectionString("testDB"));
});
services.AddDbContext<ReadOnlySampleDbContext>(s =>
{
s.UseSqlServer(Configuration.GetConnectionString("testDBRead"));
});
Use it in the Controller
[Route("api/[controller]")]
[ApiController]
public class SomeEntitiesController : ControllerBase
{
private readonly IReadSampleDbContext readSampleDbContext;
private readonly SampleDbContext sampleDbContext;
public SomeEntitiesController(ReadOnlySampleDbContext readSampleDbContext, SampleDbContext sampleDbContext)
{
this.readSampleDbContext = readSampleDbContext;
this.sampleDbContext = sampleDbContext;
}
// GET: api/<SomeEntitiesController>
[HttpGet]
public IEnumerable<SomeEntity> Get()
{
return readSampleDbContext.SomeEntities;
}
// GET api/<SomeEntitiesController>/5
[HttpGet("{id}")]
public SomeEntity Get(int id)
{
return readSampleDbContext.SomeEntities.First(d => d.Id == id);
}
// POST api/<SomeEntitiesController>
[HttpPost()]
public async Task Post([FromBody] SomeEntity value)
{
var entity = new SomeEntity();
entity.Title = value.Title;
sampleDbContext.Add(entity);
await sampleDbContext.SaveChangesAsync();
}
// PUT api/<SomeEntitiesController>/5
[HttpPut("{id}")]
public async Task<ActionResult> Put(int id, [FromBody] SomeEntity value)
{
if(value.Id != id)
{
return Problem("Body and url doesn't match", this.HttpContext.TraceIdentifier, StatusCodes.Status400BadRequest);
}
var entity = sampleDbContext.SomeEntities.First(d => d.Id == id);
entity.Title = value.Title;
await sampleDbContext.SaveChangesAsync();
return Ok();
}
// DELETE api/<SomeEntitiesController>/5
[HttpDelete("{id}")]
public async Task Delete(int id)
{
sampleDbContext.Remove(sampleDbContext.SomeEntities.First(d => d.Id == id));
await sampleDbContext.SaveChangesAsync();
}
}
Conslusion
The current solution has some downsides:
- other developers can use the wrong context
- Much code duplication because you need two contexts with almost the same content
- the ef core tooling think there are two possible contexts to apply migrations
Include provider and version information
EF Core version: 6.0.0-preview.4.21253.1 Database provider: Microsoft.EntityFrameworkCore.SqlServer Target framework: .NET 6.0.100-preview.4.21255.9 Operating system: Windows 10 21H1 IDE: Visual Studio 2019 16.11.0 Preview 1.0
Issue Analytics
- State:
- Created 2 years ago
- Reactions:3
- Comments:6 (4 by maintainers)
First, transactions aren’t necessarily only for read/write - you can use a repeatable read/serializable transaction simply to get consistent reads out of the database, without making any changes. Another issue is raw SQL. Say you’re calling some stored procedure with FromSql; technically, this looks like a query, but as it’s a stored procedure, it could also have side-effects. These may seem like corner cases, but I’m a bit hesitant for EF Core to automatically make decisions here and switch between connection strings under the hood based on what it thinks you’re doing.
How about this… We could say that the context you get is read-only by default (i.e. initialized with the read-only connection string). If you want to do an update operation, you call some method on your DbContext, something like MakeWritable; all this does is set the context’s connection string under the hood - it can even be a user extension method. This provides a clear, explicit user gesture to indicate they want read-write - if they forget it, their query will simply fail (as the read replica won’t execute updates).
How does that sound?
Note that we should be careful about assuming that read-only/read-write is necessarily a distinction made in the connection string; for Npgsql we’re discussing possibly different mechanisms in https://github.com/npgsql/npgsql/issues/4321.