第三十二節:比較Core中的內置注入、EFCore的注入、AutoFac改造后的注入的生命周期問題


一. Core的內置注入

類和接口的准備 

 public interface IU1
    {
        string guid { get; set; }
    }
    public interface IU2
    {
        string guid { get; set; }
    }
    public interface IU3
    {
        string guid { get; set; }
    }
    public interface IU4
    {
        string guid { get; set; }
    }
接口
  public class U1 : IU1
    {
        public string guid { get; set; }

        public U1()
        {
            guid = System.Guid.NewGuid().ToString("N");
        }

    }
    public class U2 : IU2
    {
        public string guid { get; set; }

        public U2()
        {
            guid = System.Guid.NewGuid().ToString("N");
        }
    }
    public class U3 : IU3
    {
        public string guid { get; set; }

        public U3()
        {
            guid = System.Guid.NewGuid().ToString("N");
        }

    }
    public class U4 : IU4
    {
        public string guid { get; set; }

        public U4()
        {
            guid = System.Guid.NewGuid().ToString("N");
        }
    }

1.測試案例

(1).比較單次請求、 兩次請求 對應的值。

(2).比較 一次請求 主線程子線程中的值。

(3).在子線程中加等待時間,看子線程中的對象是否被銷毀了,能否繼續使用。

2. 測試步驟

  將U1-U4在ConfigureService按照下面代碼進行注冊,然后每個類在控制器中注入兩次,通過看構造函數中的guid的值是否一樣,來判斷是否重新創建了。

ConfigureService中代碼

 //內置注入
 services.AddTransient<IU1, U1>();   //瞬時的
 services.AddScoped<IU2, U2>();      //請求內單例
 services.AddSingleton<IU3, U3>();   //全局單例
 services.AddSingleton<IU4>(new U4()); //全局單例

控制器中的注入代碼

    public class HomeController : Controller
    {
        public IU1 U1 { get; }
        public IU1 U11 { get; }
        public IU2 U2 { get; }
        public IU2 U22 { get; }
        public IU3 U3 { get; }
        public IU3 U33 { get; }
        public IU4 U4 { get; }
        public IU4 U44 { get; }
        public HomeController(IU1 u1, IU1 u11, IU2 u2, IU2 u22, IU3 u3, IU3 u33, IU4 u4, IU4 u44, ypfContext context1, ypfContext context2)
        {
            U1 = u1;
            U11 = u11;
            U2 = u2;
            U22 = u22;
            U3 = u3;
            U33 = u33;
            U4 = u4;
            U44 = u44;
        }
}

測試代碼

 public void myWrite(string msg)
 {
    StreamWriter sw = System.IO.File.AppendText("Log/test.txt");
    //追加文本               
    sw.WriteLineAsync($"{DateTime.Now}:【{msg}】");//自動換行
    sw.Close();
 }
 {
                myWrite($"內置依賴注入,主線程測試");
                myWrite($"U1:{U1.guid}");
                myWrite($"U11:{U11.guid}");
                myWrite($"U2:{U2.guid}");
                myWrite($"U22:{U22.guid}");
                myWrite($"U3:{U3.guid}");
                myWrite($"U33:{U33.guid}");
                myWrite($"U4:{U4.guid}");
                myWrite($"U44:{U44.guid}");

                Task.Run(() =>
                {
                    Thread.Sleep(5000);
                    myWrite($"下面是Task中的日志:");
                    myWrite($"U1:{U1.guid}");
                    myWrite($"U11:{U11.guid}");
                    myWrite($"U2:{U2.guid}");
                    myWrite($"U22:{U22.guid}");
                    myWrite($"U3:{U3.guid}");
                    myWrite($"U33:{U33.guid}");
                    myWrite($"U4:{U4.guid}");
                    myWrite($"U44:{U44.guid}");
                });
                myWrite($"主線程結束");
}

測試結果: 兩次請求先后進來,記錄日志。

 3. 結論

(1). 瞬時:單次請求內,同一個對象比如U1即使被注入多次(eg:u1,u11),每個注入的實例都是不一樣,多次請求就更不一樣了。

    請求內單例:單次請求內,同一個對象比如U1被注入多次,每個注入的實例都是一樣的;但多次請求是不一樣的。

    單例:不管是單次請求還是多次請求,同一個對象比如U1不管被注入多少次,所有的實例都是一樣的。

(2). 對於主線程和子線程這種情況, 注入的同一個實例U1,不管他是瞬時的還是請求內單例的,在主線程和子線程中都是一個對象哦,即U1.guid的在主線程和子線程是相同的.(包括子線程休眠一段時間等着主線程走完,子線程依舊可以獲取guid)。

疑問?

  子線程中為什么還能拿到對象?

   難道是對象沒有dispose的原因還是什么? (詳見EFCore上下文有dispose,看下面的測試)

 

 

二. EFCore的注入

 1. 測試

  將EFCore的上下文注冊為瞬時的,然后在控制器中注入兩個,子線程等待一段時間,主線程savechange或者dispose銷毀,測試代碼如下:

ConfigureService中代碼

services.AddDbContext<ypfContext>(option => option.UseSqlServer(Configuration.GetConnectionString("EFStr")), ServiceLifetime.Transient);

上下文代碼

 public partial class ypfContext : DbContext
 {
        public string guid { get; set; }
        public ypfContext(DbContextOptions<ypfContext> options)
            : base(options)
        {
            guid = System.Guid.NewGuid().ToString("N");
        }
 }

控制器中的注入代碼

        public ypfContext _dbContext1 { get; }
        public ypfContext _dbContext2 { get; }

        public HomeController(ypfContext context1, ypfContext context2)
        {
            _dbContext1 = context1;
            _dbContext2 = context2;
        }

測試代碼

            {
                //var data = _dbContext.Set<T_SysLoginLog>().ToList();

                myWrite($"EFCore的注入");
                myWrite($"主線程_dbContext1:{_dbContext1.guid}");
                myWrite($"主線程_dbContext2:{_dbContext2.guid}");
                Task.Run(() =>
                {
                    try
                    {
                        Thread.Sleep(5000);
                        myWrite($"下面是Task中的日志:");
                        myWrite($"子線程_dbContext1:{_dbContext1.guid}");
                        myWrite($"子線程_dbContext2:{_dbContext2.guid}");
                        var data = _dbContext1.Set<T_SysLoginLog>().ToList();
                    }
                    catch (Exception ex)
                    {
                        myWrite($"子線程報錯了:{ex.Message}");
                    }
                });
                //_dbContext1.SaveChanges();
                //_dbContext2.SaveChanges();
                _dbContext1.Dispose();
                _dbContext2.Dispose();
                myWrite($"主線程結束");
            }

測試結果:

 2. 結果與結論

  在ServiceLifetime.Transient瞬時情況下,即使在主線程中savechange、或者dispose、或者什么不做,子線程等足夠時間,然主線程已經走完,子線程中依舊可以獲取_dbContext1.guid,且和主線程中的_dbContext1.guid是一致的; 主線程中的_dbContext1.guid 和 _dbContext2.guid是不一樣的,說明是瞬時的;但是子線程中不能調用EF上下文中的任何方法,會拋異常(Cannot access a disposed object. A common cause of this error is disposing a context that was。。。。。

(請求內單例和全局單例不需要測試了,默認是請求內單例的)

推斷與解決

EFCore上下文並不是整個對象都銷毀了,因為guid屬性還是能拿到的。

那么如何解決這個問題呢?

可以在task中new一個新EFCore上下文,不用框架注入的,這樣是不受注入影響的。(在實際案例中,可以在Service層寫一個方法,然后里面new EfCore上下,把Service對應的接口IxxService類注入到控制器中,這樣子線程中的EFCore上下文不受框架注入的影響,而xxService並沒有dispose,所以主線程走完也沒問題)

 

{
  var optionsBuilder = new DbContextOptionsBuilder<ESHOPContext>();
  //傳入連接字符串           
optionsBuilder.UseSqlServer(_Configuration.GetConnectionString("EFStr"));
//創建EF上下文 ESHOPContext context = new ESHOPContext(optionsBuilder.Options); IBaseService _baseService = new BaseService(context); }

3. 解決上面內置注入留下的疑問

  結合上面內置注入遺留的疑問,可以初步的出來一個結論,如果對象沒有實現dispose,采用注入的方式,主線程走完,是不銷毀的,子線程仍然可以用。

(此處還需要進一步推斷!!!補充一下手寫dispose,destroy方法。)

 

 

三. AutoFac的注入

1.說明

(1).文檔:https://autofaccn.readthedocs.io/zh/latest/ 中文    https://autofac.org/ 英文

(2).強調:本節重點演示的Autofac聲明周期,關於core 3.x的用法,僅簡單介紹

2. Asp.Net Core3.x中的寫法

(1).通過nuget安裝程序集:【AutoFac 5.1.2】【Autofac.Extensions.DependencyInjection 6.0.0】

(2).在Program類中的CreateHostBuilder方法中添加 .UseServiceProviderFactory(new AutofacServiceProviderFactory())。

    public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .UseServiceProviderFactory(new AutofacServiceProviderFactory())   //AutoFac針對 Core3.x的寫法
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });

(3).在startup類中新增ConfigureContainer方法,用於注冊服務,該方法在ConfigureService執行完后執行。

PS:這是core3.x中最大的改進出,原ConfigureService不需要做任何變化了。

        /// <summary>
        /// AutoFac的注入
        /// 在這個方法中注冊業務,他在ConfigureService后執行
        /// </summary>
        /// <param name="builder"></param>
        public void ConfigureContainer(ContainerBuilder builder)
        {
            builder.RegisterModule<DefaultModule>();
        }

(4).將ConfigureContainer注冊的服務抽離出來一個單獨的類,如DefaultModule繼承Module類,重寫Load方法。

    public class DefaultModule : Module
    {
        protected override void Load(ContainerBuilder builder)
        {       
            //InstancePerLifetimeScope  (每個生命周期作用域)
            //InstancePerMatchingLifetimeScope (每個匹配生命周期作用域)
            //InstancePerOwned (每次被擁有的一個實例)

            builder.RegisterType<U1>().As<IU1>().InstancePerDependency(); //瞬時的
            builder.RegisterType<U2>().As<IU2>().InstancePerLifetimeScope();  //每個生命周期作用域單例

            builder.RegisterType<U3>().As<IU3>().SingleInstance();    //全局單例
            builder.RegisterType<U4>().As<IU4>().SingleInstance();  //全局單例
        }
    }

(5). 按照上述注冊的代碼,然后注入到控制器中,進行測試,得到的結果和Core中的內置注入的結果完全相同。  (這里不再詳細粘貼代碼了)

            {
                myWrite($"AutoFac測試,主線程測試");
                myWrite($"U1:{U1.guid}");
                myWrite($"U11:{U11.guid}");
                myWrite($"U2:{U2.guid}");
                myWrite($"U22:{U22.guid}");
                myWrite($"U3:{U3.guid}");
                myWrite($"U33:{U33.guid}");
                myWrite($"U4:{U4.guid}");
                myWrite($"U44:{U44.guid}");

                Task.Run(() =>
                {
                    Thread.Sleep(6000);
                    myWrite($"下面是Task中的日志:");
                    myWrite($"U1:{U1.guid}");
                    myWrite($"U11:{U11.guid}");
                    myWrite($"U2:{U2.guid}");
                    myWrite($"U22:{U22.guid}");
                    myWrite($"U3:{U3.guid}");
                    myWrite($"U33:{U33.guid}");
                    myWrite($"U4:{U4.guid}");
                    myWrite($"U44:{U44.guid}");
                });

                myWrite($"主線程結束");
            }

3.生命周期

 (1).InstancePerDependency:瞬時的 (默認就是瞬時的)

 (2).SingleInstance:全局單例

 (3).InstancePerLifetimeScope: 每個生命周期作用域內單例 (在Asp.Net Core中,AutoFac用它來替代了請求內單例,原因詳見底部)

 (4).InstancePerMatchingLifetimeScope: 名稱匹配下的嵌套作用域內單例。 

 (5).InstancePerRequest:請求內單例。已被棄用,報異常。 (實質:每個請求一個實例建立於每個匹配生命周期一個實例之上)

  PS:使用InstancePerLifetimeScope(每個生命周期作用域一個實例)而不是InstancePerRequest(每個請求一個實例). 以前的ASP.NET集成你可以注冊依賴為 InstancePerRequest ,/能保證每次HTTP請求只有唯一的依賴實例被創建. 這是因為Autofac負責 建立每個請求生命周期作用域. 隨着 Microsoft.Extensions.DependencyInjection 的引入, 每個請求和其他子生命周期作用域的創建現在是框架提供的 conforming container 的一部分, 因此所有的子生命周期作用域是被同等對待的 - 現在已經不再有特別的 "請求級別作用" .現在不再注冊你的依賴為 InstancePerRequest, 而使用 InstancePerLifetimeScope , 你也可以得到相同的行為. 注意如果你在web請求中創建 你自己的生命周期作用域 , 你將會在這些子作用域中得到新的實例.

補充InstancePerLifetimeScope:

 
         
var builder = new ContainerBuilder(); builder.RegisterType<Worker>().InstancePerLifetimeScope();
using(var scope1 = container.BeginLifetimeScope())
{
  for(var i = 0; i < 100; i++)
  {// 在這for循環里,每次resolve得到的對象都是同一個實例。
    var w1 = scope1.Resolve<Worker>();
  }
}

using(var scope2 = container.BeginLifetimeScope())
{
  for(var i = 0; i < 100; i++)
  {
    // 在這個for循環里,每次得到的對象w2都是同一個實例,但是w2和上面的w1是不同的實例!!!
    var w2 = scope2.Resolve<Worker>();
  }
}

補充InstancePerMatchingLifetimeScope:

var builder = new ContainerBuilder();
builder.RegisterType<Worker>().InstancePerMatchingLifetimeScope("myrequest");
// Create the lifetime scope using the tag.
using(var scope1 = container.BeginLifetimeScope("myrequest"))
{
  for(var i = 0; i < 100; i++)
  {
    var w1 = scope1.Resolve<Worker>();
    using(var scope2 = scope1.BeginLifetimeScope())
    {
      var w2 = scope2.Resolve<Worker>();

      //w1和w2在這個循環里永遠是一個實例,是相同。這是因為他們在一個命名匹配的生命周期作用域里
    }
  }
}

// Create another lifetime scope using the tag.
using(var scope3 = container.BeginLifetimeScope("myrequest"))
{
  for(var i = 0; i < 100; i++)
  {
    // w3 will be DIFFERENT than the worker resolved in the
    // earlier tagged lifetime scope.
    var w3 = scope3.Resolve<Worker>();
    using(var scope4 = scope3.BeginLifetimeScope())
    {
      var w4 = scope4.Resolve<Worker>();

      // w3和w4是同一個實例,但它和上面的w1和w2是不同實例。!!!
    }
  }
}

 

 

 

 

 

!

  • 作       者 : Yaopengfei(姚鵬飛)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 聲     明1 : 如有錯誤,歡迎討論,請勿謾罵^_^。
  • 聲     明2 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。
 

 


免責聲明!

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



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