• 《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

     

    博文源码

    相关资料

  • 相关阅读:
    蛙蛙推荐:蛙蛙牌正文提取算法
    javascript太牛了,还能模拟函数式编程
    蛙蛙推荐:编写一个服务监控及管理的软件
    蛙蛙推荐:Remoting超时问题及初步解决方案
    WaTu项目简介
    蛙蛙推荐:基于标记窗的网页正文提取算法的一些细节问题
    【蛙蛙推荐】windbg使用小总结
    蛙蛙推荐:windbg里查看DateTime值
    Enterprise Library :数据访问程序块学习1 dodo
    (转)一个带自定义分页,排序功能的DATAGRID控件(公开源码) dodo
  • 原文地址:https://www.cnblogs.com/zhaopei/p/auto-injection.html
Copyright © 2020-2023  润新知