《HiBlogs》重寫筆記[1]--從DbContext到依賴註入再到自動註入

分類:IT技術 時間:2017-09-25

本篇文章主要分析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上送我顆星星就再好不過了!

博文源碼

  • 嗨博客,基於ASP.NET COre 2.0的跨平臺的免費開源博客 https://github.com/zhaopeiym/Hi-Blogs (求??)
  • demo https://github.com/zhaopeiym/BlogDemoCode/tree/master/依賴註入/DIDemo

相關資料

  • http://www.cnblogs.com/hjf1223/archive/2010/10/10/static_datacontext.html
  • http://www.cnblogs.com/xishuai/p/ef-dbcontext-thread-safe.html

Tags: 線程 DbContext 實例 使用 註入 可能

文章來源:


ads
ads

相關文章
ads

相關文章

ad