【问题标题】:Using Breeze/EntityFramework/WebAPI with multiple databases将 Breeze/Entity Framework/Web API 与多个数据库一起使用
【发布时间】:2015-09-09 04:23:22
【问题描述】:

我们目前有一个使用 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>

因此,用户在登录时输入“456”作为他们的帐户代码,我们将该值作为“数据源扩展”传递给 DevForce,并且由于 DevForce,该连接在会话的其余部分与用户相关联魔法。

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

【问题讨论】:

    标签: entity-framework breeze


    【解决方案1】:

    假设您的数据库中有这三个连接字符串,其中第一个连接字符串是 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,它在内部使用实现 ITestRepo 接口的 Repository 类 TestRepo。如果不是这种情况,那么您将必须遵循此模式,因为 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 控制器的连接字符串,而不是控制器的实例。因此,您必须使用 ASP.NET Session 并使用适当的 connectionString 存储 _repo 的实例。

    【讨论】:

      【解决方案2】:

      你有一个很好的答案 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 方法,以使用我的工厂来创建上下文。那些类 sn-ps 在下面。

      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 并使用空字符串作为帐号。

      【讨论】:

        【解决方案3】:

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

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

        因此,您不必在客户端进行任何更改。从客户端的角度来看,只有一个端点,每个端点都是一样的。

        服务器(Web API)

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

        在您的(也许唯一)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>();
        }
        

        显然,您将不得不做其他事情......任何适合实例化您的DbContext 连接到与提供的数据库ID 匹配的数据库。这是你提到的DBContextFactory 的地方。我假设你知道如何处理这个问题。

        【讨论】:

          猜你喜欢
          • 2014-05-17
          • 2011-06-22
          • 1970-01-01
          • 1970-01-01
          • 2022-11-24
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多