最近在把自己的一个老项目从Framework迁移到.Net Core 3.0,数据访问这块选择的是EFCore+Mysql。使用EF的话不可避免要和DbContext打交道,在Core中的常规用法一般是:创建一个XXXContext类继承自DbContext,实现一个拥有DbContextOptions参数的构造器,在启动类StartUp中的ConfigureServices方法里调用IServiceCollection的扩展方法AddDbContext,把上下文注入到DI容器中,然后在使用的地方通过构造函数的参数获取实例。OK,没任何毛病,官方示例也都是这么来用的。但是,通过构造函数这种方式来获取上下文实例其实很不方便,比如在Attribute或者静态类中,又或者是系统启动时初始化一些数据,更多的是如下一种场景:
public class BaseController : Controller { public BloggingContext _dbContext; public BaseController(BloggingContext dbContext) { _dbContext = dbContext; } public bool BlogExist(int id) { return _dbContext.Blogs.Any(x => x.BlogId == id); } } public class BlogsController : BaseController { public BlogsController(BloggingContext dbContext) : base(dbContext) { } }
从上面的代码可以看到,任何要继承BaseController的类都要写一个“多余”的构造函数,如果参数再多几个,这将是无法忍受的(就算只有一个参数我也忍受不了)。那么怎样才能更优雅的获取数据库上下文实例呢,我想到以下几种办法。
DbContext从哪来
1、 直接开溜new
回归原始,既然要创建实例,没有比直接new一个更好的办法了,在Framework中没有DI的时候也差不多都这么干。但在EFCore中不同的是,DbContext不再提供无参构造函数,取而代之的是必须传入一个DbContextOptions类型的参数,这个参数通常是做一些上下文选项配置例如使用什么类型数据库连接字符串是多少。
public BloggingContext(DbContextOptions<BloggingContext> options) : base(options) { }
默认情况下,我们已经在StartUp中注册上下文的时候做了配置,DI容器会自动帮我们把options传进来。如果要手动new一个上下文,那岂不是每次都要自己传?不行,这太痛苦了。那有没有办法不传这个参数?肯定也是有的。我们可以去掉有参构造函数,然后重写DbContext中的OnConfiguring方法,在这个方法中做数据库配置:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlite("Filename=./efcoredemo.db"); }
即使是这样,依然有不够优雅的地方,那就是连接字符串被硬编码在代码中,不能做到从配置文件读取。反正我忍受不了,只能再寻找其他方案。
2、 从DI容器手动获取
既然前面已经在启动类中注册了上下文,那么从DI容器中获取实例肯定是没问题的。于是我写了这样一句测试代码用来验证猜想:
var context = app.ApplicationServices.GetService<BloggingContext>();
不过很遗憾抛出了异常:
报错信息说的很明确,不能从root provider中获取这个服务。我从G站下载了DI框架的源码(地址是https://github.com/aspnet/Extensions/tree/master/src/DependencyInjection),拿报错信息进行反向追溯,发现异常来自于CallSiteValidator类的ValidateResolution方法:
public void ValidateResolution(Type serviceType, IServiceScope scope, IServiceScope rootScope) { if (ReferenceEquals(scope, rootScope) && _scopedServices.TryGetValue(serviceType, out var scopedService)) { if (serviceType == scopedService) { throw new InvalidOperationException( Resources.FormatDirectScopedResolvedFromRootException(serviceType, nameof(ServiceLifetime.Scoped).ToLowerInvariant())); } throw new InvalidOperationException( Resources.FormatScopedResolvedFromRootException( serviceType, scopedService, nameof(ServiceLifetime.Scoped).ToLowerInvariant())); } }
继续往上,看到了GetService方法的实现:
internal object GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope) { if (_disposed) { ThrowHelper.ThrowObjectDisposedException(); } var realizedService = RealizedServices.GetOrAdd(serviceType, _createServiceAccessor); _callback?.OnResolve(serviceType, serviceProviderEngineScope); DependencyInjectionEventSource.Log.ServiceResolved(serviceType); return realizedService.Invoke(serviceProviderEngineScope); }
可以看到,_callback在为空的情况下是不会做验证的,于是猜想有参数能对它进行配置。把追溯对象换成_callback继续往上翻,在DI框架的核心类ServiceProvider中找到如下方法:
internal ServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors, ServiceProviderOptions options) { IServiceProviderEngineCallback callback = null; if (options.ValidateScopes) { callback = this; _callSiteValidator = new CallSiteValidator(); } //省略.... }
说明我的猜想没错,验证是受ValidateScopes控制的。这样来看,把ValidateScopes设置成False就可以解决了,这也是网上普遍的解决方案:
.UseDefaultServiceProvider(options => { options.ValidateScopes = false; })
但这样做是极其危险的。