Using multiple dbcontext instances and dependency injection
Asked Answered
M

1

12

This is kind of a similar question I asked here a few weeks ago with one significant change in requirement.

I have a new and unique (I have not found anything like this in my stackoverflow search) business requirement:

I have created two separate entity framework 6 DbContexts that point to two structurally different databases, let us call them PcMaster and PcSubs. While PcMaster is a straight forward database and the PcMasterContext will have a static connection string, PcSubs database is used as a template to create new databases out of. Obviously, since these copied databases will have the same exact structure, the idea is to just change the database (catalog) name in the connection string to point to a different db when the dbcontext is instantiated. I have also used repository pattern and dependency injection (currently Ninject, but thinking of moving to Autofac).

I have not seen an IDbContext interface for DbContext, unless you want to create one yourself. But then, I have seen many saying that it is not a good idea or not the best practice.

Basically, what I want to do is, under certain conditions, not only the application needs to switch between PCMasterContext and PCSubsContext, but also modify the connection string to PCSubsContext if PCSubsContext is the current context. the dbContext I used in the repository needs to point to a different database. I don't know how I can do this with an IoC container such as Ninject or Autofac. Here are some code snippets I have created so far. Help with some real working solutions is highly appreciated.

Here is my interface for the base repository

public interface IPCRepositoryBase<T> where T : class
{
  void Add(T entity);
  void Delete(T entity);
  T FindOne(Expression<Func<T, bool>> predicate);
  IQueryable<T> FindBy(Expression<Func<T, bool>> predicate);
  IQueryable<T> GetAll();
  void SetConnection(string connString);
  //... 
  //...
}

Here is my abstract repository base

public abstract class PCRepositoryBase<T> : IPCRepositoryBase<T>, IDisposable where T : class
{
  protected readonly IDbSet<T> dbSet;
  protected DbContext dbCtx;

  public PCRepositoryBase(DbContext dbCtx)
  {
     this.dbCtx = dbCtx;
     dbSet = dbCtx.Set<T>();
  }
  public void SetConnection(string connString)
  {
     dbCtx.Database.Connection.ConnectionString = connString;
  }
  public IQueryable<T> FindBy(Expression<Func<T, bool>> predicate)
  {
     return dbSet.Where(predicate); // DataContext.Set<T>().Where( predicate  );
  }
  public virtual IQueryable<T> GetAll()
  {
     return dbSet;
  }
  public T FindOne(Expression<Func<T, bool>> predicate)
  {
     return dbSet.SingleOrDefault(predicate);
  }
  //... Not all implementations listed
  //...
}

And now, here is the interface for one of the derived repositories:

public interface ISubscriberRepository : IPCRepositoryBase<Subscriber>
{
  IQueryable<Subscriber> GetByStatusName( PCEnums.enumSubsStatusTypes status   );
  IQueryable<Subscriber> GetByBusinessName( string businessName );
  //...
  //...
}

public class SubscriberRepository : PCRepositoryBase<Subscriber>, ISubscriberRepository
{
  public SubscriberRepository( DbContext context ) : base( context ) { }
  public IQueryable<Subscriber> GetByStatusName( PCEnums.enumSubsStatusTypes    status )
  {
     return FindBy(x => x.SubsStatusType.Name == status.ToString());
  }
  public IQueryable<Subscriber> GetByBusinessName( string businessName )
  {
     return FindBy( s => s.BusinessName.ToUpper() == businessName.ToUpper()  );
  }
  //... other operations omitted for brevity!
}

Now, my PCSubs dbContext generated by the designer:

public partial class PCSubsDBContext : DbContext
{
    public PCSubsDBContext() : base("name=PCSubsDBContext")
    {
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        throw new UnintentionalCodeFirstException();
    }

    public virtual DbSet<Currency> Currencies { get; set; }
    public virtual DbSet<DurationName> DurationNames { get; set; }
    public virtual DbSet<Subscriber> Subscribers { get; set; }
}

Is there a way, I can just use and/or inject one generic dbcontext for both databases along with the connection string for different databases. How would I register the "DbContext" in the Ioc container without a corresponding interface and still be able to inject the connection string at runtime? Some code examples will really help.

Milklivered answered 19/1, 2015 at 19:3 Comment(0)
S
11

The solution is actually quite simple. You need to make sure that your PCSubsDBContext has a constructor that takes in a connection string, connection string name, database name, or something similar. This way you can create the proper PCSubsDBContext based on the context it lives in. What value to inject into its ctor probably depends upon the logged in user, or a certain request. This is something you already know how to do.

How to register depends a bit on your container, but you will typically have to register a delegate for this. This might look like this:

// Autofac
builder.Register<PCSubsDBContext>(c =>
    new PCSubsDBContext(GetConnectionStringForCurrentRequest());

// Ninject
kernel.Bind<PCSubsDBContext>().ToMethod(m =>
    new PCSubsDBContext(GetConnectionStringForCurrentRequest());

// Simple Injector
container.Register<PCSubsDBContext>(() =>
    new PCSubsDBContext(GetConnectionStringForCurrentRequest());

Since creating the context is depending on the availability of a request, it might even be good change the your design a bit so the PCSubsDBContext can be loaded lazily, while you can still build the rest of the object graph without the existence of a web request. This is very valuable, because this allows you to verify your container's configuration.

The solution (as always) is to introduce a new abstraction, such as:

public interface IPcSubsContextProvider
{
    PCSubsDBContext Context { get; }
}

Now, instead of injecting a PCSubsDBContext directly into consumers, you can now inject a IPcSubsContextProvider and use its Context property during execution (but not during building the object graph). This allows the PCSubsDBContext to be created only if it is needed, and only after the rest of the object graph has been built. An implementation would be trivial:

class LazyPcSubsContextProvider : IPcSubsContextProvider
{
    private readonly Lazy<PCSubsDBContext> context;
    public LazyPcSubsContextProvider(Func<PCSubsDBContext> factory) {
        this.context = new Lazy<PCSubsDBContext>(factory);
    }

    public PCSubsDBContext Context { get { return this.context.Value; } }
}

This implementation can be registered with a scoped/request lifestyle:

// Autofac
builder.Register<IPcSubsContextProvider>(c =>
    new LazyPcSubsContextProvider(() =>
        new PCSubsDBContext(GetConnectionStringForCurrentRequest())))
   .InstancePerHttpRequest();

// Ninject
kernel.Bind<IPcSubsContextProvider>().ToMethod(m =>
    new LazyPcSubsContextProvider(() =>
        new PCSubsDBContext(GetConnectionStringForCurrentRequest())))
    .InRequestScope();

// Simple Injector
container.RegisterPerWebRequest<IPcSubsContextProvider>(() =>
    new LazyPcSubsContextProvider(() =>
        new PCSubsDBContext(GetConnectionStringForCurrentRequest())));

After this, Simple Injector will make it very easy to verify the configuration:

container.Verify();

And it allows you to do diagnose your configuration as well.

With the other containers this will be harder to do.

Slowworm answered 20/1, 2015 at 9:3 Comment(1)
I don't understand how you meant to implement GetConnectionStringForCurrentRequest(). I have an app with similar requirements, but how do I implement the tiny detail for getting connection string based upon request?Shebeen

© 2022 - 2024 — McMap. All rights reserved.