本篇文章主要分析DbContext的線程內唯一,然后ASP.NET Core的注入,再到實現自動注入。
DbContext為什么要線程內唯一(非線程安全)
我們在使用EF的時候,可能使用相關框架封裝過了,也可能是自己直接使用DbContext。但是有沒有想過怎么使用DbContext才是正確的姿勢呢?
DbContext可以訪問操作所有數據表、保持跟蹤狀態、SaveChanges統一提交等等強大的功能。我們會不會想,它的創建和銷毀是否要付出昂貴的代價?
其實不是的,DataContext 是輕量的,創建它不需要很大的開銷。
在EF6的DbContext文檔 https://msdn.microsoft.com/zh-cn/library/system.data.entity.dbcontext(v=vs.113).aspx 最下面有句話 此類型的任何公共 static(在 Visual Basic 中為 Shared) 成員都是線程安全的。但不保證所有實例成員都是線程安全的。
DbContext實例不保證線程安全。也就是說多線程同時操作一個DbContext實例,可能會有意想不到的問題。
比如我前面的文章 http://www.cnblogs.com/zhaopei/p/async_two.html 遇到的問題就是如此
之所以本地iis和iis express測試都是沒問題,是因為本地訪問速度快,沒有並發。
更加極端點的體現,全局使用一個靜態DbContext實例(之前我就這么想過)。
比如:線程a正在修改一個實體到一半,線程b給不小心保存了。線程c在修改一個實體,線程d又把這個實體不小心刪了。這玩笑就開大了。並發越大,此類情況越多。所以DbContext實例只能被單個線程訪問。還有,在執行異步的方法的時候切不可能自認為的“效率提升”同時發起多個異步查詢。
當然,這也只是我個人認為可能存在的問題。不過你只要記住DbContext不是線程安全類型就夠了。
如此,我們是不是應該每次數據操作都應該實例一個新的DbContext呢?也不盡然。比如方法a中的DbContext實例查詢出實體修改跟蹤,並把實體傳入了方法b,而方法b是另外實例的DbContext,那么在方法b中就不能保存方法a傳過來的實體了。如果非得這么做方法b中的DbContext也應該由方法a傳過來。也就是說我們要的效果是線程內的DbContext實例唯一。
DbContext怎么做到線程內唯一(依賴注入)
在使用EF x時你可能是
public static BlogDbContext dbEntities
{
get
{
DbContext dbContext = CallContext.GetData("dbContext") as DbContext;
if (dbContext == null)
{
dbContext = new BlogDbContext();
//將新創建的 ef上下文對象 存入線程
CallContext.SetData("dbContext", dbContext);
}
return dbContext as BlogDbContext;
}
}
而在EF Core中沒有了CallContext。其實我們不需要CallContext,通過自帶的注入框架就可以實現線程內唯一。
我們來寫個demo
首先創建一個類庫,通過注入得到DbContext。然后在web里面也注入一個DbContext,然后在web里面調用類庫里面的方法。驗證兩個DbContext的GetHashCode()值是否一致。
類庫內獲取DbContext的HashCode
namespace DemoLibrary
{
public class TempDemo
{
BloggingContext bloggingContext;
public TempDemo(BloggingContext bloggingContext)
{
this.bloggingContext = bloggingContext;
}
//獲取DbContext的HashCode
public int GetDBHashCode()
{
return bloggingContext.GetHashCode();
}
}
}
然后在web里面也注入DbContext,並對比HashCode
public IActionResult Index()
{
// 獲取類庫中的DbContext實例Code
var code1 = tempDemo.GetDBHashCode();
// 獲取web啟動項中DbContext實例Code
var code2 = bloggingContext.GetHashCode();
return View();
}
效果圖:
由此可見通過注入得到的DbContext對象是同一個(起碼在一個線程內是同一個)
另外,我們還可以反面驗證通過new關鍵字實例DbContext對象在線程內不是同一個
為什么可以通過注入的方式得到線程內唯一(注入的原理)
這里不說注入的定義,也不說注入的好處有興趣可查看。我們直接來模擬實現注入功能。
首先我們定義一個接口IUser和一個實現類User
public interface IUser
{
string GetName();
}
public class User : IUser
{
public string GetName()
{
return "農碼一生";
}
}
然后通過不同方式獲取User實例
第一種不用說大家都懂的
第二種和第三種我們看到使用到了DI類(自己實現的一個簡易注入"框架"),下面我們來看看DI類中的Resolve到底是個什么鬼
public class DI
{
//通過反射 獲取實例 並向上轉成接口類型
public static IUser Resolve(string name)
{
Assembly assembly = Assembly.GetExecutingAssembly();//獲取當前代碼的程序集
return (IUser)assembly.CreateInstance(name);//這里寫死了,創建實例后強轉IUser
}
//通過反射 獲取“一個”實現了此接口的實例
public static T Resolve<T>()
{
Assembly assembly = Assembly.GetExecutingAssembly();
//獲取“第一個”實現了此接口的實例
var type = assembly.GetTypes().Where(t => t.GetInterfaces().Contains(typeof(T))).FirstOrDefault();
if (type == null)
throw new Exception("沒有此接口的實現");
return (T)assembly.CreateInstance(type.ToString());//創建實例 轉成接口類型
}
是不是想說“靠,這么簡單”。簡單的注入就這樣簡單的實現了。如果是相對復雜點的呢?比如我們經常會用到,構造注入里面的參數本身也需要注入。
比如我們再創建一個IUserService接口和一個UserService類
public interface IUserService
{
IUser GetUser();
}
public class UserService : IUserService
{
private IUser _user;
public UserService(IUser user)
{
_user = user;
}
public IUser GetUser()
{
return _user;
}
}
我們發現UserService的構造需要傳入IUser,而IUser的實例使用也是需要注入IUser的實例。
這里需要思考的就是userService.GetUser()怎么可以得到IUser的實現類實例。所以,我們需要繼續看Resolve2的具體實現了。
public static T Resolve2<T>()
{
Assembly assembly = Assembly.GetExecutingAssembly();//獲取當前代碼的程序集
//獲取“第一個”實現了此接口的實例(UserService)
var type = assembly.GetTypes().Where(t => t.GetInterfaces().Contains(typeof(T))).FirstOrDefault();
if (type == null)
throw new Exception("沒有此接口的實現");
var parameter = new List<object>();
//type.GetConstructors()[0]獲取第一個構造函數 GetParameters的所有參數(IUser接口)
var constructorParameters = type.GetConstructors()[0].GetParameters();
foreach (var constructorParameter in constructorParameters)
{
//獲取實現了(IUser)這個接口類型(User)
var tempType = assembly.GetTypes().Where(t => t.GetInterfaces()
.Contains(Type.GetType(constructorParameter.ParameterType.FullName)))
.FirstOrDefault();
//並實例化成對象(也就是User實例) 添加到一個集合里面 供最上面(UserService)的注入提供參數
parameter.Add(assembly.CreateInstance(tempType.ToString()));
}
//創建實例,並傳入需要的參數 【public UserService(IUser user)】
return (T)assembly.CreateInstance(type.ToString(), true, BindingFlags.Default, null, parameter.ToArray(), null, null);//true:不區分大小寫
}
仔細看了也不難,就是稍微有點繞。
既然知道了注入的原理,那我們控制通過方法A注入創建實例每次都是重新創建、通過方法B創建的實例在后續參數使用相同的實例、通過方便C創建的實例全局單例,就不是難事了。
以下偽代碼:
//每次訪問都是新的實例(通過obj1_1、obj1_2可以體現)
public static T Transient<T>()
{
//var obj1_1 = assembly.CreateInstance(name);
//var obj2 = assembly.CreateInstance(obj1_1,...)
//var obj1_2 = assembly.CreateInstance(name);
//var obj3 = assembly.CreateInstance(obj1_2,...)
//var obj4 = assembly.CreateInstance(,...[obj2,obj3],...)
//return (T)obj4;
}
//一次請求中唯一實例(通過obj1可以體現)
public static T Scoped<T>()
{
//var obj1 = assembly.CreateInstance(name);
//var obj2 = assembly.CreateInstance(obj1,...)
//var obj3 = assembly.CreateInstance(obj1,...)
//var obj4 = assembly.CreateInstance(,...[obj2,obj3],...)
//return (T)obj4;
}
//全局單例(通過obj1 == null可以體現)
public static T Singleton<T>()
{
//if(obj1 == null)
// obj1 = assembly.CreateInstance(name);
//if(obj2 == null)
// obj2 = assembly.CreateInstance(obj1,...)
//if(obj3 == null)
// obj3 = assembly.CreateInstance(obj1,...)
//if(obj4 == null)
// obj4 = assembly.CreateInstance(,...[obj2,obj3],...)
//return (T)obj4;
}
通過偽代碼,應該不難理解怎么通過注入框架實現一個請求內實現DbContext的唯一實例了吧。
同時也應該更加深刻的理解了ASP.NET Core中對應的AddScoped、AddTransient、AddSingleton這三個方法和生命周期了吧。
在ASP.NET Core中實現自動注入
不知道你有沒有在使用AddScoped、AddTransient、AddSingleton這類方法的時候很煩。每次要使用一個對象都需要手動注入,每次都要到Startup.cs文件里面去做對應的修改。真是煩不勝煩。
使用過ABP的同學就有種感覺,那就是根本體會不到注入框架的存在。我們寫的接口和實現都自動注入了。使用的時候直接往構造函數里面扔就好了。那我們在使用ASP.NET Core的時候很是不是也可以實現類似的功能呢?
答案是肯定的。我們先定義這三種生命周期的標識接口,這三個接口僅僅只是做標記作用。(名字你可以隨意)
// 瞬時(每次都重新實例)
public interface ITransientDependency
//一個請求內唯一(線程內唯一)
public interface IScopedDependency
//單例(全局唯一)
public interface ISingletonDependency
我們以ISingletonDependency為例
/// 自動注入
/// </summary>
private void AutoInjection(IServiceCollection services, Assembly assembly)
{
//獲取標記了ISingletonDependency接口的接口
var singletonInterfaceDependency = assembly.GetTypes()
.Where(t => t.GetInterfaces().Contains(typeof(ISingletonDependency)))
.SelectMany(t => t.GetInterfaces().Where(f => !f.FullName.Contains(".ISingletonDependency")))
.ToList();
//獲取標記了ISingletonDependency接口的類
var singletonTypeDependency = assembly.GetTypes()
.Where(t => t.GetInterfaces().Contains(typeof(ISingletonDependency)))
.ToList();
//自動注入標記了 ISingletonDependency接口的 接口
foreach (var interfaceName in singletonInterfaceDependency)
{
var type = assembly.GetTypes().Where(t => t.GetInterfaces().Contains(interfaceName)).FirstOrDefault();
if (type != null)
services.AddSingleton(interfaceName, type);
}
//自動注入標記了 ISingletonDependency接口的 類
foreach (var type in singletonTypeDependency)
{
services.AddSingleton(type, type);
}
然后在Startup.cs文件的ConfigureServices方法里調用下就好了
public void ConfigureServices(IServiceCollection services)
{
var assemblyWeb = Assembly.GetExecutingAssembly();
// 自動注入
AutoInjection(services, assemblyApplication);
這樣以后我們只要給某個接口和類定義了ISingletonDependency接口就會被自動單例注入了。是不是很酸爽!
什么?反射低效?別鬧了,這只是在程序第一次啟動的時候才運行的。
嗨-博客,的源代碼就是如此實現。
當然,給你一個跑不起來的Demo是很痛苦的,沒有對應源碼的博文看起來更加痛苦。特別是總有這里或那里有些細節沒注意,導致達不到和博文一樣的效果。
所以我又另外重寫了一個Demo。話說,我都這么體貼了你不留下贊了再走真的好嗎?如果能在github上送我顆星星就再好不過了!
-------------------- 更新 -------------------------
基於園友對關於DbContext能不能單次請求內唯一?DbContex需不需要主動釋放?:http://www.cnblogs.com/zhaopei/p/dispose-on-dbcontext.html
博文源碼
嗨博客,基於ASP.NET COre 2.0的跨平台的免費開源博客
https://github.com/zhaopeiym/Hi-Blogs (求⭐⭐)demo
https://github.com/zhaopeiym/BlogDemoCode/tree/master/依賴注入/DIDemo