- ubuntu12.04环境下使用kvm ioctl接口实现最简单的虚拟机
- Ubuntu 通过无线网络安装Ubuntu Server启动系统后连接无线网络的方法
- 在Ubuntu上搭建网桥的方法
- ubuntu 虚拟机上网方式及相关配置详解
CFSDN坚持开源创造价值,我们致力于搭建一个资源共享平台,让每一个IT人在这里找到属于你的精彩世界.
这篇CFSDN的博客文章从EFCore上下文的使用到深入剖析DI的生命周期最后实现自动属性注入由作者收集整理,如果你对这篇文章有兴趣,记得点赞哟.
故事背景 。
最近在把自己的一个老项目从Framework迁移到.Net Core 3.0,数据访问这块选择的是EFCore+Mysql。使用EF的话不可避免要和DbContext打交道,在Core中的常规用法一般是:创建一个XXXContext类继承自DbContext,实现一个拥有DbContextOptions参数的构造器,在启动类StartUp中的ConfigureServices方法里调用IServiceCollection的扩展方法AddDbContext,把上下文注入到DI容器中,然后在使用的地方通过构造函数的参数获取实例。OK,没任何毛病,官方示例也都是这么来用的。但是,通过构造函数这种方式来获取上下文实例其实很不方便,比如在Attribute或者静态类中,又或者是系统启动时初始化一些数据,更多的是如下一种场景:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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类型的参数,这个参数通常是做一些上下文选项配置例如使用什么类型数据库连接字符串是多少.
1
2
3
|
public
BloggingContext(DbContextOptions<BloggingContext> options) :
base
(options)
{
}
|
默认情况下,我们已经在StartUp中注册上下文的时候做了配置,DI容器会自动帮我们把options传进来。如果要手动new一个上下文,那岂不是每次都要自己传?不行,这太痛苦了。那有没有办法不传这个参数?肯定也是有的。我们可以去掉有参构造函数,然后重写DbContext中的OnConfiguring方法,在这个方法中做数据库配置:
1
2
3
4
|
protected
override
void
OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite(
"Filename=./efcoredemo.db"
);
}
|
即使是这样,依然有不够优雅的地方,那就是连接字符串被硬编码在代码中,不能做到从配置文件读取。反正我忍受不了,只能再寻找其他方案.
2、 从DI容器手动获取 。
既然前面已经在启动类中注册了上下文,那么从DI容器中获取实例肯定是没问题的。于是我写了这样一句测试代码用来验证猜想:
1
|
var context = app.ApplicationServices.GetService<BloggingContext>();
|
不过很遗憾抛出了异常:
报错信息说的很明确,不能从root provider中获取这个服务。我从G站下载了DI框架的源码(地址是https://github.com/aspnet/Extensions/tree/master/src/DependencyInjection),拿报错信息进行反向追溯,发现异常来自于CallSiteValidator类的ValidateResolution方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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方法的实现:
1
2
3
4
5
6
7
8
9
10
11
12
|
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中找到如下方法:
1
2
3
4
5
6
7
8
9
10
|
internal
ServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors, ServiceProviderOptions options)
{
IServiceProviderEngineCallback callback =
null
;
if
(options.ValidateScopes)
{
callback =
this
;
_callSiteValidator =
new
CallSiteValidator();
}
//省略....
}
|
说明我的猜想没错,验证是受ValidateScopes控制的。这样来看,把ValidateScopes设置成False就可以解决了,这也是网上普遍的解决方案:
1
2
3
4
|
.UseDefaultServiceProvider(options =>
{
options.ValidateScopes =
false
;
})
|
但这样做是极其危险的.
为什么危险?到底什么是root provider?那就要从原生DI的生命周期说起。我们知道,DI容器被封装成一个IServiceProvider对象,服务都是从这里来获取。不过这并不是一个单一对象,它是具有层级结构的,最顶层的即前面提到的root provider,可以理解为仅属于系统层面的DI控制中心。在Asp.Net Core中,内置的DI有3种服务模式,分别是Singleton、Transient、Scoped,Singleton服务实例是保存在root provider中的,所以它才能做到全局单例。相对应的Scoped,是保存在某一个provider中的,它能保证在这个provider中是单例的,而Transient服务则是随时需要随时创建,用完就丢弃。由此可知,除非是在root provider中获取一个单例服务,否则必须要指定一个服务范围(Scope),这个验证是通过ServiceProviderOptions的ValidateScopes来控制的。默认情况下,Asp.Net Core框架在创建HostBuilder的时候会判定当前是否开发环境,在开发环境下会开启这个验证:
所以前面那种关闭验证的方式是错误的。这是因为,root provider只有一个,如果恰好有某个singleton服务引用了一个scope服务,这会导致这个scope服务也变成singleton,仔细看一下注册DbContext的扩展方法,它实际上提供的是scope服务:
如果发生这种情况,数据库连接会一直得不到释放,至于有什么后果大家应该都明白.
所以前面的测试代码应该这样写:
1
2
3
4
|
using
(var serviceScope = app.ApplicationServices.CreateScope())
{
var context = serviceScope.ServiceProvider.GetService<BloggingContext>();
}
|
与之相关的还有一个ValidateOnBuild属性,也就是说在构建IServiceProvider的时候就会做验证,从源码中也能体现出来:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
if
(options.ValidateOnBuild)
{
List<Exception> exceptions =
null
;
foreach
(var serviceDescriptor
in
serviceDescriptors)
{
try
{
_engine.ValidateService(serviceDescriptor);
}
catch
(Exception e)
{
exceptions = exceptions ??
new
List<Exception>();
exceptions.Add(e);
}
}
if
(exceptions !=
null
)
{
throw
new
AggregateException(
"Some services are not able to be constructed"
, exceptions.ToArray());
}
}
|
正因为如此,Asp.Net Core在设计的时候为每个请求创建独立的Scope,这个Scope的provider被封装在HttpContext.RequestServices中.
[小插曲] 。
通过代码提示可以看到,IServiceProvider提供了2种获取service的方式:
这2个有什么区别呢?分别查看各自的方法摘要可以看到,通过GetService获取一个没有注册的服务时会返回null,而GetRequiredService会抛出一个InvalidOperationException,仅此而已.
1
2
3
4
5
6
7
8
9
10
11
|
// 返回结果:
// A service object of type T or null if there is no such service.
public
static
T GetService<T>(
this
IServiceProvider provider);
// 返回结果:
// A service object of type T.
//
// 异常:
// T:System.InvalidOperationException:
// There is no service of type T.
public
static
T GetRequiredService<T>(
this
IServiceProvider provider);
|
终极大招 。
到现在为止,尽管找到了一种看起来合理的方案,但还是不够优雅,使用过其他第三方DI框架的朋友应该知道,属性注入的快感无可比拟。那原生DI有没有实现这个功能呢,我满心欢喜上G站搜Issue,看到这样一个回复(https://github.com/aspnet/Extensions/issues/2406):
官方明确表示没有开发属性注入的计划,没办法,只能靠自己了.
我的思路大概是:创建一个自定义标签(Attribute),用来给需要注入的属性打标签,然后写一个服务激活类,用来解析给定实例需要注入的属性并赋值,在某个类型被创建实例的时候也就是构造函数中调用这个激活方法实现属性注入。这里有个核心点要注意的是,从DI容器获取实例的时候一定要保证是和当前请求是同一个Scope,也就是说,必须要从当前的HttpContext中拿到这个IServiceProvider.
先创建一个自定义标签:
1
2
3
4
5
|
[AttributeUsage(AttributeTargets.Property)]
public
class
AutowiredAttribute : Attribute
{
}
|
解析属性的方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public
void
PropertyActivate(
object
service, IServiceProvider provider)
{
var serviceType = service.GetType();
var properties = serviceType.GetProperties().AsEnumerable().Where(x => x.Name.StartsWith(
"_"
));
foreach
(PropertyInfo property
in
properties)
{
var autowiredAttr = property.GetCustomAttribute<AutowiredAttribute>();
if
(autowiredAttr !=
null
)
{
//从DI容器获取实例
var innerService = provider.GetService(property.PropertyType);
if
(innerService !=
null
)
{
//递归解决服务嵌套问题
PropertyActivate(innerService, provider);
//属性赋值
property.SetValue(service, innerService);
}
}
}
}
|
然后在控制器中激活属性:
1
2
3
4
5
6
7
8
|
[Autowired]
public
IAccountService _accountService {
get
;
set
; }
public
LoginController(IHttpContextAccessor httpContextAccessor)
{
var pro =
new
AutowiredServiceProvider();
pro.PropertyActivate(
this
, httpContextAccessor.HttpContext.RequestServices);
}
|
这样子下来,虽然功能实现了,但是里面存着几个问题。第一个是由于控制器的构造函数中不能直接使用ControllerBase的HttpContext属性,所以必须要通过注入IHttpContextAccessor对象来获取,貌似问题又回到原点。第二个是每个构造函数中都要写这么一堆代码,不能忍。于是想有没有办法在控制器被激活的时候做一些操作?没考虑引入AOP框架,感觉为了这一个功能引入AOP有点重。经过网上搜索,发现Asp.Net Core框架激活控制器是通过IControllerActivator接口实现的,它的默认实现是DefaultControllerActivator(https://github.com/aspnet/AspNetCore/blob/master/src/Mvc/Mvc.Core/src/Controllers/DefaultControllerActivator.cs):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
/// <inheritdoc />
public
object
Create(ControllerContext controllerContext)
{
if
(controllerContext ==
null
)
{
throw
new
ArgumentNullException(nameof(controllerContext));
}
if
(controllerContext.ActionDescriptor ==
null
)
{
throw
new
ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull(
nameof(ControllerContext.ActionDescriptor),
nameof(ControllerContext)));
}
var controllerTypeInfo = controllerContext.ActionDescriptor.ControllerTypeInfo;
if
(controllerTypeInfo ==
null
)
{
throw
new
ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull(
nameof(controllerContext.ActionDescriptor.ControllerTypeInfo),
nameof(ControllerContext.ActionDescriptor)));
}
var serviceProvider = controllerContext.HttpContext.RequestServices;
return
_typeActivatorCache.CreateInstance<
object
>(serviceProvider, controllerTypeInfo.AsType());
}
|
这样一来,我自己实现一个Controller激活器不就可以接管控制器激活了,于是有如下这个类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
public
class
HosControllerActivator : IControllerActivator
{
public
object
Create(ControllerContext actionContext)
{
var controllerType = actionContext.ActionDescriptor.ControllerTypeInfo.AsType();
var instance = actionContext.HttpContext.RequestServices.GetRequiredService(controllerType);
PropertyActivate(instance, actionContext.HttpContext.RequestServices);
return
instance;
}
public
virtual
void
Release(ControllerContext context,
object
controller)
{
if
(context ==
null
)
{
throw
new
ArgumentNullException(nameof(context));
}
if
(controller ==
null
)
{
throw
new
ArgumentNullException(nameof(controller));
}
if
(controller
is
IDisposable disposable)
{
disposable.Dispose();
}
}
private
void
PropertyActivate(
object
service, IServiceProvider provider)
{
var serviceType = service.GetType();
var properties = serviceType.GetProperties().AsEnumerable().Where(x => x.Name.StartsWith(
"_"
));
foreach
(PropertyInfo property
in
properties)
{
var autowiredAttr = property.GetCustomAttribute<AutowiredAttribute>();
if
(autowiredAttr !=
null
)
{
//从DI容器获取实例
var innerService = provider.GetService(property.PropertyType);
if
(innerService !=
null
)
{
//递归解决服务嵌套问题
PropertyActivate(innerService, provider);
//属性赋值
property.SetValue(service, innerService);
}
}
}
}
}
|
需要注意的是,DefaultControllerActivator中的控制器实例是从TypeActivatorCache获取的,而自己的激活器是从DI获取的,所以必须额外把系统所有控制器注册到DI中,封装成如下的扩展方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
/// <summary>
/// 自定义控制器激活,并手动注册所有控制器
/// </summary>
/// <param name="services"></param>
/// <param name="obj"></param>
public
static
void
AddHosControllers(
this
IServiceCollection services,
object
obj)
{
services.Replace(ServiceDescriptor.Transient<IControllerActivator, HosControllerActivator>());
var assembly = obj.GetType().GetTypeInfo().Assembly;
var manager =
new
ApplicationPartManager();
manager.ApplicationParts.Add(
new
AssemblyPart(assembly));
manager.FeatureProviders.Add(
new
ControllerFeatureProvider());
var feature =
new
ControllerFeature();
manager.PopulateFeature(feature);
feature.Controllers.Select(ti => ti.AsType()).ToList().ForEach(t =>
{
services.AddTransient(t);
});
}
|
在ConfigureServices中调用:
1
|
services.AddHosControllers(
this
);
|
到此,大功告成!可以愉快的继续CRUD了.
结尾 。
市面上好用的DI框架一堆一堆的,集成到Core里面也很简单,为啥还要这么折腾?没办法,这不就是造轮子的乐趣嘛。上面这些东西从头到尾也折腾了不少时间,属性注入那里也还有优化的空间,欢迎探讨.
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我.
原文链接:https://www.cnblogs.com/hohoa/p/11884719.html 。
最后此篇关于从EFCore上下文的使用到深入剖析DI的生命周期最后实现自动属性注入的文章就讲到这里了,如果你想了解更多关于从EFCore上下文的使用到深入剖析DI的生命周期最后实现自动属性注入的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
最近做一个项目,由于是在别人框架里开发app,导致了很多限制,其中一个就是不能直接引用webservice 。 我们都知道,调用webserivice 最简单的方法就是在 "引用"
这是SDL2代码的一部分 SDL主函数 int main(int argc,char *argv[]) { ... ... bool quit=false; S
c 中的函数: PHPAPI char *php_pcre_replace(char *regex, int regex_len, ch
我有以下映射: public class SecurityMap : ClassMap { public SecurityMap() {
我在vue-lic3中使用了SCSS,但是有一个奇怪的错误,使用/ deep /会报告错误,我不想看到它。 代码运行环境 vue-cli3 + vant + scss 的CSS /deep/ .van
我在深入阅读 C# 时遇到了这个我能理解的内容: 当它被限制为引用类型时,执行的比较类型完全取决于类型参数被限制为什么。 但是不能理解这个: 如果进一步限制派生自重载 == 和 != 运算符的特定类型
Closed. This question is opinion-based。它当前不接受答案。 想改善这个问题吗?更新问题,以便editing this post用事实和引用来回答。 3年前关闭。
有人可以详细介绍关于自赋值的运算符重载中的 *this 和 const 例如: Class& Class::operator=(const Class& other) { a = other.
在向树中插入新节点时,如何填充闭包表的深度/长度列? ancestor 和 descendant 中的值是来自另一个表的 ID,表示要以树结构排列的页面。 关闭表: ancestor desce
现在我正在阅读“深入了解 C#”。缺少的一件事是完成一章后我可以解决的一系列问题。那会帮助我理解我刚刚学到的概念。 哪里可以找到适合 C#3.0 的问题集? 谢谢 最佳答案 你可以试试LINQ 101
TypeScript 给 JavaScript 扩展了类型的语法,我们可以给变量加上类型,在编译期间会做类型检查,配合编辑器还能做更准确的智能提示。此外,TypeScript 还支持了高级类型用
是否有一个单行代码来获取生成器并生成该生成器中的所有元素?例如: def Yearly(year): yield YEARLY_HEADER for month in range(1, 13)
所以我阅读了一些与“什么是方法组”相关的 StackOverflow 问题以及其他互联网文章,它们在底线都说了同样的话——方法组是“一组重载方法” ". 但是,在阅读 Jon Skeet 的“C# 深
有什么方法可以从子组件中获取子组件吗? 想象一下以下组件树: 应用程序 问题 问题选项(包含复选框) 问题选项(包含复选框) 问题选项(包含复选框) 我想从 App 访问问题选项以选中所有复选框。 参
class_eval 和 instance_eval 在定义方法等情况下是完全可以预测的。我也理解类的实例和类的单例(又名特征类)之间的区别。 但是 我无法弄清楚以下唯一的事情:比方说,出于某些策略目
我想出了如何将符号 rwx 部分读取/转换为 421 个八进制部分,这非常简单。但是当涉及到特殊字符时,我感到非常困惑。我们知道 -r-xr---wx 转换为 0543,但 -r-sr---wt 或
我怀疑我系统的 Java 版本有问题。某些应用程序出现段错误或内存不足或存在链接错误。如果我从源代码安装了 JDK,我会做类似“make test”的事情,看看哪些测试失败了。但是,看起来从源代码构建
如何克隆一个 repo(使用 libgit2 ) 我想做什么git clone确实,但有 libgit2 .我可能要问的是什么 git clone确实很深入。 这是我目前正在做的: 初始化一个repo
00、头痛的JS闭包、词法作用域? 被JavaScript的闭包、上下文、嵌套函数、this搞得很头痛,这语言设计的,感觉比较混乱,先勉强理解总结一下😂😂😂.
我开始玩 lubridate R 中的包。我注意到 now(tzone="EST")计算为: [1] "2015-08-25 13:01:08 EST" 而 now(tzone="PST")导致警告:
我是一名优秀的程序员,十分优秀!