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/

Make the DbContext Ambient with UnitOfWorkScope(now named DbContextScope by mehdime)的更多相关文章

  1. Managing DbContext the right way with Entity Framework 6: an in-depth guide by mehdime

    UPDATE: the source code for DbContextScope is now available on GitHub: DbContextScope on GitHub. A b ...

  2. DbContextScope,A simple and flexible way to manage your Entity Framework DbContext instances,by mehdime

    DbContextScope A simple and flexible way to manage your Entity Framework DbContext instances. DbCont ...

  3. EF6 Database First (DbContext) - Change Schema at runtime

    Problem:There are two SQL databases (dev and live) with on Azure which has identical table structure ...

  4. 在EntityFramework6中管理DbContext的正确方式——3环境上下文DbContext vs 显式DbContext vs 注入DbContext(外文翻译)

    (译者注:使用EF开发应用程序的一个难点就在于对其DbContext的生命周期管理,你的管理策略是否能很好的支持上层服务 使用独立事务,使用嵌套事务,并行执行,异步执行等需求? Mehdi El Gu ...

  5. 在EntityFramework6中管理DbContext的正确方式——4DbContextScope:一个简单的,正确的并且灵活的管理DbContext实例的方式(外文翻译)

    (译者注:使用EF开发应用程序的一个难点就在于对其DbContext的生命周期管理,你的管理策略是否能很好的支持上层服务 使用独立事务,使用嵌套事务,并行执行,异步执行等需求? Mehdi El Gu ...

  6. EF Core 2.0中Transaction事务会对DbContext底层创建和关闭数据库连接的行为有所影响

    数据库 我们先在SQL Server数据库中建立一个Book表: CREATE TABLE [dbo].[Book]( ,) NOT NULL, ) NULL, ) NULL, ) NULL, [Cr ...

  7. MVC模式下unity配置,报错“No connection string named '**Context' could be found in the application config file”

     写在前面: 第一次配置时好好的,后来第二次改到MVC模式,把依赖注入写成字典的单例模式时,由于新建的ORM(数据库映射模型EF),怎么弄都不用,一直报错"No connection str ...

  8. EntityFramework Core 1.1是如何创建DbContext实例的呢?

    前言 上一篇我们简单讲述了在EF Core1.1中如何进行迁移,本文我们来讲讲EF Core1.1中那些不为人知的事,细抠细节,从我做起. 显式创建DbContext实例 通过带OnConfiguri ...

  9. Entity Framework 教程——DBContext

    DBContext: 在之前的章节<创建实体数据模型>中,EDM为我们创建了SchoolDBEntities 类,它派生子System.Data.Entity.DbContext这个类,这 ...

随机推荐

  1. 005_ss-link.info的ping探测工具

    用小工具ping.py测试距离您最快的节点 #!/usr/bin/env python # coding: utf-8 """ A pure python ping im ...

  2. Python_oldboy_自动化运维之路_面向对象(十)

    面向对象编程 OOP编程是利用“类”和“对象”来创建各种模型来实现对真实世界的描述,使用面向对象编程的原因一方面是因为它可以使程序的维护和扩展变得更简单,并且可以大大提高程序开发效率 ,另外,基于面向 ...

  3. mybatis SQL构造器

    org.apache.ibatis.jdbc.AbstractSQL<T> org.apache.ibatis.jdbc.AbstractSQL<T> 抽象泛型类,它主要用于解 ...

  4. 压缩跟踪Compressive Tracking(转)

    这位博主总结的实在太好了,从原理到论文到代码,连论文都不用看:论文:http://blog.csdn.net/zouxy09/article/details/8118360 代码部分:http://b ...

  5. 关于vim复制剪贴粘贴命令的总结

    最近在使用vim,感觉很好很强大,但是在使用复制剪切粘贴命令是,碰到了一些小困惑,网上找了一些资料感觉很不全,讲的也不好,遂自己进行实践并总结了. 首先是剪切(删除): 剪切其实也就顺带删除了所选择的 ...

  6. CI框架中集成CKEditor编辑器的教程

    CKEditor是在很多开发过程中都会用到的一个富文本编辑器,那么如何在CI框架中使用它呢?这里介绍了在CI下使用CKEditor的方法,版本比较低,是在CI 1.7.3下使用fckeditor 2. ...

  7. Java编程的逻辑 (16) - 继承的细节

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  8. hdu 1272 判断所给的图是不是生成树 (并查集)

    判断所给的图是不是生成树,如果有环就不是,如果没环但连通分量大于1也不是 find函数 用递归写的话 会无限栈溢出 Orz要加上那一串 手动扩栈 Sample Input6 8 5 3 5 2 6 4 ...

  9. C语言:指针实现交换两个变量的值

    用指针交换两个变量的值(10分) 题目内容: 用指针交换两个变量的值 主函数参考: int main( ) { int a,b; scanf("%d%d",&a,& ...

  10. 【AtCoder】AGC030

    A - Poisonous Cookies 有毒还吃,有毒吧 #include <bits/stdc++.h> #define fi first #define se second #de ...