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


本篇文章主要分析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

 

博文源碼

相關資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM