第三十二节:比较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,所以主线程走完也没问题)

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 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
原文地址:https://www.cnblogs.com/yaopengfei/p/12664400.html