将Breeze / EntityFramework / WebAPI与多个数据库一起使用

时间:2015-02-25 22:05:19

标签: entity-framework breeze

我们目前有一个使用DevForce 2012的Silverlight应用程序。与Silverlight世界一样,我们已经开始移植到HTML5。我们将使用由Breeze支持的Angular以及EntityFramework / WebAPI。

我们的每个客户都有自己的数据库,所有客户都共享相同的模型。由于我们有数百个客户,因此我们的web.config包含数百个连接字符串。当用户登录时,他们输入他们的帐户代码,该帐户代码直接链接到连接字符串。 DevForce有一个"数据源扩展的概念"这是我们的Silverlight应用程序用于获得正确连接的内容。所以我们的配置示例是

<connectionStrings>
   <add name="Entities_123" connectionString="myConnectionString" />    
   <add name="Entities_456" connectionString="myConnectionString2" />
   ...
</connectionStrings>

所以用户输入&#34; 456&#34;作为他们登录时的帐户代码,我们将该值作为&#34;数据源扩展名&#34;到DevForce,由于DevForce魔术,该连接与用户的剩余会话相关联。

我很难缠绕我的头脑是如何用Breeze / EF做类似的事情。我已经搜索过网络,找不到任何关于如何使用Breeze连接到多个数据库的示例,而无需创建多个Controller / Context类。我猜我需要以某种方式使用DBContextFactory,但我甚至不知道从哪里开始。

3 个答案:

答案 0 :(得分:2)

假设您在数据库中有这三个连接字符串,其中第一个字符串是在创建模型时由EF在设计时创建的。剩下的两个你自己添加并希望在运行时使用。

<connectionStrings>
        <add name="TestDbContext" connectionString="metadata=res://*/Models.TestModel.csdl|res://*/Models.TestModel.ssdl|res://*/Models.TestModel.msl;provider=System.Data.SqlClient;provider connection string=&quot;data source=TEST_DB_SERVER\test_dev;initial catalog=Test_1;persist security info=True;user id=dbuser_1;password=pwd1;MultipleActiveResultSets=True;App=EntityFramework&quot;" providerName="System.Data.EntityClient" />
        <add name="TestDbContext_1" connectionString="metadata=res://*/Models.TestModel.csdl|res://*/Models.TestModel.ssdl|res://*/Models.TestModel.msl;provider=System.Data.SqlClient;provider connection string=&quot;data source=TEST_DB_SERVER\test_dev;initial catalog=Test_1;persist security info=True;user id=dbuser_1;password=pwd1;MultipleActiveResultSets=True;App=EntityFramework&quot;" providerName="System.Data.EntityClient" />
        <add name="TestDbContext_2" connectionString="metadata=res://*/Models.TestModel.csdl|res://*/Models.TestModel.ssdl|res://*/Models.TestModel.msl;provider=System.Data.SqlClient;provider connection string=&quot;data source=TEST_DB_SERVER\test_dev;initial catalog=Test_2;persist security info=True;user id=dbuser_2;password=pwd2;MultipleActiveResultSets=True;App=EntityFramework&quot;" providerName="System.Data.EntityClient" />
</connectionStrings>

让我们假设您有一个Breeze WebAPI控制器TestController,它在内部使用Repository类TestRepo来实现ITestRepo接口。如果不是这种情况,则必须遵循此模式,因为Unity依赖注入(DI)将需要它。顺便说一下,我不打算深入研究如何获得Unity DI软件包以及这种性质的东西。因此,假设您安装了Unity DI,下面是UnityResolver类的完整实现

using Microsoft.Practices.Unity;
using System;
using System.Collections.Generic;
using System.Web.Http.Dependencies;

namespace Test.Common.DI
{
    public class UnityResolver : IDependencyResolver
    {
        public IUnityContainer container;

        public UnityResolver(IUnityContainer container)
        {
            if (container == null)
            {
                throw new ArgumentNullException("container");
            }
            this.container = container;
        }

        public object GetService(Type serviceType)
        {
            try
            {
                return container.Resolve(serviceType);
            }
            catch (ResolutionFailedException)
            {
                return null;
            }
        }

 public IEnumerable<object> GetServices(Type serviceType)
        {
            try
            {
                return container.ResolveAll(serviceType);
            }
            catch (ResolutionFailedException)
            {
                return new List<object>();
            }
        }

        public IDependencyScope BeginScope()
        {
            var child = container.CreateChildContainer();
            return new UnityResolver(child);
        }

        public void Dispose()
        {
            container.Dispose();
        }
    }
}

以下是在WebApiConfig.cs文件中配置Unity DI的方法

using Test.Common.DI;
using Microsoft.Practices.Unity;
using QuickStaff.Controllers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;

namespace Test
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {

            string[] connNames = TestController.GetConnectionStringNamesCore();

                if (connNames.Length <= 0)
                {
                    throw new Exception("ERROR: There needs to be at least one connection string configured in the web.config file with a name starting with 'TestDbContext_'");
                }

                // Web API configuration and services
                var container = new UnityContainer();
                container.RegisterType<ITestRepo, TestRepo>(new HierarchicalLifetimeManager());
                container.RegisterInstance(new TestRepo(connNames[0])); // THIS IS NEEDED IN OERDER TO TRIGGER THE "TestController" CONSTRUCTOR THAT HAS ONE STRING ARGUMENT RATHER THAN THE DEFAULT
                config.DependencyResolver = new UnityResolver(container);


                // Web API routes
                config.MapHttpAttributeRoutes();

                config.Routes.MapHttpRoute(
                    name: "DefaultApi",
                    routeTemplate: "api/{controller}/{id}",
                    defaults: new { id = RouteParameter.Optional }
                );
            }
        }
    }

这是EF生成的模型和带有数据库优先方法的DBContext类

//--------------------------------------------------------------------------    ----
// <auto-generated>
//     This code was generated from a template.
//
//     Manual changes to this file may cause unexpected behavior in your application.
//     Manual changes to this file will be overwritten if the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

namespace TestModels
{
    using System;
    using System.Data.Entity;
    using System.Data.Entity.Infrastructure;

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

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

        public virtual DbSet<EMP_EDUCATION> EMP_EDUCATION { get; set; }
        public virtual DbSet<EMP_POSITIONS> EMP_POSITIONS { get; set; }
        public virtual DbSet<EMP_STATUS> EMP_STATUS { get; set; }
        public virtual DbSet<EMP_TALENT_TYPES> EMP_TALENT_TYPES { get; set; }
        public virtual DbSet<EMPLOYEES> EMPLOYEES { get; set; }
        public virtual DbSet<LOCATION_TYPES> LOCATION_TYPES { get; set; }
        public virtual DbSet<LOCATIONS> LOCATIONS { get; set; }
        public virtual DbSet<POSITION_CATEGORIES> POSITION_CATEGORIES { get; set; }
        public virtual DbSet<PosJobClass> PosJobClass { get; set; }
        public virtual DbSet<PRJ_LOCATIONS> PRJ_LOCATIONS { get; set; }
        public virtual DbSet<PRJ_POSITIONS> PRJ_POSITIONS { get; set; }
        public virtual DbSet<PRJ_STATUS> PRJ_STATUS { get; set; }
        public virtual DbSet<PROJECTS> PROJECTS { get; set; }
        public virtual DbSet<REPORTS> REPORTS { get; set; }
    }
}

现在我们需要实现一个与上面相同名称的部分类,以便引入另一个构造函数,该构造函数将接受包含用户将在客户端选择的连接字符串的字符串参数。所以这是一段代码

namespace Test.Models
{
    using Breeze.ContextProvider.EF6;
    using System;
    using System.Data.Entity;
    using System.Data.Entity.Core.EntityClient;
    using System.Data.Entity.Infrastructure;
    using System.Data.SqlClient;

    public partial class TestDbContext : DbContext
    {
        public TestDbContext(string connectionString)
            : base(connectionString)
        {
        }
    }
}

现在我们有一个带有构造函数的DbContext类,它以连接字符串作为参数,但问题是我们如何调用第二个构造函数,因为我们不能直接调用它,因为我们使用的是负责调用DbContext的Breeze的EFContextProvider。好消息是我们可以覆盖EFContextProvider,这里是代码

namespace Test.Models
{
    using Breeze.ContextProvider.EF6;
    using System;
    using System.Data.Entity;
    using System.Data.Entity.Core.EntityClient;
    using System.Data.Entity.Infrastructure;
    using System.Data.SqlClient;

    public class EFContextProviderEx<T> : EFContextProvider<T> where T : class, new()
    {
        private string _connectionString;

        public EFContextProviderEx(string connectionString){
            _connectionString = connectionString;
        }
        protected override T CreateContext()
        {
            return (T)Activator.CreateInstance(typeof(T), _connectionString);
        }
    }
}

好的,到目前为止一切顺利。我们现在需要使用我们介绍的上述构造函数。实现ITestRepo接口的TestRepo类是我们这样做的地方,这里是Respository类的代码以及为了完成而使用的接口代码

using Breeze.ContextProvider.EF6;
using Test.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace Test.Controllers
{

    public interface ITestRepo
    {
        string Metadata();
        SaveResult SaveChanges(JObject saveBundle);

        IQueryable<POSITION_CATEGORIES> PositionCategories();
    }

    public class TestRepo : ITestRepo
    {
        //public readonly EFContextProvider<TestDbContext> _contextProvider = new EFContextProvider<TestDbContext>();
        public readonly EFContextProvider<TestDbContext> _contextProvider;

        public TestRepo(string connectionString)
        {
            _contextProvider = new EFContextProviderEx<TestDbContext>(connectionString);
        }

        public string Metadata()
        {
            return _contextProvider.Metadata();
        }

        public Breeze.ContextProvider.SaveResult SaveChanges(Newtonsoft.Json.Linq.JObject saveBundle)
        {
            return _contextProvider.SaveChanges(saveBundle);
        }


        public IQueryable<POSITION_CATEGORIES> PositionCategories()
        {
            return _contextProvider.Context.POSITION_CATEGORIES;
        }
    }
}

现在最后一块是我们的Breeze控制器。我们需要能够以某种方式将连接字符串信息传递给我们的Breeze控制器。我们这样做的方式是通过两件事的组合。 1)通过提供一个构造函数,通过接口接收我们的存储库类的实例,以及2)通过在我们的控制器上创建一个HttpPost API方法(SetConnectionString(...))来设置所需的连接字符串,以便每当我们想要改变时连接字符串我们只是调用这个API,然后在我们的控制器上开始针对相应的数据库工作。

让我们看一下控制器的代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using Breeze.ContextProvider;
using Breeze.ContextProvider.EF6;
using Breeze.WebApi2;
using Test.Models;
using System.Web.Http.Controllers;
using System.Web;
using Microsoft.Practices.Unity;
using System.Configuration;
using Test.Common.DI;


namespace Test.Controllers
{
    [BreezeController]
    public class TestController : ApiController
    {
        private const string TEST_DB_CNTXT_PREFIX = "testdbcontext_";

        //public readonly EFContextProvider<TestDbContext> _contextProvider = new EFContextProvider<TestDbContext>();
        private readonly ITestRepo _repo;

        //public TestController()
        //{
        // UNCOMMENT THIS IN CASE YOU HAVE SOME COMPILE ERROR ASKING FOR THE DEFAULT CONSTRUCTOR    
        //}

        public TestController(ITestRepo repository)  
        {
            _repo = repository;
        }

        [HttpGet]
        public string[] GetConnectionStringNames()
        {
            string[] connNames = GetConnectionStringNamesCore();

            SetConnectionStringCore(connNames[0]); // select this as the default on the UI too

            return connNames;
        }
        public static string[] GetConnectionStringNamesCore()
        {
            string[] connNames = new string[0];
            List<string> temp = new List<string>();
            for (int i = 0; i < ConfigurationManager.ConnectionStrings.Count; i++)
            {
                string cn = ConfigurationManager.ConnectionStrings[i].Name;
                if (cn.ToLower().StartsWith(TEST_DB_CNTXT_PREFIX))
                {
                    temp.Add(cn.Substring(TEST_DB_CNTXT_PREFIX.Length));
                }

            }
            connNames = temp.ToArray();
            return connNames;
        } 

        [HttpPost]
        public void SetConnectionString([FromUri] string connectionString)
        {
            connectionString = SetConnectionStringCore(connectionString);
        }
        private string SetConnectionStringCore(string connectionString)
        {
            connectionString = TEST_DB_CNTXT_PREFIX + connectionString;

            if (!string.IsNullOrEmpty(connectionString))
            {
                // REGISTER A NEW INSTANCE OF THE REPO CLASS WITH THE NEW CONN. STRING SO THAT ANY SUBSEQUENT CALLS TO OUR CONTROLLER WILL USE THIS INSTANCE AND THUS WE WILL BE TALKING TO THAT DATABASE
                UnityResolver r = (UnityResolver)(this.ControllerContext.Configuration.DependencyResolver);
                r.container.RegisterInstance(new TestRepo(connectionString));
            }
            return connectionString;
        } 

        [HttpGet]
        public string Metadata()
        {
            return _repo.Metadata();
        }
        [HttpPost]
        public SaveResult SaveChanges(Newtonsoft.Json.Linq.JObject saveBundle)
        {
            return _repo.SaveChanges(saveBundle);
        }
        [HttpGet]
        public IQueryable<POSITION_CATEGORIES> PositionCategories()
        {
            return _repo.PositionCategories().OrderBy(pc => pc.POS_CAT_CODE);
        }

        //// GET api/<controller>
        //public IEnumerable<string> Get()
        //{
        //    return new string[] { "value1", "value2" };
        //}

        //// GET api/<controller>/5
        //public string Get(int id)
        //{
        //    return "value";
        //}

        //// POST api/<controller>
        //public void Post([FromBody]string value)
        //{
        //}

        //// PUT api/<controller>/5
        //public void Put(int id, [FromBody]string value)
        //{
        //}

        //// DELETE api/<controller>/5
        //public void Delete(int id)
        //{
        //}
    }
}

正如您在上面的代码中看到的那样,神奇发生在由SetConnectionString(...)调用的SetConnectionStringCore(...)中。基本上我们正在做的是,在UnityResolver的帮助下,我们告诉WebAPI框架将TestRepo类的哪个实例注入到我们的WebAPI控制器中。

如果你正在徘徊其余的代码,那么发生的事情是客户端(在我的情况下是一个Angular SPA)应该对我们控制器上的GetConnectionStringNames()方法进行http调用以获得所有可用的连接字符串并将其呈现给用户,以便他可以选择一个。一旦他选择了连接字符串,客户端就会调用控制器上的SetConnectionString(...)方法将其传递给WebAPI,之后客户端对该数据库执行的任何调用都会执行。另请注意,我已选择将一部分连接字符串呈现给客户端,因为有一些解析代码。但是你可以拥有自己的逻辑。需要记住的一点是,在WebApiConfig.cs文件中,我们最初使用的是遇到的第一个连接字符串。

我希望这有助于其他人,因为我真的很难让这个工作。但我仍然要感谢帮助我完成任务的人。这是我咨询过的页面列表。

http://www.asp.net/web-api/overview/advanced/dependency-injection

https://myadventuresincoding.wordpress.com/2013/03/27/c-using-unity-for-dependency-injection-in-an-asp-net-mvc-4-web-api/

Using a dynamic connection string with the Breeze EFContextProvider

Using Breeze/EntityFramework/WebAPI with multiple databases

http://cosairus.com/Blog/2015/3/10/programmatic-connection-strings-in-entity-framework-6

http://blogs.msdn.com/b/jmstall/archive/2012/05/11/per-controller-configuration-in-webapi.aspx

如您所见,我不需要覆盖WebAPI控制器的Initialize方法  protected override void Initialize(HttpControllerContext controllerContext)

你也可以在这里找到这个解决方案 https://sskasim.wordpress.com/

更新:上面有问题。它不适用于多用户场景,因为我们正在更改Web API Controller的连接字符串而不是控制器的实例。因此,您必须使用ASP.NET Session并使用适当的connectionString存储_repo的实例。

答案 1 :(得分:1)

你有一个很好的答案sskasim,我很抱歉没有回应这个早先解释我最终做的事情。我最终使用DbContextFactory来启动与正确数据库的连接,客户端发送它想要连接到的每个调用的数据库。我也不是在这里使用Unity,虽然这将是一个很好的改进。这就是我所做的,以防将来帮助其他人。

当用户登录时,他们提供的帐号对应于web.config中指向要使用的数据库的connectionString条目。连接字符串的名称采用“MyEntities_XXX”格式,其中XXX是帐号。所以在客户端上的entityManagerFactory中,我添加了以下几行,以便在每次回调服务器的标题中添加帐号。

    var adapter = breeze.config.getAdapterInstance('ajax');
    adapter.defaultSettings = {
        headers: { "account": account.user.accountNumber }
    };

然后在Breeze Controller中,我覆盖了Initialize方法,从头文件中解析出帐号并将其传递给我的存储库。

[BreezeController]
public class MyController : ApiController
{
    private readonly MyRepository _repository = new MyRepository();

    protected override void Initialize(HttpControllerContext controllerContext)
    {
        base.Initialize(controllerContext);

        IEnumerable<string> values;
        if (Request.Headers.TryGetValues("account", out values))
            _repository.SetAccountNumber(values.FirstOrDefault());
    }

    ...
}

当在repo上调用SetAccountNumber方法时,它会初始化一个新的MyContextProvider,将帐号传递给构造函数。我从EFContextProvider覆盖了CreateContext方法,以使用我的工厂创建Context。这些课程片段如下。

public class MyRepository
{
    private MyContextProvider _contextProvider = new MyContextProvider("");

    private MyContext Context { get { return _contextProvider.Context; } }


    public void SetAccountNumber(string accountNumber)
    {
        _contextProvider = new MyContextProvider(accountNumber);
    }
}


public class MyContextProvider : EFContextProvider<MyContext>
{
    private readonly MyContextFactory _contextFactory;

    public MyContextProvider(string accountNumber)
    {
        _contextFactory = new MyContextFactory();
        _contextFactory.AccountNumber = accountNumber;
    }

    protected override MyContext CreateContext()
    {
        var context = _contextFactory.Create();
        return context;
    }
}


public class MyContextFactory : IDbContextFactory<MyContext>
{
    public string AccountNumber { get; set; }

    public MyContext Create()
    {
        var dbName = "MyEntities" + (string.IsNullOrEmpty(AccountNumber) ? "" : "_" + AccountNumber);
        var contextInfo = new DbContextInfo(typeof(MyContext), new DbConnectionInfo(dbName));
        var context = contextInfo.CreateInstance() as MyContext;

        return context;
    }
}

这里唯一的“问题”是在web.config中需要一个通用条目,用于EF将在生成元数据时使用的“MyEntities”。这就是为什么你看到我最初创建一个带有空字符串的ContextProvider作为帐号。

答案 2 :(得分:0)

我认为这与数据库选择问题一样是一个安全问题。因此,我将继续让您的服务器根据经过身份验证的用户确定数据库ID。

客户端不应直接了解或影响数据库ID的选择。这是属于服务器的私事。

因此,您无需在客户端进行任何更改。从客户端的角度来看,只有一个端点,并且每个人的端点都是相同的。

服务器(Web API)

需要每个数据库的控制器。您可能出于其他原因需要多个控制器,但这是由其他问题驱动的,而非此问题。

在您的(可能是 one-only-only )Web API控制器中,您可以某种方式获取数据库ID。我今天不知道你是怎么做的Silverlight + DevForce;它可能与您的Web API控制器中的方法相同。

您的控制器将实例化一个EFContextProvider ...或者更好的是一个包装EFContextProvider的存储库/工作单元组件,并传递数据库ID。

您可能无法在控制器的构造函数中获取数据库ID,因为此时请求对象不可用。在这个例子中,我们将在控制器的Initialize方法中告诉存储库它。

以下是可能适合您的Web API控制器的开头:

[BreezeController]
public class YourController : ApiController {
    private readonly YourRepository _repository;

    // default ctor
    public YourController() : this(null) { }

    // Test / Dependency Injection ctor.
    // Todo: inject via an IYourRepository interface rather than "new" the concrete class
    public YourController(YourRepository repository) {
        _repository = repository ?? new YourRepository();
    }

    protected override void Initialize(HttpControllerContext controllerContext) {
        base.Initialize(controllerContext);
        _repository.SetDatabaseId(getDatabaseId());
    }

    /// <summary>
    /// Get the DatabaseId from ???
    /// </summary>
    private string getDatabaseId() {
        try {
            return ...; // your logic here. The 'Request' object is available now
        } catch  {
            return String.Empty;
        }
    }

    ...
}

当然YourRepository会延迟EFContextProvider的实例化,直到有人拨打SetDatabaseId

现在,如果您没有动态更改连接字符串,那么您就完成了。但是因为您在最后一刻定义了连接字符串,所以您需要创建EFContextProvider的子类并覆盖其默认实现为的CreateContext方法:

protected virtual T CreateContext() // 'T' is your DbContext type
{
  return Activator.CreateInstance<T>();
}

显然,您必须做其他事情......无论是否适合实例化连接到与提供的数据库ID匹配的数据库的DbContext。这是您提到的DBContextFactory的地方。我假设你知道如何照顾这个。