Make the DbContext Ambient with UnitOfWorkScope(now named DbContextScope by mehdime)

The Entity Framework DbContext (or LINQ-to-SQL DataContext) is a Unit Of Work implementation. That means that the same DbContext should be used for all operations (both reading and writing) within a single web or service request. That means that there are a lot of different places where the DbContext have to be accessed. To avoid having to pass the DbContext around as a parameter, I’ve created a UnitOfWorkScope that makes the DbContext ambient and easily accessible.

A common beginners problem when working with Entity Framework or LINQ-to-SQL is to have too short life times of the DbContext. A problem that I’ve seen many questions about on Stack Overflow is when questions are encapsulated in repositories or helper methods. Inside each method a new DbContext is created for that specific read. Later, when the returned entity has been updated and is to be saved the problem occurs. The entity should be saved using the same DbContext that once read it from the database to allow change tracking to work properly. Clearly, having separate DbContexts is a problem.

The first attempt to solve it is usually to pass the DbContext around. That only solves half the problem though, that of accessing it. The other half of the problem is to decide where to call SaveChanges to persist the changes done. Calling it from every method making changes spoils the entire unit of work concept. Trusting the creator of the context to know when any of a myriad of called functions have made changes seems risky.

I’ve been looking for a better way to handle the DbContext and have come up with an ambient DbContext, using a UnitOfWorkScope which is similar to TransactionScope.

The main features of the UnitOfWorkScope are:

  • The first method in the call chain opening a UnitOfWorkScope creates an ambient DbContext.
  • Subsequent methods in the call chain utilizes the same DbContext.
  • Changes are only saved if all participating scopes called SaveChanges
  • Read only mode is available, where data is read using the existing DbContext, but no changes need to be saved. This is useful for GetSomeThing methods that are used both for pure reading and for reading for update.

 

A Shared Query

A shared query uses the UnitOfWorkScope, instead of creating a DbContext instance directly.

public static Entities.Car GetCar(int id)
{
    using (var uow = new UnitOfWorkScope<CarsContext>(UnitOfWorkScopePurpose.Reading))
    {
        return uow.DbContext.Cars.Single(c => c.CarId == id);
    }
}

The reading purpose is used to mark that this unit of work scope will not do any updates, so SaveChanges will not be called. The method can be used both standalone to read data, or to fetch data for subsequent update. Let’s look at using it for updating.

using (var uow = new UnitOfWorkScope<CarsContext>(UnitOfWorkScopePurpose.Writing))
{
    Car c = SharedQueries.GetCar(carId);
    c.Color = "White";
    uow.SaveChanges();
}

Here, the scope is opened for writing. Leaving the scope without calling SaveChanges indicates an error, just like Complete works on TransactionScope.

Experiences of Using the UnitOfWorkScope

We’re using the unit of work scope in my current project. The experience so far is that we’ve removed the need to pass around DbContext instances all over. It is also much easier to reuse the same common set of base queries for reading and for writing. Any method can easily get hold of the ambient scope. A business logic method on an entity can a service method, which can now be part of the same unit of work without having to pass the DbContext around.

The UnitOfWorkScope code

The code makes use of the Disposable base class, that I’ve written about before. The code is also available on GitHub.

/// <summary>
/// Purpose of a UnitOfWorkScope.
/// </summary>
public enum UnitOfWorkScopePurpose
{
    /// <summary>
    /// This unit of work scope will only be used for reading.
    /// </summary>
    Reading,
 
    /// <summary>
    /// This unit of work scope will be used for writing. If SaveChanges
    /// isn't called, it cancels the entire unit of work.
    /// </summary>
    Writing
}
 
/// <summary>
/// Scoped unit of work, that merges with any existing scoped unit of work
/// activated by a previous function in the call chain.
/// </summary>
/// <typeparam name="TDbContext">The type of the DbContext</typeparam>
public class UnitOfWorkScope<TDbContext> : Disposable
    where TDbContext : DbContext, new()
{
    /// <summary>
    /// Handle class for holding the real DbContext and some state for it.
    /// </summary>
    private class ScopedDbContext : Disposable
    {
        /// <summary>
        /// The real DbContext.
        /// </summary>
        public TDbContext DbContext { get; private set; }
 
        /// <summary>
        /// Has there been a failure that should block saving?
        /// </summary>
        public bool BlockSave { get; set; }
 
        /// <summary>
        /// Was any unit of work scope using this DbContext opened for writing?
        /// </summary>
        public bool ForWriting { get; private set; }
 
        /// <summary>
        /// Switch off guard for direct calls to SaveChanges.
        /// </summary>
        public bool AllowSaving { get; set; }
 
        /// <summary>
        /// Ctor.
        /// </summary>
        /// <param name="forWriting">Is the root context opened for writing?</param>
        public ScopedDbContext(bool forWriting)
        {
            ForWriting = forWriting;
            DbContext = new TDbContext();
            ((IObjectContextAdapter)DbContext).ObjectContext.SavingChanges
                += GuardAgainstDirectSaves;
        }
 
        void GuardAgainstDirectSaves(object sender, EventArgs e)
        {
            if (!AllowSaving)
            {
                throw new InvalidOperationException(
                    "Don't call SaveChanges directly on a context owned by a UnitOfWorkScope. " +
                    "use UnitOfWorkScope.SaveChanges instead.");
            }
        }
 
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (DbContext != null)
                {
                    ((IObjectContextAdapter)DbContext).ObjectContext.SavingChanges
                        -= GuardAgainstDirectSaves;
 
                    DbContext.Dispose();
                    DbContext = null;
                }
            }
            base.Dispose(disposing);
        }
    }
 
    [ThreadStatic]
    private static ScopedDbContext scopedDbContext;
 
    private bool isRoot = false;
 
    private bool saveChangesCalled = false;
 
    /// <summary>
    /// Access the ambient DbContext that this unit of work uses.
    /// </summary>
    public TDbContext DbContext
    {
        get
        {
            return scopedDbContext.DbContext;
        }
    }
 
    private UnitOfWorkScopePurpose purpose;
 
    /// <summary>
    /// Ctor
    /// </summary>
    /// <param name="purpose">Will this unit of work scope be used for reading or writing?</param>
    public UnitOfWorkScope(UnitOfWorkScopePurpose purpose)
    {
        this.purpose = purpose;
        if (scopedDbContext == null)
        {
            scopedDbContext = new ScopedDbContext(purpose == UnitOfWorkScopePurpose.Writing);
            isRoot = true;
        }
        if (purpose == UnitOfWorkScopePurpose.Writing && !scopedDbContext.ForWriting)
        {
            throw new InvalidOperationException(
                "Can't open a child UnitOfWorkScope for writing when the root scope " +
                "is opened for reading.");
        }
    }
 
    /// <summary>
    /// Dispose implementation, checking post conditions for purpose and saving.
    /// </summary>
    /// <param name="disposing">Are we disposing?</param>
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            // We're disposing and SaveChanges wasn't called. That usually
            // means we're exiting the scope with an exception. Block saves
            // of the entire unit of work.
            if (purpose == UnitOfWorkScopePurpose.Writing && !saveChangesCalled)
            {
                scopedDbContext.BlockSave = true;
                // Don't throw here - it would mask original exception when exiting
                // a using block.
            }
 
            if (scopedDbContext != null && isRoot)
            {
                scopedDbContext.Dispose();
                scopedDbContext = null;
            }
        }
 
        base.Dispose(disposing);
    }
 
    /// <summary>
    /// For child unit of work scopes: Mark for saving. For the root: Do actually save.
    /// </summary>
    public void SaveChanges()
    {
        if (purpose != UnitOfWorkScopePurpose.Writing)
        {
            throw new InvalidOperationException(
                "Can't save changes on a UnitOfWorkScope with Reading purpose.");
        }
 
        if (scopedDbContext.BlockSave)
        {
            throw new InvalidOperationException(
                "Saving of changes is blocked for this unit of work scope. An enclosed " +
                "scope was disposed without calling SaveChanges.");
        }
 
        saveChangesCalled = true;
 
        if (!isRoot)
        {
            return;
        }
 
        scopedDbContext.AllowSaving = true;
        scopedDbContext.DbContext.SaveChanges();
        scopedDbContext.AllowSaving = false;
    }
}

The code has been updated with a better way to handle when a writing scope is opened to a reading root scope. In this version, an exception is thrown when the child scope is opened. Previously the exception was thrown when the root scope was disposed.

The code is written to be easy to use right and hard to use wrong. There is a risk that someone would call SaveChanges directly on the wrapped DbContext – possibly saving changes early. To avoid that there is a guard which disallows that.

The code is thread safe as it uses thread local storage to store the current scope. However it will probably not work with async/await as it does not consider the synchronization context but rather is tied to the thread directly.

 

from:https://coding.abel.nu/2012/10/make-the-dbcontext-ambient-with-unitofworkscope/

posted @   遥望星空  阅读(537)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
历史上的今天:
2015-02-10 关于Hyper-V备份的四大注意事项
2014-02-10 SharePoint Server 2013 Offline Installation (without Internet)
2014-02-10 安装sql2012 正在启动操作系统功能"NetFx3"
2012-02-10 sqlite3自增key设定(创建自增字段)
2012-02-10 让PowerDesigner支持SQLite 3.0[转]
2009-02-10 asp.net结合html,javascript实现无刷新跨域数据提交
2009-02-10 配置自己的域名解析
点击右上角即可分享
微信分享提示