Antler:用于您喜欢的 .NET ORM 的抽象(第二部分)






4.89/5 (6投票s)
使用相同的语法处理不同的 ORM。深入了解 Antler 框架的细节。
引言
在上一篇文章中,我向 CodeProject 社区介绍了 Antler 框架的高级概述。
现在我想深入内部,向您展示该框架的设计细节。
框架以可插拔的方式实现,由一个 Antler.Core 库和许多适配器组成。核心库包含所有必要的抽象和共享功能,而适配器库包含不同 ORM 和 IoC 容器的具体实现。
这种可插拔的结构允许在项目中的不同 ORM/数据库和 IoC 容器之间轻松切换,并使用通用的习惯性语法来处理它们。
目前,通过 NuGet 可提供 NHibernate、EntityFramework、Linq2Db、Castle Windsor、StructureMap 适配器。
在本文中,我们将深入探讨 Antler 框架的 Unit-of-work 和 ORM 适配器的实现细节,并以 Antler.NHibernate 适配器为例。
使用示例
所有数据库操作都通过 UnitOfWork 类进行,该类代表底层数据库事务。
例如,具有多个操作的 UnitOfWork
UnitOfWork.Do(uow =>
{
var hasHockeyTeams = uow.Repo<Team>().AsQueryable().
Any(t => t.Description == "Hockey");
if (!hasHockeyTeams)
{
uow.Repo<Team>().Insert(new Team() {Name = "Penguins", Description = "Hockey"});
uow.Repo<Team>().Insert(new Team() {Name = "Capitals", Description = "Hockey"})
}});
带有简单查询的 UnitOfWork
var hockeyTeams = UnitOfWork.Do(uow => uow.Repo<Team>().AsQueryable().
Where(t => t.Description == "Hockey").
ToList());
UnitOfWork 完全可配置。我们可以在应用程序级别在引导程序中配置 UnitOfWork 的行为。例如,让我们配置一个使用 Castle Windsor 容器和 NHibernate + Oracle 存储的应用程序,以便在任何 UnitOfWork 结束时执行 Rollback 而不是 Commit。
var configurator = new AntlerConfigurator();
configurator.UseWindsorContainer()
.UseStorage(NHibernateStorage.Use.WithDatabaseConfiguration(
OracleDataClientConfiguration.Oracle10.
ConnectionString(Config.ConnectionString).
DefaultSchema(Config.DbSchemaName).ShowSql()).
WithMappings("Sport.Mappings")).
SetUnitOfWorkDefaultSettings(new UnitOfWorkSettings()
{ RollbackOnDispose = true });
并且不需要在应用程序级别进行配置,您也可以配置特定的 UnitOfWork。
UnitOfWork.Do(uow =>
{
var hasHockeyTeams = uow.Repo<Team>().AsQueryable().
Any(t => t.Description == "Hockey");
if (!hasHockeyTeams)
{
uow.Repo<Team>().Insert(new Team() { Name = "Penguins", Description = "Hockey" });
uow.Repo<Team>().Insert(new Team() { Name = "Capitals", Description = "Hockey" });
}
}, new UnitOfWorkSettings(){ RollbackOnDispose = true });
您可以通过 UnitOfWorkSettings 类禁用提交、在检测到嵌套 UnitOfWork 时抛出异常、指定要使用的具体存储以及其他功能。这些功能在测试项目中可能很有用。因为,不幸的是,在企业项目中,我们有时无法生成测试数据库来运行测试。因此,像 RollbackOnDispose 这样的选项可能非常方便。
值得一提的另一件事是,我们可以配置我们的应用程序拥有多个存储,以允许它们之间的数据传输。例如
var configurator = new AntlerConfigurator(); configurator.UseBuiltInContainer() .UseStorage(EntityFrameworkStorage.Use. WithConnectionString("Data Source=.\\SQLEXPRESS; Initial Catalog=Database1;Integrated Security=True").WithLazyLoading(). WithDatabaseInitializer( new DropCreateDatabaseIfModelChanges<DataContext>()). WithMappings(Assembly.Load("Blog.Mappings.EF")), "Store1") .UseStorage(EntityFrameworkStorage.Use. WithConnectionString("Data Source=.\\SQLEXPRESS; Initial Catalog=Database2;Integrated Security=True"). WithMappings(Assembly.Load("Blog.Mappings.EF")), "Store2");
现在我们可以在应用程序中使用两个存储。例如,让我们将员工信息从一个存储传输到另一个存储。
var userFromSourceStorage = UnitOfWork.Do(uow => uow.Repo<Employee>().GetById(gpin),
new UnitOfWorkSettings {StorageName = "Store1"});
UnitOfWork.Do(uow =>
{
var foundUserInCurrentStorage = uow.Repo<Employee>().GetById(gpin);
if (foundUserInCurrentStorage == null)
{
uow.Repo<Employee>().Insert(userFromSourceStorage);
}
}, new UnitOfWorkSettings{StorageName = "Store2"});
使用 UnitOfWork 的首选方式是使用 Do 方法中指定的 lambda 表达式。但有时我们可能需要像这样获取当前的 UnitOfWork。
var hasHockeyTeams = UnitOfWork.Current.Value.Repo<Team>().AsQueryable().
Any(t=>t.Description == "Hockey");
if (!hasHockeyTeams)
{
UnitOfWork.Current.Value.Repo<Team>().Insert(new Team()
{ Name = "Penguins", Description = "Hockey" });
UnitOfWork.Current.Value.Repo<Team>().Insert(new Team()
{ Name = "Capitals", Description = "Hockey" });
}
我们可以这样做,因为我们使用线程静态字段来将 UnitOfWork 保留在线程级别。当然,在这个例子中,在使用之前检查当前上下文中是否有 UnitOfWork 是一个好习惯。
实现细节
让我们看看 UnitOfWork 类。
public class UnitOfWork: IDisposable
{
public ISessionScope SessionScope { get; private set; }
[ThreadStatic]
private static UnitOfWork _current;
public static Option<UnitOfWork> Current
{
get { return _current.AsOption(); }
}
public bool IsFinished
{
get { return _current == null; }
}
public bool IsRoot { get; private set; }
public Guid Id { get; private set; }
public UnitOfWorkSettings Settings { get; private set; }
public static Func<string, ISessionScopeFactory> SessionScopeFactoryExtractor
{ get; set; }
private UnitOfWork(UnitOfWorkSettings settings)
{
Settings = settings ?? UnitOfWorkSettings.Default;
Assumes.True(SessionScopeFactoryExtractor != null, "SessionScopeFactoryExtractor
should be set before using
UnitOfWork. Wrong configuraiton?");
Assumes.True(!string.IsNullOrEmpty(Settings.StorageName), "Storage name can't be null
or empty.
Wrong configuration?");
var sessionScopeFactory = SessionScopeFactoryExtractor(Settings.StorageName);
Assumes.True(sessionScopeFactory != null, "Can't find storage with name {0}.
Wrong storage name?",Settings.StorageName);
SetSession(sessionScopeFactory);
}
private void SetSession(ISessionScopeFactory sessionScopeFactory)
{
Requires.NotNull(sessionScopeFactory, "sessionScopeFactory");
if (_current == null)
{
SessionScope = sessionScopeFactory.Open();
IsRoot = true;
}
else
{
if (Settings.ThrowIfNestedUnitOfWork)
throw new NotSupportedException("Nested UnitOfWorks are not
supported due to UnitOfWork Settings
configuration");
SessionScope = _current.SessionScope;
IsRoot = false;
}
_current = this;
Id = Guid.NewGuid();
}
/// <summary>
/// Start database transaction.
/// </summary>
public static void Do(Action<UnitOfWork> work, UnitOfWorkSettings settings = null)
{
Requires.NotNull(work, "work");
using (var uow = new UnitOfWork(settings))
{
work(uow);
}
}
/// <summary>
/// Start database transaction and return result from it.
/// </summary>
public static TResult Do<TResult>(Func<UnitOfWork, TResult> work, UnitOfWorkSettings
settings = null)
{
Requires.NotNull(work, "work");
using (var uow = new UnitOfWork(settings))
{
return work(uow);
}
}
/// <summary>
/// Commit/Rollback transaction(depending on the configuration) explicitly. Will be called
/// automatically in the end of the "Do" block.
/// </summary>
public void Dispose()
{
if (Marshal.GetExceptionCode() == 0)
{
if (Settings.RollbackOnDispose)
Rollback();
else
Commit();
}
else
{
if (IsRoot && !IsFinished)
CloseUnitOfWork();
}
}
/// <summary>
/// Commit database transaction explicitly(not necessarily to use in standard
/// configuration, because transaction will be committed anyway in the end of the
/// "Do" block).
/// </summary>
public void Commit()
{
Perform(() =>
{
if (Settings.EnableCommit)
SessionScope.Commit();
});
}
/// <summary>
/// Rollback database transaction explicitly. As alternative you can use
/// RollbackOnDispose setting to rollback transaction automatically in the end of the "Do"
/// block(may be useful in testing).
/// </summary>
public void Rollback()
{
Perform(() => SessionScope.Rollback());
}
private void Perform(Action action)
{
Requires.NotNull(action, "action");
if (IsRoot && !IsFinished)
{
try
{
action();
}
finally
{
CloseUnitOfWork();
}
}
}
private void CloseUnitOfWork()
{
SessionScope.Dispose();
_current = null;
}
/// <summary>
/// Get Repository object to perform queries/operations on database.
/// </summary>
public IRepository<TEntity> Repo<TEntity>() where TEntity: class
{
return SessionScope.CreateRepository<TEntity>();
}
}
UnitOfWork 类位于 Antler.Core 库中(该库没有 NuGet 依赖项),因此 UnitOfWork 需要与具体的 ORM 实现完全解耦。通常注入依赖项的正确方法是使用构造函数,但在这种情况下,我们不希望每次创建 UnitOfWork 时都这样做。因此,具体的 ISessionScopeFactory(见下面的示例)依赖项通过静态属性传入 UnitOfWork 类,这是上述流畅配置的结果。
ISessionScopeFactory 实现具有单个 Open 方法,该方法在 UnitOfWork 开始时被调用以创建 ISessionScope 的具体实现。NHibernate 的 ISessionScopeFactory 实现如下所示:
public class NHibernateSessionScopeFactory: ISessionScopeFactory, ISessionScopeFactoryEx
{
private readonly ISessionFactory _sessionFactory;
private ISession _session;
public NHibernateSessionScopeFactory(ISessionFactory sessionFactory)
{
Requires.NotNull(sessionFactory, "sessionFactory");
_sessionFactory = sessionFactory;
}
public ISessionScope Open()
{
if (_session == null)
return new NHibernateSessionScope(_sessionFactory);
return new NHibernateSessionScope(_session);
}
void ISessionScopeFactoryEx.SetSession(ISession session)
{
Requires.NotNull(session, "session");
_session = session;
}
void ISessionScopeFactoryEx.ResetSession()
{
_session = null;
}
}
这里唯一值得注意的是,我们允许显式设置/重置会话。但此选项很少直接在应用程序中使用 - 主要在测试项目中,当您需要在多个 UnitOfWork 之间保持单个会话时,例如编写使用内存数据库(Sqlite)的集成测试。
NHibernate 的 ISessionScope 实现如下所示:
public class NHibernateSessionScope: ISessionScope
{
private readonly ISession _session;
private readonly ITransaction _transaction;
private readonly bool _ownSession;
public NHibernateSessionScope(ISessionFactory sessionFactory)
{
Requires.NotNull(sessionFactory, "sessionFactory");
_session = sessionFactory.OpenSession();
_transaction = _session.BeginTransaction();
_ownSession = true;
}
public NHibernateSessionScope(ISession session)
{
Requires.NotNull(session, "session");
_session = session;
_transaction = _session.BeginTransaction();
_ownSession = false;
}
public void Commit()
{
AssertIfDone();
try
{
_transaction.Commit();
}
catch (HibernateException)
{
_transaction.Rollback();
throw;
}
}
public void Rollback()
{
AssertIfDone();
_transaction.Rollback();
}
private void AssertIfDone()
{
Assumes.True(!_transaction.WasCommitted, "Transaction already was commited");
Assumes.True(!_transaction.WasRolledBack, "Transaction already was rolled back");
}
public IRepository<TEntity> CreateRepository<TEntity>() where TEntity:class
{
return new NHibernateRepository<TEntity>(_session);
}
public TInternal GetInternal<TInternal>() where TInternal : class
{
var internalSession = _session as TInternal;
Assumes.True(internalSession != null, "Can't cast Internal Session to TInternal type");
return internalSession;
}
public void Dispose()
{
_transaction.Dispose();
if (_ownSession)
_session.Dispose();
}
}
ISessionScope 的具体实现实际上是底层 ORM 会话的包装器,UnitOfWork 使用它来创建、提交、回滚事务以及获取特定 ORM 的 IRepository 实现。此外,您可以深入了解以获取内部 ORM 的会话,以执行 Antler 统一语法不支持的一些特定 ORM 操作。
NHibernate 的 IRepository 实现如下所示:
public class NHibernateRepository<TEntity>: IRepository<TEntity> where TEntity: class
{
private readonly ISession _session;
public NHibernateRepository(ISession session)
{
Requires.NotNull(session, "session");
_session = session;
}
public virtual IQueryable<TEntity> AsQueryable()
{
return _session.Query<TEntity>();
}
public TEntity GetById<TId>(TId id)
{
return _session.Get<TEntity>(id);
}
public TEntity Insert(TEntity entity)
{
Requires.NotNull(entity, "entity");
_session.Save(entity);
return entity;
}
public TId Insert<TId>(TEntity entity)
{
Requires.NotNull(entity, "entity");
Requires.True(typeof(TId).IsValueType, "Only value type Ids are
supported(int, decimal etc.)");
return (TId)_session.Save(entity);
}
public TEntity Update(TEntity entity)
{
Requires.NotNull(entity, "entity");
return _session.Merge(entity);
}
public void Delete(TEntity entity)
{
Requires.NotNull(entity, "entity");
_session.Delete(entity);
}
public void Delete<TId>(TId id)
{
var entity = GetById(id);
if (entity != null)
{
_session.Delete(entity);
}
}
}
IRepository 实现允许通过 ORM 的会话执行标准的 set 操作。
结论
如果您有兴趣,可以在GitHub上找到 NHibernate、EntityFramework 和 Linq2Db 的适配器实现。
如果您想为 Antler 尚未支持的另一个(您自己的?)ORM 或 IoC 容器实现适配器,请随意。
或者您也可以直接从 NuGet 安装该框架。
核心库,以及NHibernate、EntityFramework、 Linq2Db、Castle Windsor、StructureMap的适配器。