15

I'm currently in the process of converting my 6 years old C# application to .NET Core v3 and EF Core (and also using Blazor). Most of it is working except for the Sharding part.
Our application creates a new database for each client. We use more or less this code for it: https://learn.microsoft.com/en-us/azure/sql-database/sql-database-elastic-scale-use-entity-framework-applications-visual-studio
I'm now trying to convert it to EF Core, but get stuck at this part:

        // C'tor to deploy schema and migrations to a new shard
        protected internal TenantContext(string connectionString)
            : base(SetInitializerForConnection(connectionString))
        {
        }

        // Only static methods are allowed in calls into base class c'tors
        private static string SetInitializerForConnection(string connnectionString)
        {
            // We want existence checks so that the schema can get deployed
            Database.SetInitializer<TenantContext<T>>(new CreateDatabaseIfNotExists<TenantContext<T>>());
            return connnectionString;
        }

        // C'tor for data dependent routing. This call will open a validated connection routed to the proper
        // shard by the shard map manager. Note that the base class c'tor call will fail for an open connection
        // if migrations need to be done and SQL credentials are used. This is the reason for the 
        // separation of c'tors into the DDR case (this c'tor) and the internal c'tor for new shards.
        public TenantContext(ShardMap shardMap, T shardingKey, string connectionStr)
            : base(CreateDDRConnection(shardMap, shardingKey, connectionStr), true /* contextOwnsConnection */)
        {
        }

        // Only static methods are allowed in calls into base class c'tors
        private static DbConnection CreateDDRConnection(ShardMap shardMap, T shardingKey, string connectionStr)
        {
            // No initialization
            Database.SetInitializer<TenantContext<T>>(null);

            // Ask shard map to broker a validated connection for the given key
            var conn = shardMap.OpenConnectionForKey<T>(shardingKey, connectionStr, ConnectionOptions.Validate);
            return conn;
        }

The above code doesn't compile because the Database object doesn't exist in this way in EF Core. I assume I can simplify it using TenantContext.Database.EnsureCreated(); somewhere. But I can't figure out how to modify the methods, which to remove, which to change (and how).

Of course, I've been searching for an example using sharding and EF Core but couldn't find it. Does anybody here has done this before in EF Core and is willing the share?

I'm specifically looking for what to put in startup.cs and how to create a new sharding/database when I create a new client.

Paul Meems
  • 3,002
  • 4
  • 35
  • 66
  • Hey Paul, are you asking how to automatically migrate a database using EF Core? Or are you having a specific error or issue creating a sharded connection in EF Core? – Code Slinger May 06 '20 at 15:47
  • Hi Mark, I've updated my post. I can't compile the EF code because the Database object doesn't exists in EF Core. – Paul Meems May 06 '20 at 18:50
  • Be careful what you wish for. After MONTHS of trying to get EfCore working, I am now moving back to Ef classic - which is available on .NET Core. Too many limitations in the generated SQL, which got a lot worse in 3.1 thanks to "oh, we od not even TRY to evaluate on the client". – TomTom May 06 '20 at 19:09
  • Thanks @TomTom for the warning. I agree. We started converting to .NET Core v3 en EF Core v3 expecting v3 means is it quite mature. But if you work a little bit out of the box it is hard to get it working. I also spend weeks on integrating with MS Identity, couldn't get it to work. We're using Google now to login. Sharding is also something like that, no sample code – Paul Meems May 07 '20 at 06:20
  • @PaulMeems Have you solved the problem? We still have similar issues. – Milan M. Feb 10 '21 at 09:18
  • Here's me wrestling with the same thing in 2016: https://stackoverflow.com/questions/39298630/sharding-strategies-using-entity-framework-core – Homr Zodyssey Feb 08 '22 at 14:28

2 Answers2

7

In EF.Core just resolve the shard in OnConfiguring. EG

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    var con = GetTenantConnection(this.tenantName);

    optionsBuilder.UseSqlServer(con,o => o.UseRelationalNulls());

    base.OnConfiguring(optionsBuilder);
}

Note that if you have a service or factory that returns open DbConnections, then you'll need to Close()/Dispose() them in the DbContext.Dispose(). If you get a connection string or a closed connection then DbContext will take care of closing the connection.

ASP.NET Core best-practices probably call for injecting an ITenantConfiguration service or somesuch in your DbContext. But the pattern is the same. Just save the injected service instance to a DbContext field and use it in OnConfiguring.

David Browne - Microsoft
  • 80,331
  • 6
  • 39
  • 67
  • 2
    Thanks @david-browne-microsoft I'm also learning .NET Core in the process, making it hard for me to understand how to implement all parts. Do you have a fully working example and/or some links to documentation? – Paul Meems May 07 '20 at 06:23
  • What are you supposed to do if `GetTenantConnection()` is async? – Phil Dec 10 '20 at 04:18
  • 1
    @david-browne-microsoft. I don't see any response here. Do you have a fully working example and/or some links to documentation? – Milan M. Feb 10 '21 at 09:20
0

With the app that I'm working on, the desired shard is not discoverable until request time (for example, knowing what user is making the request, and then routing that user to their database). This meant that the OnConfiguring solution proposed above was not viable.

I worked around this by using IDbContextFactory<TContext>, and defining an extension on top of it, which sets the connection string based on whatever you want. I believe the database connection is created lazily in EF, and you are able to set the connection string up until the EF first needs to actually connect to the database.

In my case, it looked something like this:

var dbContext = _dbContextFactory.CreateDbContext();
var connectionString = $"DataSource={_sqlliteDirectory}/tenant_{tenant.TenantId}.db";

dbContext.Database.SetConnectionString(connectionString);

The downside is that it breaks the database abstraction (this code knows that my database is a local sqllite instance). An abstraction was not necessary in this layer of my app, but it is something very solvable if it's required.

Michael Fry
  • 1,090
  • 9
  • 12