解耦的 LINQ to SQL 框架





5.00/5 (3投票s)
使用依赖注入 (Unity) 和策略注入 (简单的 AOP) 的解耦 LINQ to SQL 框架。
引言
本文基于之前一篇关于将依赖注入与 LINQ to SQL 相结合的快速演示文章的想法进行扩展,该文章可以在 这里 查看。
本文定义的框架使用微软的依赖注入框架 Unity 和微软的简单 AOP 版本策略注入。这两个框架都包含在 Microsoft Enterprise Library 4+ 中。通过使用依赖注入,我们可以依赖 IoC 来创建所有依赖项都已正确连接的对象。在这种情况下,我们将确保在创建的任何实体或服务之间只使用一个 DataContext
(或者,在本例中是 IDataContext
)。这样,我们就无需手动管理 DataContext
的作用域。
由于该框架中的所有对象都将通过依赖注入感知 IDataContext
,因此我们可以为我们的实体添加一些“锦上添花”的方法,例如“Save”(示例:Article.Save();
)。我经常使用 EntitySpaces,我很喜欢这种语法,与 LINQ to SQL 相比。使用 Unity 作为依赖注入框架,我们在配置文件中定义数据层。配置本质上是将接口映射到对象,这意味着非常容易更改我们希望实例化的对象的类型。这意味着,我们可以创建一个模拟数据层,或者轻松地映射到一个完全不同的数据层。
我将策略注入也加入其中,因为这个框架的构建方式使得实现起来非常容易。这可以实现非常简单的基于方法的日志记录和缓存(以及其他功能)。这是一个将依赖注入与策略注入结合起来的好例子。
源项目基于 NUnit 测试。
背景
本文使用了以下技术,您需要了解它们:LINQ to SQL、依赖注入、策略注入框架 (AOP)。
事实上,微软使得在 LINQ to SQL 中使用依赖注入变得相当困难,因为最重要的部分(例如 System.Data.Linq.DataContext
和 System.Data.Linq.Table<>
类)没有被接口化。我猜想这篇文章应该被命名为:强制 LINQ to SQL 使用接口。为了实现这一目标,我创建了一个 IDataContext
接口。它包含了 System.Data.Linq.DataContext
中所有可以被自定义类实现的属性和方法。
int ExecuteCommand(string command, params object[] parameters);
IEnumerable<TResult> ExecuteQuery<TResult>(string query, params object[] parameters);
IEnumerable ExecuteQuery(Type elementType, string query, params object[] parameters);
DbCommand GetCommand(IQueryable query);
ITable GetTable(Type type);
MetaModel Mapping { get; }
void SubmitChanges();
IEnumerable<TResult> Translate<TResult>(DbDataReader reader);
IEnumerable Translate(Type elementType, DbDataReader reader);
您会注意到 GetTable<T>
方法在列表中缺失。这是因为该方法无法被其他类实现,因为没有直接的方法来构造一个 System.Data.Linq.Table<>
。微软确实公开了一个 ITable
接口,其中包含了表格所需的基本方法;但是,它不是 IEnumerable<T>
,并且不能用于编写 LINQ 查询。所以,为了在一个方法中同时获得 ITable
和 IEnumerable<T>
的功能,我创建了另一个方法。
IEnumerableTable<T> GetITable<T>() where T : class;
该方法公开了一个自定义接口,该接口扩展了 ITable
和 IEnumerable<T>
。现在,我们可以调用 IDataContext.GetITable<T>()
来查询表格,并且仍然可以对返回的对象调用所有 ITable
方法(例如 InsertOnSubmit
)。
在本文中,我创建了一个简单的 XML 数据上下文 (XDataContext
) 来从 XML 文件而不是 SQL 数据库检索和存储数据。这个实现非常简单,因此并非所有接口成员都已实现。它只是为了展示可以做到(它确实可以工作!:)
为了保持简单,将使用的数据结构包括成员、文章和评论。
设置 DataContext
为了将生成的 LINQ to SQL DataContext
映射到 IDataContext
,我们需要为生成的类创建一个部分类,并确保它实现了 IDataContext
。由于生成的 DataContext
不包含 GetITable<T>
的定义,因此我们还必须定义它。
public IEnumerableTable<T> GetITable<T>() where T : class
{
return new EnumerableTable<T>(this.GetTable(typeof(T)));
}
EnumerableTable
类实际上只是一个包装类,用于同时公开 ITable
和 IEnumerable<T>
。这基本上是我们绕过无法实例化 LINQ Table<>
对象的方法。
设置数据模型类
每个实体模型定义是通过创建一个接口来创建的,该接口简单地定义了模型的属性,然后让接口继承 IBaseEntity
,该接口公开了对 IDataContext
的依赖以及实体应包含的基本方法,例如 Save
和 Delete
。然后,每个 LINQ to SQL 实体都需要通过为其创建部分类来实现模型。然后,为了让依赖注入工作,为 LINQ to SQL 类创建了一个构造函数方法,该方法以 IDataContext
对象作为参数(当 Unity 构造一个对象时,它会查找参数最多的构造函数,并且由于这个新构造函数比默认生成的构造函数有更多的参数,Unity 就知道 IDataContext
是一个依赖项)。
在此项目中,将手动创建 IArticle
、IMember
和 IComment
,并且需要为 Article
、Member
和 Comment
创建部分类,以确保 LINQ to SQL 类实现这些接口。
实体设置方式的类图
设置服务层类
设置一个服务来公开与每个表格的数据交互的方法。需要创建一个服务接口来定义应该进行的任何数据函数。然后,它需要继承 IBaseService<>
,该接口公开了对 IEntityServiceFactory
的依赖(后者又拥有对 IDataContext
和所有其他数据服务的引用)。设置完接口后,然后创建实际的服务类。这个类继承自 BaseService<>
,后者已经定义了所需的基本属性和方法。为了让依赖注入工作,每个服务的构造函数都被创建,该构造函数以 IEntityServiceFactory
对象作为参数。
数据服务设置方式的类图
配置设置
Unity 的配置部分定义了 IoC 容器。每个容器将接口映射到实际对象,并且在每个映射中,我们可以定义依赖注入创建对象的生命周期。由于我们只想为 LINQ to SQL 容器创建一个 DataContext
,我们可以将其定义为单例。这会将 IDataContext
映射到 LINQ to SQL 生成对象的单例。
<type type="IDataContext" mapTo="LinqUnity.Linq.DataContext, LinqUnity">
<lifetime type="singleton"/>
<typeConfig
extensionType="Microsoft.Practices.Unity.Configuration.TypeInjectionElement,
Microsoft.Practices.Unity.Configuration">
<constructor/>
<!-- Ensure it is created with the default empty parameter constructor -->
</typeConfig>
</type>
现在我们需要将我们的数据模型接口映射到实际对象;在本例中,是生成的 LINQ to SQL 类。
<type type="LinqUnity.Model.IMember, LinqUnity"
mapTo="LinqUnity.Linq.Member, LinqUnity"/>
<type type="LinqUnity.Model.IComment, LinqUnity"
mapTo="LinqUnity.Linq.Comment, LinqUnity"/>
<type type="LinqUnity.Model.IArticle, LinqUnity"
mapTo="LinqUnity.Linq.Article, LinqUnity"/>
最后,我们需要设置数据服务。语法看起来很困难,但这正是微软定义泛型类型的字符串语法。这实际上是将 IBaseService<T>
映射到实际的服务。例如,第一个映射是将 IBaseService<Member>
映射到 MemberService
。
<!-- The mangled syntax is Microsoft's standard for generic types -->
<type
type="TheFarm.Data.Linq.IBaseService`1[[LinqUnity.Linq.Member, LinqUnity]],
TheFarm.Data.Linq"
mapTo="LinqUnity.Service.MemberService, LinqUnity"/>
<type
type="TheFarm.Data.Linq.IBaseService`1[[LinqUnity.Linq.Comment, LinqUnity]],
TheFarm.Data.Linq"
mapTo="LinqUnity.Service.CommentService, LinqUnity"/>
<type
type="TheFarm.Data.Linq.IBaseService`1[[LinqUnity.Linq.Article, LinqUnity]],
TheFarm.Data.Linq"
mapTo="LinqUnity.Service.ArticleService, LinqUnity"/>
EntityServiceFactory 对象
现在,我们需要一种方法来让依赖注入为我们构建所有对象。有了上述配置,当请求 IDataContext
时,IoC 容器将为我们提供一个 LinqUnity.Linq.DataContext
对象,当请求 IArticle
时,将提供一个 LinqUnity.Linq.Article
对象,依此类推。为了实现这一点,创建了 EntityServiceFactory
类,它包含一些方法,用于让 Unity 为我们创建这些对象。
T CreateEntity<T>()
:创建一个指定类型的新实体。TQuery GetService<TEntity, TQuery>()
:创建一个具有TQuery
接口类型和TEntity
实体类型的数据服务。T BuildEntity<T>(T entity)
:这个方法“重新连接”一个现有的实体对象及其所有依赖项。在这种情况下,实体都依赖于IDataContext
。
EntityServiceFactory
实现 IEntityServiceFactory
,您会注意到它是 IBaseService<T>
的一个属性,因此是一个依赖项,因为它是一个数据服务构造函数参数。上面的 XML 配置没有定义对 IEntityServiceFactory
的映射,所以在这种情况下,依赖注入实际上无法连接所有对象。然而,当 EntityServiceFactory
被构造时,它会在运行时作为单例将其自身插入到它从 Unity 解析的容器中。
container.RegisterInstance<IEntityServiceFactory>(this,
new ContainerControlledLifetimeManager());
也可以在 XML 中定义此映射,但那时需要创建一个额外的自定义类来创建 Unity 对象等。我想让 EntityServiceFactory
成为框架的基本对象,以便实现此框架不需要了解 Unity 的任何知识。EntityServiceFactory
的默认构造函数将加载配置文件中名为 *DataLayer* 的容器。或者,您可以将不同的容器名称传递给重载的构造函数方法。
每个服务都依赖于 IEntityServiceFactory
,因为每个服务可能需要对 IDataContext
和其他数据服务的引用。
使用代码
实现数据服务
通常,使用 LINQ to SQL,我们会基于 DataContext
上生成的表格属性编写查询,例如:
var article = myDataContext.Articles.Where(x => x.ArticleId == 1).SingleOrDefault();
或
var article = myDataContext.GetTable<Article>().
Where(x => x.ArticleId == 1).SingleOrDefault()
此框架无法做到这一点,因为 Article
和 GetTable<T>
都不是 IDataContext
的成员。相反,我们需要使用自定义的 GetITable<T>
方法来公开一个 IEnumerable<T>
对象进行查询。
var article = myDataContext.GetITable<Article>().
Where(x => x.ArticleId == 1).SingleOrDefault()
使用上述语法,我们的数据服务方法可能看起来像这样:
public List<IMember> GetMemberStartingWith(char c)
{
return (from m in this.Factory.DataContext.GetITable<Member>()
where m.Name.StartsWith(c.ToString())
select (IMember)m)
.ToList();
}
正如本文开头所述,这有一个缺点,我们不是直接查询 System.Data.Linq.Table<T>
,所以我们丢失了 System.Data.Linq.Table<T>
对象上可用的额外扩展方法,与 IEnumerable<T>
对象相比。
公开数据服务
EntityServiceFactory
包含了创建具有已连接所有依赖项的服务和实体的基本方法;然而,一个更好的实现是扩展此类并公开访问每个数据服务的属性。在这个例子中,这个类叫做 ServiceFactory
,它非常简单,只有三个属性:CommentService
、ArticleService
和 MemberService
。每次访问其中一个属性都会返回一个由依赖注入创建的新服务对象。在其最简单的形式中,其中一个属性可能看起来像:
public IArticleService ArticleService
{
get
{
return this.GetService<Article, IArticleService>();
}
}
策略注入
策略注入是微软 Enterprise Library 中的一个简单的 AOP 类型框架。在这个例子中,我们将使用策略注入来实现方法级别的日志记录和缓存,只需为要记录或缓存的方法添加属性即可。要实现策略注入,我们将上述属性代码更改为:
public IArticleService ArticleService
{
get
{
IArticleService service = this.GetService<Article, IArticleService>();
return PolicyInjection.Wrap<IArticleService>(service);
}
}
策略注入要求对象继承 MarshalByRefObject
,或者实现包含将在策略注入中使用的方法的接口。由于我们的所有类都已接口化,因此这非常容易实现。
要缓存方法的输出,只需添加 CachingCallHandler
。
[CachingCallHandler(0, 5, 0)]
public new List<IComment> SelectAll()
{
return base.SelectAll()
.Cast<IComment>()
.ToList();
}
现在,SelectAll()
的输出将被缓存 5 分钟。日志记录同样简单;但是,它需要配置文件中的一些条目(请参阅源代码和 微软文档 以获取更多详细信息)。
[LogCallHandler(BeforeMessage = "Begin", AfterMessage = "End")]
public IMember CreateNew(string name, string email, string phone)
{
Member member = this.Factory.CreateEntity<Member>();
member.Name = name;
member.Email = email;
member.Phone = phone;
member.DateCreated = DateTime.Now;
return (IMember)member;
}
以上将会在方法调用前创建一条日志条目,包含传入的参数值,并在方法调用后创建一条日志条目,包含返回对象的值。日志应用程序块的配置部分允许您精确配置要记录的内容以及如何格式化。
虽然通过属性来配置非常简单,但您也可以在配置文件中配置策略注入,以动态更改缓存、记录等内容,而无需重新编译。但是,被定位的方法仍然必须存在于被策略注入包装或创建的对象中。
使用数据服务
要使用数据服务,您只需创建一个 ServiceFactory
并访问属性来调用相应的方法。这将创建一个新的 IMember
。
ServiceFactory factory = new ServiceFactory();
IMember newMember = factory.MemberService
.CreateNew("Shannon", "blah@blah.com", "12345676");
在后台,这会创建一个新的 Member
对象,并调用其相应的成员 ITable
的 InsertOnSubmit
方法。要将更改保存到 DataContext
,我们只需调用:
newMember.Save();
调用 factory.DataContext.SubmitChanges()
也会做同样的事情(但我认为上面的方法更易于使用 :) LINQ to SQL 没有一个很好的方法(据我所知)来执行单个实体或表的更新,它只会更新所有更改,所以 Save()
方法实际上只是 DataContext.SubmitChanges()
的一个包装。
由于我们将 IDataContext
声明为单例,这意味着我们不必担心哪个 DataContext
创建了哪个实体,因为它在从工厂解析时始终是相同的。这允许我们创建来自不同服务的不同实体,将它们链接在一起,将更改保存到数据库,而不必担心关于不匹配 DataContext
的任何错误。
IMember newMember = memberService.CreateNew("My Name",
"blah@blah.com", "00000000");
IArticle newArticle = articleService.CreateNew("My Article",
"Some text...", "Some description...");
//Create 20 new comments with the IMember and IArticle created above
List<IComment> comments = new List<IComment>();
for (int i = 0; i < 20; i++)
comments.Add(commentService.CreateNew("My Comment", newMember, newArticle));
//save all new comments to the database at once
factory.DataContext.SubmitChanges();
使用依赖注入映射到备用数据上下文
正如本文开头提到的,我创建了一个名为 XDataContext
的测试数据上下文,它将数据存储在 XML 文件而不是数据库中。我在配置文件中定义了第二个容器,它与 SQL 容器完全相同;然而,IDataContext
被映射到这个 XDataContext
而不是 LINQ to SQL DataContext
。我没有创建自定义实体,因为 LINQ to SQL 实体本身就很简单,并且已经处理了实体关系。
要使用这个另一个容器,我们只需使用容器名称构造 EntityServiceFactory
。
XDataContext
管理身份的生成和递增,以及跟踪添加和删除。
关注点
像 Delete()
和 Save()
这样的“锦上添花”方法也伴随着一个陷阱。使用 EntityServiceFactory
的 CreateEntity<T>
方法来创建实体会自动连接实体的依赖项和 IDataContext
,以便可以调用 Save()
和 Delete()
。然而,当这些实体从数据源返回时,它们的依赖项没有被设置。为了实现这一点,我们必须使用 EntityServiceFactory
的 BuildEntity<T>
方法来连接每个对象的依赖项。这可能会带来一些性能开销。例如,SelectAll()
方法:
public virtual List<T> SelectAll()
{
List<T> entities = (from x in Factory.DataContext.GetITable<T>() select x).ToList();
entities.ForEach(x => Factory.BuildEntity<T>(x));
return entities;
}
对从数据存储返回的每个项调用 BuildEntity
。考虑到可能有数百甚至数千行,这可能会付出代价。然而,除了 BuildEntity
的性能开销之外,与运行大量的迭代的普通 LINQ to SQL 相比,开销微乎其微。
另外,我读过很多地方说将 LINQ to SQL 实体序列化到 XML 是不可能的,除非采取一些技巧,所以在这个例子中,我只实现了 IXmlSerializable
并自定义序列化了这些对象。
结论
我认为这是一个非常有趣的练习,可以展示一些很酷的技术。设法绕过微软的 LINQ to SQL 类结构来实现模拟服务而不使用任何类型的模拟库也相当有趣。
最好下载源代码来了解实际情况!
参考文献
以下是有关所用技术的更多信息: