si2030
si2030

Reputation: 4035

Configuring DBContext in the constructor of my base repository class

I have a situation where I need to instantiate my DBContext after my solution has started up. I asked this question which indicated that I could do this with a constructor argument.

It was suggested that I implement as an example this:

var connection = @"Server=(localdb)\mssqllocaldb;Database=JobsLedgerDB;Trusted_Connection=True;ConnectRetryCount=0";
var optionsBuilder = new DbContextOptionsBuilder<BloggingContext>();
optionsBuilder.UseSqlServer(connection);

using (var context = new BloggingContext(optionsBuilder.Options))
{
   // do stuff
}

However I have implemented the repository pattern (for better or worst) and given my changed circumstances - not having a connection string until after the solution has run startup - I need to implement this into the base repository class and I am at a bit of a loss..

Currently I have this:

    public class EntityBaseRepository<T> : IEntityBaseRepository<T> where T : class, IEntityBase, new()
{

    public JobsLedgerAPIContext _context;

    #region Properties
    public EntityBaseRepository(JobsLedgerAPIContext context)
    {
        _context = context;
    }
    #endregion
    public virtual IQueryable<T> GetAll()
    {
        return _context.Set<T>().AsQueryable();
    }

    public virtual int Count()
    {
        return _context.Set<T>().Count();
    }
     ......

How do I implement this change both instantiating the DBContext in the constructor (there by bypassing the need to add the context as a service in startup) and then with the wrapping each of the virtual methods with "using" etc

EDIT.. Camilo indicated I had not identified when I have the database name.

The basic situation is that the system starts up (This is an Aurelia SPA project which is irrelevant to this issue) sends the package to the browser which shows a login screen. User logs in.. User is verified via a JWT controller.. Once verified in the controller (using a catalog database that has one table with 3 fields - username, password, database name) I use the database name to create a connection string and then instantiate my DBContext at that point.. so via a constructor.

The answers below need to be modified as the one with the factory answer (promising) has errors as discovered by this question.. Nkosi responded with an great answer to the error.

EDIT 2.. This is a response to the edited question below:

Here is my original Client Repository with :base(context) on the constructor.

using JobsLedger.DATA.Abstract;
using JobsLedger.MODEL.Entities;

namespace JobsLedger.DATA.Repositories
{
    public class ClientRepository : EntityBaseRepository<Client>, IClientRepository
    {
        private new JobsLedgerAPIContext _context;

        public ClientRepository(JobsLedgerAPIContext context) : base(context)
        {
            _context = context;
        }

        public void RelatedSuburbEntities(Suburb _suburb)
        {
            _context.Entry(_suburb).Reference<State>(a => a.State).Load();
        }
    }
}

It has a reference to the base class "context". I am not sure how to modify this given that I believe I still need that ":base(context)" at the end. As well, I have a method in this that accesses _context as well which is part of the constructor...

Further I assume that I can no longer inject the service into the controller but instead new it up once I have secured the connection string and then pass that connection string to service.

Also, Given I have now added a singleton on the startup do I need to remove the original entry? :

        services.AddDbContext<JobsLedgerAPIContext>(options => options.
          UseSqlServer(Configuration.GetConnectionString("DefaultConnection"), b => b.MigrationsAssembly("JobsLedger.API")));

effectively replacing it with my singleton reference as per below:

services.AddSingleton(typeof(IContextFactory<>), typeof(ContextFactory<>));

Upvotes: 0

Views: 2839

Answers (2)

Shahzad Hassan
Shahzad Hassan

Reputation: 1003

Edited

The answer has been edited to rectify the mistake spotted and fixed by Nkosi. Thanks, @Nkosi.

Implement a factory pattern. You can create a factory, call it ContextFactory as below:

First, define the interface. Further modified, removed the connectionString parameter

public interface IContextFactory<T> where T : DbContext
{
    T CreateDbContext();
}

Create a factory class that implements this interface (edited as per Nkosi answer). Further modified to inject IHttpContextAccessor

public class ContextFactory<T> : IContextFactory<T> where T : DbContext
{
    private readonly HttpContext _httpContext;

    public ContextFactory(IHttpContextAccessor contextAccessor)
    {
        _httpContext = contextAccessor.HttpContext;
    }

    public T CreateDbContext()
    {
        // retreive the connectionString from the _httpContext.Items
        // this is saved in the controller action method
        var connectionString = (string)_httpContext.Items["connection-string"];
        var optionsBuilder = new DbContextOptionsBuilder<T>();
        optionsBuilder.UseSqlServer(connectionString);
        return (T)Activator.CreateInstance(typeof(T), optionsBuilder.Options);
    }
}

Then modify your base repository and make the JobsLedgerAPIContext protected. This context is going to be set by the derived class. Further modified to remove the constructor. It will use the parameterless constructor.

public class EntityBaseRepository<T> : IEntityBaseRepository<T> where T : class, IEntityBase, new()
{
    protected JobsLedgerApiContext Context { get; set; }

    public virtual IQueryable<T> GetAll()
    {
        return Context.Set<T>().AsQueryable();
    }

    public virtual int Count()
    {
        return Context.Set<T>().Count();
    }
}

Change your derived class to use IContextFactory. Further modified to use the _contextFactory.CreateDbContext() parameter less method

The IClientRepository should have SetContext method defined.

public class ClientRepository : EntityBaseRepository<Client>, IClientRepository
{
    private readonly IContextFactory<JobsLedgerApiContext> _contextFactory;

    public ClientRepository(IContextFactory<JobsLedgerApiContext> factory)
    {
        _contextFactory = factory;
    }

    // this method will set the protected Context property using the context
    // created by the factory
    public void SetContext()
    {
        Context = _contextFactory.CreateDbContext();
    }

    public void RelatedSuburbEntities(Suburb suburb)
    {
        Context.Entry(suburb).Reference<State>(a => a.State).Load();
    }
}

In the controller, that receives IClientRepository instance, you can set the connection in the HttpContext.Items, which will be valid for the request. This value will then be retrieved by the ContextFactory using IHttpContextAccessor. Then you simply call the _repository.SetContext(); method on the repository.

public class HomeController : Controller
{
    private readonly IClientRepository _repository;

    public HomeController(IClientRepository repository)
    {
        _repository = repository;
    }

    public IActionResult Index()
    {
       // save the connectionString in the HttpContext.Items
       HttpContext.Items["connection-string"] = "test-connection";

       // set the context 
       _repository.SetContext();

       return View();
    }
}

Make sure you register the IContextFactory in ConfigureServices as open generics and Singleton as below, also register the HttpContextAccessor and IClientRepository

services.AddHttpContextAccessor();
services.AddSingleton(typeof(IContextFactory<>), typeof(ContextFactory<>));
services.AddTransient<IClientRepository, ClientRepository>();

Upvotes: 2

Mason Zhang
Mason Zhang

Reputation: 3459

You may define your JobsLedgerAPIContext like this:

public class JobsLedgerAPIContext : DbContext
{
    // public DbSet<Job> Jobs { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("Data Source=localhost;Integrated Security=SSPI;Initial Catalog=dotnetcore;");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // may need to reflect entity classes and register them here.

        base.OnModelCreating(modelBuilder);
    }
}

Upvotes: 1

Related Questions