2

I've put together an MVC application using a repository pattern with Entity Framework and everything is going smoothly - but I've run into a stopping block and I'm not sure how to proceed.

I have a few dozen databases with the same schema, and I want to be able to choose one or many at runtime. For example, let's say I start with a database of users (not made yet). That user has connection string information associated with them (possibly more than one). Once the user has "logged in", I want the Enumerables I feed to my Views to contain matching data from all of the databases that user has access to.

Here's an example of what I have right now:

Entity:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations.Schema;

namespace Dashboard.Domain.Entities
{
    public class Flight
    {
        public Guid Id { get; set; }
        public string CarrierCode { get; set; }
        public string FlightNo { get; set; }
        public string MarketingCarrierCode { get; set; }
        public string MarketingFlightNo { get; set; }
        public string Type { get; set; }
        public string TailNo { get; set; }
        public string OriginIATA { get; set; }
        ...

    }
}

DB Context:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Data.Entity;
using Dashboard.Domain.Entities;

namespace Dashboard.Domain.Concrete
{
    public class EFDbContext : DbContext
    {
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Passenger>().ToTable("PAX");
        }
        public DbSet<Flight> Flights { get; set; }
        public DbSet<Passenger> PAX { get; set; }
        public DbSet<Airport> Airports { get; set; }
    }
}

Flight repository interface:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Dashboard.Domain.Entities;

namespace Dashboard.Domain.Abstract
{
    public interface IFlightRepository
    {
        IQueryable<Flight> Flights { get; }
    }
}

EF Flight Repository:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Dashboard.Domain.Abstract;
using Dashboard.Domain.Entities;

namespace Dashboard.Domain.Concrete
{
    public class EFFlightRepository : IFlightRepository
    {
        private EFDbContext context = new EFDbContext();

        public IQueryable<Flight> Flights
        {
            get { return context.Flights; }
        }
    }
}

Controller:

 public class FlightController : Controller
    {
        private IFlightRepository fRepository;
        private IPaxRepository pRepository;
        private IAirportRepository aRepository;
        public int PageSize = 10;

        public FlightController(IFlightRepository flightRepository, IPaxRepository paxRepository, IAirportRepository airportRepository)
        {
            this.fRepository = flightRepository;
            this.pRepository = paxRepository;
            this.aRepository = airportRepository;
        }

        public ViewResult List(byte status = 1, int page = 1)
        { ...

I want those repositories to contain all of the data from all of the connection strings specified, but I have no idea where to start. EF is getting my connection string from the web.config, but I need to be able to set it dynamically somehow and I need to put more than one database's data into the repository.

Is this possible? I should mention that the site is READ ONLY, so I won't need to write changes back to the DBs.

UPDATE:

I've changed the code so I can pass a connection string to the constructor of my EF Repository, but when I try to merge the IQueryables from two different contexts, as below:

public class EFFlightRepository : IFlightRepository
{
    private EFDbContext context1 = new EFDbContext(connectionstring1);
    private EFDbContext context2 = new EFDbContext(connectionstring2);
    private IQueryable<Flight> context;

    public EFFlightRepository()
    {
        context = (IQueryable<Flight>)context1.Flights.Union(context2.Flights);
    }
    public IQueryable<Flight> Flights
    {
        get { return context;}
    }
}

I get this exception:

The specified LINQ expression contains references to queries that are associated with different contexts.

How can I combine them so I can run my LINQ queries just like it's ONE set of data?

aufty
  • 407
  • 2
  • 9
  • 26
  • http://stackoverflow.com/questions/14561903/ef-connection-string-as-dbcontext-constructor-argument – Gelootn Jan 06 '16 at 15:50
  • 1
    Possible duplicate of [Using entity framework on multiple databases](http://stackoverflow.com/questions/17941225/using-entity-framework-on-multiple-databases) – NightOwl888 Jan 06 '16 at 15:51
  • 1
    This question is similar to Using entity framework on multiple databases but not a duplicate of that question. The OP wants to enumerate data from multiple databases (a sort of multi-database UNION) – Ron Deijkers Jan 06 '16 at 15:57
  • @RonDeijkers how can I edit the question to make that more clear? Thank you – aufty Jan 06 '16 at 16:01
  • You could make the sentence " I want the Enumerables...." bold or italic. That should be enough I think. – Ron Deijkers Jan 06 '16 at 16:09
  • 1
    @NightOwl888 - dissimilar, since in that question the situation is really multi-tenancy, where the application *can* point at many databases, but only one per context ("*page loads with available companies...user will then select a company...*"). In this question, the user may query multiple databases simultaneously. – Marc L. Jan 06 '16 at 18:19

3 Answers3

2

It is difficult to come up with a detailed solution because it really depends on your software design choices, but I think a possible solution consists of the following things:

1) A method / class that creates a collection of DbContext objects using the DbContext constructor with connection string or connection string name (is the same constructor) as Willian Werlang mentioned:

new DbContext("DB1");

2) Your repositories should be able to accept the list of DbContext's rather than a single one. It could e.g. be injected with the constructor of it.

3) The retrieval methods should iterate over the repositories and load (eager load when detaching) the relevant objects.

4) The retrieved objects could be detached from their DbContext using the following code:

dbContext.Entry(entity).State = EntityState.Detached;

This isn't required but might be a consideration since you would return a mix of different data sources.

5) The retrieved/detached objects should be added to a returned List<> or you could yield return the results one by one with IEnumerable<> is return type.

Returning an IQueryable isn't possible in this case but an IEnumerable will do as result.

An example of a simple retrieval method for a flight repository could be something like:

public IEnumerable<Flight> GetFlights() {
    // dbContexts is an IEnumerable<DbContext> that was injected in the constructor
    foreach (var ctx in dbContexts) {
        foreach (var flight in ctx.Flights) {
            yield return flight;
        }
    }
}
Ron Deijkers
  • 2,791
  • 2
  • 22
  • 28
  • I think, maybe, I'm not fully understanding how Entity Framework works. I tried to "Union" the IQueryables from two contexts in my EFFlightRepository class and return the Union instead, but I'm getting "The specified LINQ expression contains references to queries that are associated with different contexts." exception, so I guess I can't run LINQ queries on the Union? – aufty Jan 06 '16 at 18:35
  • You have to keep in mind that IQueryables have a deferred execution. If you have an IQueryable and then use LINQ methods on it (e.g. Union) it will create a new IQueryable which in essence is a query. Only when you use the actual data it gets executed which means it will be converted to one SQL statement which in your case then isn't possible. If you use IEnumerables however then you can use Union. – Ron Deijkers Jan 06 '16 at 19:35
  • it must not be as simple as changing the IQueryables to IEnumerables, because I get the same error – aufty Jan 06 '16 at 19:59
  • Even with just one context, changing to IEnumerable breaks some of my controller code and is much much slower – aufty Jan 06 '16 at 20:22
0

You can set multiples databases on your web.config, but with different names, so your DbContext's can receive the name of the database you want as parameter, like:

new DbContext("DB1");

This way you can choose from which database you'll get the data but I don't think you can get data from multiples bases at the same time with only onde dbContext;

0

My solution was to change my Repository classes to take a connection string parameter, like this:

namespace Dashboard.Domain.Concrete
{
    public class EFFlightRepository : IFlightRepository
    {
        private EFDbContext context;

        public IQueryable<Flight> Flights
        {
            get { return context.Flights;}
        }

        public EFFlightRepository(string connectionString)
        {
            context = new EFDbContext(connectionString);
        }
    }
}

Then create a factory class (using Ninject.Extensions.Factory) to pass the parameter when the repository is being created (How to pass parameters down the dependency chain using Ninject):

namespace Dashboard.Domain.Factories
{
    public interface IFlightRepoFactory
    {
        IFlightRepository CreateRepo(string connectionString);
    }
}

I have another Factory class that produces a list of Repositories based on a list of strings (connection strings to feed to the individual repository classes).

namespace Dashboard.Domain.Factories
{
    public interface IRepoCollectionFactory
    {
        IRepositoryCollection CreateCollection(List<string> connectionStrings);
    }
}

Then, in my controller class, I iterate through the Collection generated by the Collection Factory, running whatever query needs to be run on each set of repositories, and combine the results.

This ultimately gives me a list that contains all of the data from each query on each repository.

public FlightController(IRepoCollectionFactory repoCollectionFactory)
{
    this.repoCollectionFactory = repoCollectionFactory;
    this.collection = repoCollectionFactory.CreateCollection(new List<string> { 
                // each connection string for each database here
    });
}

Bindings in Ninject class:

    private void AddBindings()
    {
        ninjectKernel.Bind<IFlightRepoFactory>().ToFactory();
        ninjectKernel.Bind<IAirportRepoFactory>().ToFactory();
        ninjectKernel.Bind<IPaxRepoFactory>().ToFactory();
        ninjectKernel.Bind<IRepoFactory>().ToFactory();
        ninjectKernel.Bind<IRepoCollectionFactory>().ToFactory();

        ninjectKernel.Bind<IRepositories>().To<EFRepositories>();
        ninjectKernel.Bind<IRepositoryCollection>().To<EFRepositoryCollection>();
        ninjectKernel.Bind<IFlightRepository>().To<EFFlightRepository>();
        ninjectKernel.Bind<IPaxRepository>().To<EFPaxRepository>();
        ninjectKernel.Bind<IAirportRepository>().To<EFAirportRepository>();
    }
Community
  • 1
  • 1
aufty
  • 407
  • 2
  • 9
  • 26