本文系Mainz在博客园的原创(http://www.cnblogs.com/mainz/),若您在银光中国(SilverlightChina)或其它网站看到本文,请注意是转载,文中很多链接已经丢失。言归正传,前天园友Smok.问我这个动态切换数据库的问题,今天中午研究了一下发出来,因为大家可能都有这种类似的需求,也许发出来大家还有更好的解决方案。本文说的是Silverlight+EntityFramework+WCF Ria Service的动态切换数据库的问题。如何在服务端动态切换数据库?如何在客户端传递database关键字到服务端并连接到不同的数据库?

通常,Ado.net EntityFramework的数据库连接字符串ConnectionString是存在Web.config里的,类似这样:

<connectionStrings>
<add name="YourEntities" connectionString="metadata=res://*/Model1.csdl|res://*/Model1.ssdl|res://*/Model1.msl;provider=System.Data.SqlClient;provider connection string=&quot;data source=192.168.1.88;initial catalog=Northwind;persist security info=True;user id=sa;password=abcdefg;multipleactiveresultsets=True;App=EntityFramework&quot;" providerName="System.Data.EntityClient" />
</connectionStrings>

EntityConnection的ConnectionString和以前的Ado.net的connectionString有点不一样。有2个新元素,解释下:

  • metatata: 指定Entity Framework数据模型的路径,包括CSDL(概念架构定义语言)、SSDL(存储架构定义语言)、MSL(映射规范语言)的路径
  • provider: 数据库provider,例如SQLServer的默认Ado.net:System.Data.SqlClient,也可以是其它的数据库例如Oracle、SQLite、MySql

这是用配置的方式指定ConnectionString,也可以用代码动态指定Entity framework的数据库连接,用SqlConnectionStringBuilder手动创建

1 public partial class YourEntities
2 {
3 partial void OnContextCreated()
4 {
5 SqlConnectionStringBuilder sb = new SqlConnectionStringBuilder(
6 ((EntityConnection)Connection).StoreConnection.ConnectionString);
7 sb.IntegratedSecurity = false;
8 sb.UserID = "sa";
9 sb.Password = "abcdefg";
10 sb.InitialCatalog = "AnotherDatabase";
11 ((EntityConnection)Connection).StoreConnection.ConnectionString = sb.ConnectionString;
12 }
13 }

但这样的方式ConnectionString就写死了,所以还是要用Web.config配置的方式。

OK,为了切换数据库,我们在Web.config添加一个数据库连接方式用来测试,关键词叫“MyTestDBConn”:

<connectionStrings>
<add name="YourEntities" connectionString="metadata=res://*/Model1.csdl|res://*/Model1.ssdl|res://*/Model1.msl;provider=System.Data.SqlClient;provider connection string=&quot;data source=192.168.1.88;initial catalog=Northwind;persist security info=True;user id=sa;password=abcdefg;multipleactiveresultsets=True;App=EntityFramework&quot;" providerName="System.Data.EntityClient" />
<add name="MyTestDBConn" connectionString="metadata=res://*/Model1.csdl|res://*/Model1.ssdl|res://*/Model1.msl;provider=System.Data.SqlClient;provider connection string=&quot;data source=192.168.1.88;initial catalog=Northwind_AAA;persist security info=True;user id=sa;password=abcdefg;multipleactiveresultsets=True;App=EntityFramework&quot;" providerName="System.Data.EntityClient" />
</connectionStrings>

在Domain Service中改变EntityFramework的ConnectionString,这儿指定用"MyTestDBConn":

1 using System.Configuration;
2
3 [EnableClientAccess()]
4 public class TestDomainService : LinqToEntitiesDomainService<TestEntities>
5 {
6 protected override TestEntities CreateObjectContext()
7 {
8 string connection = ConfigurationManager.ConnectionStrings["MyTestDBConn"].ConnectionString;
9 return new TestEntities(connection);
10 }
11 }

这样的确可以在DomainService切换Entity Framework数据库连接ConnectionString,但我如果有好多DomainService怎么办,岂不是每一个都要这样来一下子?第二个问题是,如何把"MyTestDBConn"作为一个参数传进来?

先解决第一个问题,我们写一个DomainService的基类,继承原来的基类:LinqToEntitiesDomainService<T>,然后让每一个DomainService继承这个新的基类即可。

1 using System;
2 using System.Data.Objects;
3 using System.ServiceModel.DomainServices.EntityFramework;
4 using System.ServiceModel.DomainServices.Server;
5 using System.Configuration;
6
7 public abstract class DomainServiceBase<TContext> : LinqToEntitiesDomainService<TContext>
8 where TContext : ObjectContext, new()
9 {
10 protected override TContext CreateObjectContext()
11 {
12 // Build up the connection string
13 string connection = ConfigurationManager.ConnectionStrings["MyTestDBConn"].ConnectionString;
14
15 Type contextType = typeof(TContext);
16 object objContext = Activator.CreateInstance(contextType, connection);
17
18 return objContext as TContext;
19 }
20
21 protected override void OnError(DomainServiceErrorInfo errorInfo)
22 {
23 // Deal with errors
24 }
25 }
26
27
28 //接着,让你的DomainService继承这个基类,不要继承LinqToEntitiesDomainService了
29
30 [EnableClientAccess()]
31 public class TestDomainService : DomainServiceBase<TestEntities>
32 {
33 //...
34 }

第二个问题是如何把"MyTestDBConn"这个ConnectionString关键词作为参数传进来?这个问题有点难。上面说的都是在服务器端指定连接到哪个数据库服务器,如何实现在客户端用户登录的时候,有个下拉框选择数据库服务器,然后在Silverlight客户端把这个参数传递到服务器端DomainService并连接到指定的数据库服务器?

如果用上面的思路,就是要在DomainService的构造函数上动手,加一个参数string databaseKey,但是这个有点罗嗦。为什么?因为DomainService不是一般的class,它是WCF Service,在DomainService里面的每一个方法就是一个定义了request和response的contract的WCF (Web Service),只不过WCF Ria Services隐藏了这些调用WCF的复杂性,让你感觉在客户端就可以简单的调用DomainService。所以,如果你要给DomainService的构造函数传一个参数,就要研究一下如何给WCF Service的构造函数传递一个参数的方法,可以用IInstanceProviderServiceBehavior来实现,代码见这篇文章

这里我用另外一个Session的方法来实现更简单,每个客户端session不同,所以在客户端用户登录的时候,有个下拉框选择数据库服务器,然后在Silverlight客户端把这个参数传递到服务器端DomainService并让Entity framework连接到指定的数据库服务器是可以用Session来实现的。具体方法是在DomainService里加一个SetDatabase函数设置数据库,客户端把参数传给服务端,并保存到Session。然后重载CreateObjectContext ()函数并根据Session的值连接到指定的数据库。最后在SetDatabase这个函数的Callback时读取真正的数据。代码:

在DomainService里面加个SetDatabase()方法:

1 using System.Web;
2
3 [EnableClientAccess()]
4 public class TestDomainService : LinqToEntitiesDomainService<TestEntities>
5 {
6 public void SetDatabase(string Database)
7 {
8 HttpContext.Current.Session["UserDatabase"] = Database;
9 }
10 }

在DomainService里面重载CreateObjectContext()方法:

1 using System.Web;
2
3 [EnableClientAccess()]
4 public class TestDomainService : LinqToEntitiesDomainService<TestEntities>
5 {
6 protected override TestEntities CreateObjectContext()
7 {
8 if (HttpContext.Current.Session["UserDatabase"] != null)
9 {
10 string connection = ConfigurationManager.ConnectionStrings[HttpContext.Current.Session["UserDatabase"].ToString()].ConnectionString;
11 return new TestEntities(connection);
12 }
13 else
14 return base.CreateObjectContext();
15 }
16 }
前台调用xaml.cs:

1 public partial class MainPage : UserControl
2 {
3 private DomainService1 s = new DomainService1();
4
5 public MainPage()
6 {
7 InitializeComponent();
8
9 //使用MyTestDBConn作为ConnectionString,加载数据到DataGrid
10 LoadData("MyTestDBConn");
11 }
12
13 private void button1_Click(object sender, RoutedEventArgs e)
14 {
15 //使用Web.config中另外一个ConnectionString,加载数据到DataGrid
16 LoadData("YourEntities");
17 }
18
19 //EF连接指定的ConnectionString
20 //Param: database - web.config中ConnectionString的Name关键词
21 private void LoadData(string database)
22 {
23 this.busyIndicator1.IsBusy = true;
//****SetDatabase callback的时候读取数据*********
24 s.SetDatabase(database, (InvokeOperation) =>
25 {
26 //GetCompanies改为你自己的方法
27 s.Load(s.GetCompaniesQuery(), (t) =>
28 {
29 dataGrid1.ItemsSource = t.Entities;
30 this.busyIndicator1.IsBusy = false;
31 }, true);
32 }, null);
33 }
34 }

可以另外按照上面DomainServiceBase的方法写一个基类避免每个DomainService重写一遍。

结束

不知道大家有没有更好的方法实现客户端传参到服务端动态切换数据库呢?另:由于Entity framework的模型是由链接到本地数据库映射生成的,所以不提供源码下载了,按照本文的步骤一步步做就能运行。如果对Silverlight+Wcf Ria service+EF还不明白的,可以看博客园其他的博文。本人也有Silverlight系列文章可以一读。

作者: Mainz 发表于 2011-05-16 14:46 原文链接

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架