question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Optionally allow a different connection string to be used for SaveChanges

See original GitHub issue

Ask 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:open
  • Created 2 years ago
  • Reactions:3
  • Comments:6 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
rojicommented, Feb 12, 2022

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?

0reactions
rojicommented, Feb 12, 2022

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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Entity Framework change connection at runtime
You can do this on-the-fly with an IDbConnectionInterceptor . This has the advantage of allowing you to work with a standard connection string...
Read more >
Connection Strings and Configuration Files - ADO.NET
Learn how to store connection strings for ADO.NET applications in an application configuration file, as a best practice for security and ...
Read more >
Using Entity Framework Core on Azure Functions with ...
The project showed off various Azure Functions bindings by implementing the REST API against four different backing stores: In-memory; Azure ...
Read more >
Connection String in Entity Framework Core
Connection string contain information about the data source that is being connected. This information varies from provider to provider, ...
Read more >
Working with parameter labels - AWS Systems Manager
Manage different versions of a parameter with parameter labels in Systems Manager.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found