65.9K
CodeProject 正在变化。 阅读更多。
Home

扩展 Kerosene ORM 以支持 WCF

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (28投票s)

2011年1月28日

CPOL

15分钟阅读

viewsIcon

78126

downloadIcon

280

Kerosene ORM 支持完整的类数据库的 WCF 服务和连接

重要提示

本文基于已弃用的库版本。正在进行适应新 7.0.1 版本的工作。谢谢。

引言

本文介绍了如何扩展 Kerosene ORM 库以支持 WCF 服务和连接,所使用的主要概念及其使用场景。它还可以用来理解如何开发自己的“link”类以适应您自己的场景(如果您需要的话)。

如果您还没有这样做,您绝对应该阅读 Kerosene ORM 入门文章,否则您会感到迷茫。请确保您也看看配套的“动态记录”和“实体映射”文章,因为它们的概念将在后面的内容中有所应用。请注意,本文不附带任何特定下载:它假设您使用的是入门文章中提供的文件,您可以在其中找到所有示例和库包。

一点动机

假设,例如,您正在处理的业务问题要求您开发一个客户端应用程序,可能是一个重量级的客户端,它必须能够访问数据库服务。这没什么新鲜事,我们以前做过很多次,对吧?

现在,假设这个应用程序将被分发出去,因此您的安全人员开始出现心脏病发作的症状。嘿,您很感激这个人,也很感激您不会因为绕过公司nice的安全规定而被解雇。

更糟糕的是,让我们假设您必须使用一些公司提供的 POCO 类,而您无法修改它们。是的,您不允许用任何 ORM 或与数据库相关的属性来修改它们。但同时,规范的漏洞让您担心您将不得不使用非常复杂的逻辑,涉及可能需要大量使用类中不存在的列和成员的查询。

但是……您向安全委员会展示了下图,并告诉他们:如果我们的客户端应用程序能够使用类似数据库的服务而不连接到数据库本身,那么它将隐藏并受到我们公司防火墙的保护,会怎么样?如果该服务甚至能够过滤连接到它的客户端,从而能够为每个具体客户端提供适应性视图的数据库,又会怎么样?突然,那个家伙脸上露出害羞的微笑(这些人是拿钱不笑的),所以您现在可以行动了。

Kerosene WCF diagram

好的,您获得了绿灯,于是开始分析您的选择。让我们也假设,对于您当前的具体场景,基于 HTML 的客户端是不够的。让我们也假设,无论出于何种原因,使用 REST 或 OData 都不是有效的选项:您确实需要在应用程序中使用类似数据库的服务。

最后,您想起 Kerosene ORM 提供了一个 WCF 兼容版本,同时为您提供了极大的灵活性和强大的功能。您阅读了入门文章以回顾您已经深厚的知识,然后就来到了本文,以了解它如何帮助您解决此场景。

基础知识

所以,我们要做的就是开发一个客户端应用程序,该应用程序将能够连接到一个类似数据库的服务,并将其用作一个功能齐全的常规数据库,无论从哪个实际目的来看。当然,我们无法避免 WCF 的所有细枝末节,但本文提出的 Kerosene ORM 的“WCF 链接”解决方案与您正在使用的任何具体 WCF 配置无关,因此您可以按照您想要的任何方式进行配置……或者需要以取悦那些安全人员。

说了这么多,Kerosene ORM 的作用是:拆分应用程序将使用的上下文对象分为客户端和服务器端两种身份。客户端仍然实现基础的 'IKLink' 接口,因此您的应用程序可以使用它,就像使用其他任何 'IKLink' 派生对象一样,例如,不受任何限制地使用其“动态记录”或“实体映射”操作模式。

服务器端将是一个继承自抽象类 'KServerWCF' 的类的实例。这个类负责处理客户端的请求,并代表服务器将它们推进到任何底层数据库。连接时,服务器将接受来自客户端的“连接包”,该包可以携带客户端想要发送的任何任意信息,以便告诉服务器它可能想要连接到哪个数据库(如果信息可用),使用什么用户名和密码,以及任何您可以想象的其他信息。处理请求时,服务器甚至可以过滤、修改、拒绝或记录它们……如果您愿意,几乎可以实现任何其他有趣的自定义。

服务器

创建和启动服务器

因此,如前所述,我们的首要任务是创建一个继承自基础抽象类的类。在接下来的示例中,我将不落俗套地将其命名为 'MyProxy'。

public class MyProxy : KServerWCF
{
   ...
}

在查看如何自定义我们的自定义代理服务器之前,我需要提到该库的架构是为每个连接到服务的客户端创建一个类的实例。这样,每个客户端都可以享受自己的私有“服务器代理服务”,而不会干扰同时连接到同一物理服务器的任何其他客户端。以下代码显示了您的服务器端服务启动代码的示例。

void Main(string[] args)
{
   ...
   KEngineFactory.Register(new KEngineSqlServer2008());
   KengineFactory.Register(new KNEgineSqlServer2012());
   ...
   KTypesWCF.Initialize():
   KTypesWCF.RegisterType(typeof(CalendarDate));
   KTypesWCF.RegisterType(typeof(ClockTime));
   ...
   ServiceHost host = new ServiceHost(typeof(MyProxy));
   host.Open();
   ...
   Console.ReadLine();
   host.Close(new TimeSpan(0, 0, 5));
}

第一个块只是向 Kerosene ORM 注册我们希望代理类能够定位和使用的数据库引擎。如果我们只希望使用核心库附带的默认 'engine' 适配器,我们甚至不必编写这些行。但是,在此示例中,我们注册了包含在附加库(包含在下载文件中)中的 MS SQL Server 数据库(版本 2008 和 2012)的自定义引擎适配器。

第二个块只是告诉 WCF 哪些类型将在服务器和客户端之间进行序列化和反序列化。第一行自动向 WCF 注册核心 Kerosene ORM 类。第二行和第三行注册了一个自定义类的示例,这些类恰好作为示例, precisely 显示了这种能力。

第三个块向 WCF 注册我们的自定义代理类并启动服务。在 WCF 上下文中有很多有趣的方式可以做到这一点,但这可能是最简单的方式之一。

最后,第四个块只是等待按 [Enter] 键,然后我们关闭服务。我们使用了一个等待五秒钟再继续的重写,但您可以根据需要随意使用您自己的。

从服务基础设施的角度来看,我们只需要做这么多。剩下的就是看如何最终确定我们自定义代理类的定义,以及如何根据需要自定义其行为。

自定义服务器

唯一真正需要的是实现基类的抽象 'CreateLink()' 方法。每次客户端发起连接时都会调用此方法,它的作用是创建并返回代理服务器将使用的服务器端数据上下文或“link”来处理该客户端将来的请求。例如:

protected override IKLink CreateLink()
{
   Console.WriteLine("\n> Proxy Id: {0}, Package: {1}",
      ProxyId.ToString(),
      Package == null ? "-" : Package.ToString());

   var link = KLinkDirectFactory.Create();
   link.AddTransformer<CalendarDate>(x => x.ToDateTime());
   link.AddTransformer<ClockTime>(x => x.ToString());
   ...;
   
   return link;
}

首先需要考虑的是,客户端可能已在连接请求中向服务器发送了一个可选的“连接包”。此对象保存在代理的 'Package' 属性中,如果客户端未发送连接包,则可能为 null。如果它不为 null,那么它就是一个多级动态 'DeepObject' 类的实例,能够维护任意数量的动态成员,每个成员可能都有自己的动态成员。

我们可以使用此包来定制代理将为处理该客户端而创建的“link”。例如,它可能包含数据库的名称,或者客户端希望服务器使用的引擎类型和版本,客户端提供的登录和密码……总而言之,任何我们的服务器需要从客户端获取的信息。

如果此方法返回 'null',则会向客户端抛出异常并中止连接。

在示例中,我们仅将其用于信息目的,而不是用它来创建要返回的链接,而这正是接下来的几行正在做的工作。在我们的例子中,我们选择直接连接到底层数据库,如我们服务器的配置文件 ('KLinkDirectFactory.Create()') 中所述。在接下来的两行中,我们还注册了解决方案将用于表示命令参数的自定义类的转换器。您绝对应该查看入门文章和配套文章以获取更多详细信息。

在一般情况下,就是这样!

为了完成这一部分,我只想提一下,基础代理类实现了 'IKProxyWCF' 接口,该接口基本上是双方将使用的 WCF 服务合同。 'KServerWCF' 中定义的大部分方法都是虚拟方法,您可以根据需要覆盖它们。

尽管如此,最有趣的是 'OnEnumeratorCreate(text, pars)' 和 'OnExecutorCreate(text, pars)'。第一个是在客户端想要枚举命令时调用的,第二个是在客户端想要执行命令时调用的。

两者都接受两个参数。第一个只是客户端想要执行或枚举的 SQL 命令的原始文本。您可以拦截此调用并修改此文本,验证其内容是否符合您的安全策略,记录它,或做任何您想做的事情。第二个只是该命令的参数列表,您可以随心所欲地使用它。

还有许多其他可用的方法,我们鼓励您查看源代码,看看它们是否以及何时需要覆盖它们以满足您自己的需求。

客户端

现在让我们继续看看客户端。如前所述,客户端 'link' 实现 'IKLink' 接口,因此,从任何实际角度来看,您都可以使用我们在配套文章中看到的所有功能,例如“动态记录”操作模式或“实体映射”模式。您可以创建它们支持的任何类型的数据库相关命令,或者您可以根据需要定义自己的客户端映射。

创建客户端链接

让我们看一下您的客户端应用程序的启动代码。就像我们在服务器端所做的那样,我们需要做的第一件事是奠定基础架构。

void Main(string[] args)
{
   ...
   KEngineFactory.Register(new KEngineSqlServer2008());
   KengineFactory.Register(new KNEgineSqlServer2012());
   ...
   KTypesWCF.Initialize():
   KTypesWCF.RegisterType(typeof(CalendarDate));
   KTypesWCF.RegisterType(typeof(ClockTime));
   ...
}

和以前一样,我们需要注册自定义引擎适配器,以便我们的客户端能够定位和使用它们,并且我们需要向 WCF 注册将在网络上传输的类型。

在客户端时,请记住我们无法看到没有信息关于服务器将实际使用的、隐藏的数据库将是什么,以处理我们的请求。因此,我们的第一个任务是获取一个我们注册的引擎适配器实例,以便在客户端,我们可以像它是服务器使用的数据库类型一样进行操作。我们可以为此目的使用静态 'KEngineFactory' 类。

   ...
   var provider = "SqlClient";
   var version = "11";
   var engine = KEngineFactory.Locate(provider, version);
   ...

在此示例中,我们获得了为之前注册的 MS SQL Server 数据库(版本 2012)定制的引擎。您可以使用您喜欢的任何引擎,您自己的自定义引擎,或者核心库包附带的任何通用引擎。

我们的第二个任务是创建我们的客户端将发送到服务器的“连接包”。如果不需要连接包,我们可以只发送 'null' 就完成了。

您可以用无数种方式使用它。作为第一个例子,让我们假设客户端被告知服务器可以使用多个连接字符串条目名称中的一个,并且它期望您的客户端从中选择一个。好的,我们就这样做吧。

   ...
   dynamic package = new DeepObject();
   package.ConnectionStringEntry = "MyappDB";
   ...

另一种情况是,在这种情况下,服务器期望您的客户端指定要使用的引擎类型、版本以及用于模拟服务器端连接的登录名和密码。好的,我们也可以这样做。

   ...
   p.Engine = "SqlClient";
   p.Version = "11";
   p.DB.Login = "Login_name";
   p.DB.Password = "Password";
   ...

仅作为示例,请注意我们是如何指定上述登录名和密码的:作为我们根连接包的动态成员。当然,这是您的服务器和客户端必须提前商定的约定,但它为发送您需要的任何类型的信息的方式提供了极大的灵活性和组织性。

好的,就这些了,还有几件事。第一个是,嗯,实际创建我们的客户端链接实例,并将其连接到服务器。我们可以通过使用以下链接构造函数一举完成这两件事。

   ...
   var endpoint = "My_Service_EndPoint";
   var link = new KLinkWCF(engine, endpoint, package);
   ...

您也可以使用 'new KLinkWCF(engine)' 版本,但在这种情况下,您创建的是一个断开连接的实例。您可以使用 'Connect(endpoint, package)' 方法将其连接到服务器,完成后,您可以通过使用链接的 'Disconnect()' 方法显式断开与服务器的连接。

为了完整起见,请不要忘记为作为命令参数使用的自定义类型注册转换器。

   ...   
   link.AddTransformer<CalendarDate>(x => x.ToDateTime());
   link.AddTransformer<ClockTime>(x => x.ToString());
   ...

使用注意事项

这一节几乎是多余的:我们说过客户端链接的行为就像一个常规链接一样,那么关于它我们还有什么要说的呢?

事务

需要牢记的第一件事是 Kerosene ORM 在此 WCF 场景中如何处理事务。您可能还记得,每个 'link' 对象都包含一个 'Transaction' 对象,该对象提供了可嵌套且无关的事务能力。我们的 WCF 客户端链接也这样做,但在此情况下,该属性包含一个适应于此场景的对象实例。

它的工作方式与我们在入门文章和配套文章中看到的基本相同,只是允许的模式是 'Database' 模式。原因是,我们不希望服务器拥有全局范围的事务,因为如果任何事务失败,都会影响连接到服务器的所有其他客户端。实际上,这只是背景信息,因为请记住,在客户端,我们甚至不知道服务器正在使用什么数据库(如果有的话),或者它可能只是在为我们模拟数据库服务。

更重要的是,即使客户端链接实例提供了与任何其他常规链接相同的 'Open()' 和 'Close()' 方法,服务器也可以根据其自身的需求和设计决定是响应这些请求还是执行任何其他操作。因此,使用事务的最佳方法就是使用该属性,并依赖服务代表我们采取适当的行动。

命令、记录和映射

我是否已经提到过,您可以像使用常规链接一样使用您的客户端链接实例?是的,这同样适用于该链接可以使用的所有对象,包括动态记录、架构和映射。

记录是可序列化的,因此,如果您枚举一个命令并期望获得那些记录,那么您将满意。它们如您所料。唯一的例外,这是一个非常罕见的边界情况,是如果您的记录包含任何 WCF 未知的类型的值,您需要提前注册该类型,如我们之前所见。

架构也是可序列化的,并且由 Kerosene ORM 自动处理。需要牢记的唯一注意事项是,从数据库获取的某些元数据可能不属于任何可序列化的类型。在这种情况下,这些元数据条目将被忽略,不会通过网络发送,以避免难以调试的异常。

一些内部信息

在服务器端维护连接状态

服务器端会为每个客户端维护一些元素,这些元素共同表示给定连接的状态。所有这些元素都由用于处理该客户端的自定义代理实例管理。

需要提及的重要一点是,这些元素会保留在服务器内存中,直到客户端被释放或断开连接,或者直到客户端端将它们释放。例如,服务器会创建并保留为给定命令创建的“enumerator”,直到该对象在服务器端被释放。使用命令提供的任何扩展方法时,都会自动为我们处理释放。但关键是,当您在客户端端枚举命令时,尽量不要让它们保持打开状态,而是,如果有可能这样做,要么将它们枚举直到没有更多记录可用,要么在不再需要时释放它们。

还有什么?

我鼓励您深入研究入门文章中出现的链接,即“动态记录”和“实体映射”这两篇文章,因为我们在这里描述的 WCF 功能,从 Kerosene ORM 的角度来看,只是其生态系统中的一个行为良好的组件。因此,它们可以充分利用其功能。

© . All rights reserved.