Zongsoft.Services
理解 Zongsoft.Services 的应用上下文、服务注册、服务解析和插件服务发现。
Zongsoft.Services 是 Zongsoft 运行时的服务模型。它连接标准 .NET 依赖注入、应用上下文、应用模块和插件树,让宿主程序、插件程序集和声明式构件可以把能力注册到同一个运行时,并按应用、模块或插件树位置解析出来。
它不是要替代 .NET DI,而是在 IServiceCollection 源码、System.IServiceProvider 和 IServiceProviderFactory<TContainerBuilder> 源码 之上补充这些能力:
应用上下文:用
IApplicationContext表示当前应用实例,统一暴露配置、环境、模块、服务、事件、工作器和生命周期。应用模块:用
IApplicationModule表示一个子系统或插件模块,并为模块提供自己的服务解析域。服务发现:通过服务名称、标签、匹配参数、模块名和插件树表达式寻找服务,而不只按类型解析。
服务构建:通过
ServiceAttribute、IServiceRegistration、ServiceDependencyAttribute和ServiceProviderFactory把声明式注册、代码注册和属性注入串起来。分布式协作:在
Zongsoft.Services.Distributing中提供分布式锁和锁令牌等基础抽象。
运行时结构
在插件式应用中,服务模型通常形成三层:
应用服务容器
ApplicationContext.Current.Services
全局默认容器,承载宿主、插件和框架注册的服务。
模块服务容器
ApplicationContext.Current.Modules["模块名"].Services
以模块名为边界解析服务;模块内找不到时会回退到应用服务容器。
插件树构件
/Workspace/Environment/Services、/Workbench/...
声明式对象和扩展点;其中服务节点会被注册到应用服务容器,其他节点通常通过路径或构件解析器发现。
这种结构的设计意图是把“进程级基础设施”和“插件提供的业务能力”分开:宿主负责建立 Host 和服务容器,插件负责声明程序集、构件和扩展点,应用上下文负责把两者组织成可查询的运行时。
注册来源
服务进入应用通常有四个来源。
代码注册
宿主或插件可以在 Host 构建阶段直接向服务集合添加服务。下面是 framework 中 gRPC 扩展的真实注册器,配置协议服务及反射支持。普通 .NET 服务生命周期仍然按 DI 规则生效,例如单例、作用域和瞬态。
来源:framework/Zongsoft.Web/grpc/GrpcInitializer.cs(节选;上下文见源文件)。
代码注册适合宿主私有服务、启动入口相关服务,或需要明确生命周期的基础设施服务。
程序集扫描
插件宿主在构建应用时,会调用 ServiceCollectionExtension.Register 扫描程序集中的公共类型:
先扫描入口程序集引用的程序集。
再扫描入口程序集本身。
再递归扫描已加载插件
manifest中声明的程序集;同一个程序集只注册一次。
扫描时有两种注册模式:
实现 IServiceRegistration
一个程序集需要集中注册多项服务、复杂生命周期或条件注册。
框架创建注册器实例,调用 Register(services, configuration);注册器接管该类型的服务注册。
标注 ServiceAttribute
类型本身就是服务,适合简单、约定化注册。
框架把实现类型注册为单例,并按契约、名称、标签和静态成员规则追加注册。
来源:src/Services/ForumService.cs(节选;上下文见源文件)。
当 ServiceAttribute 指定名称时,框架会登记该名称,名称以 Service 结尾时还会登记去掉后缀后的短名。因此 Discussions 的 ForumService 会登记 ForumService 名称以及 Forum 短名。
静态成员注册
如果服务对象本来就是静态属性或字段,可以通过 ServiceAttribute.Members 暴露成员值。框架会读取指定的公开静态成员,并按成员类型和显式契约注册为单例实例。
来源:framework/Zongsoft.Diagnostics/protocols/server/src/Listener.Metrics.cs(节选;上下文见源文件)。
这种方式适合无状态、全局唯一、已经由框架或第三方库提供的对象。需要依赖注入构造参数的服务,优先使用普通类型注册。
插件树服务节点
插件宿主还会查找 /Workspace/Environment/Services 节点,并把该节点下的每个子构件作为单例服务注册到应用服务集合中。服务类型来自构件的 ValueType,服务实例由构件在解析时创建或解包。
Discussions 的插件声明把模块挂到 /Workbench/Modules,再暴露数据访问器、事件与属性。它没有声明 /Workspace/Environment/Services 节点;完整清单见插件文件,不要把任意模块节点当成此专用服务注册节点。
这种方式适合需要从插件文件声明、用 {option:...}、{path:...} 或 {service:...} 组装的服务。它的生命周期在当前宿主实现中按单例注册,因此不要把请求级状态、会话状态或必须频繁重建的对象放在这里。
服务解析
服务解析分为按类型、按名称、按标签和按匹配参数四类。
类型解析
Resolve<T>()、ResolveRequired<T>()、ResolveAll<T>()
对标准 GetService、GetRequiredService、GetServices 的语义封装。
名称解析
Resolve("name")、ResolveRequired("name")
根据 ServiceAttribute.Name 登记的名称解析服务。
匹配解析
Find<T>(argument)、FindAll<T>(argument)
在同一契约的多个实现中查找匹配者。服务可实现 IMatchable 或 IMatcher<T>,也可以通过 Name 属性与参数做忽略大小写匹配。
标签解析
Resolves(tag)、GetTags(tag)
按 ServiceAttribute.Tags 组织服务集合,适合把一组同类扩展归入同一用途。
按类型解析
按类型解析适合普通依赖:调用方知道需要哪个契约,不关心具体实现是谁。Resolve<T>() 适合可选依赖,ResolveRequired<T>() 适合缺失即失败的强依赖,ResolveAll<T>() 适合初始化器、处理器、过滤器这类多实现集合。
ApplicationContext 中有两个典型用法:
Initialize()通过ResolveAll<IApplicationInitializer>()收集所有应用初始化器,然后逐个执行。
来源:framework/Zongsoft.Core/src/Services/ApplicationContext.cs(节选;上下文见源文件)。
来源:framework/Zongsoft.Core/src/Services/ApplicationContext.cs(节选;上下文见源文件)。
这两个例子体现了类型解析的边界:单个可选对象用 Resolve<T>(),多扩展点集合用 ResolveAll<T>()。如果调用方必须拿到服务才能继续,则改用 ResolveRequired<T>(),把注册缺失暴露为明确异常。
按名称解析
按名称解析适合“文本配置最终指向一个服务实例”的场景。核心库中的 MessageQueueConverter 就是这样的例子:它把字符串转换为 IMessageQueue。
它的解析顺序是:
如果文本形如
queue@provider,先用Find<IMessageQueueProvider>(provider)找到对应队列提供器,再从该提供器取队列。如果没有指定提供器,就遍历
ResolveAll<IMessageQueueProvider>(),找到第一个包含该队列名的提供器。如果所有提供器都找不到,最后才用
Resolve(text)按服务名称解析队列实例。
来源:framework/Zongsoft.Core/src/Messaging/MessageQueueConverter.cs(节选;上下文见源文件)。
这里的 Resolve(text) 是兜底方案,适合某个队列对象本身已经通过 ServiceAttribute.Name 登记为命名服务的情况。若配置必须命中命名服务,可以使用 ResolveRequired(name),让配置错误在解析阶段直接失败。
根据参数查找
Find<T>(argument) 和 FindAll<T>(argument) 适合同一契约存在多个实现,并且调用方只知道一个“选择参数”的场景。核心库里大量基础类型已经为这种模式实现了 IMatchable 或 IMatchable<string>。
例如 ExpressionEvaluatorBase 以 Name 表示表达式求值器名称,并用 IMatchable 支持忽略大小写匹配。调用方只需要传入名称,就能在多个 IExpressionEvaluator 实现中找到目标求值器。
来源:framework/Zongsoft.Core/src/Expressions/ExpressionEvaluatorBase.cs(节选;上下文见源文件)。
实际按名称匹配并取得队列的调用已经展示在上面的 MessageQueueConverter 中。表达式求值器的执行范例见脚本与表达式,名称是否可用取决于对应扩展包是否注册。
TextRegular 则把匹配参数当作待验证文本。也就是说,Find<ITextRegular>(text) 不是按名称找正则,而是在一组文本规则服务中找到能够匹配该文本的规则。
来源:framework/Zongsoft.Core/src/Text/TextRegular.cs(节选;上下文见源文件)。
消息队列提供器也是典型场景。MessageQueueFactoryBase 通过名称匹配队列工厂,MessageQueueConverter 解析 queue@provider 时就调用了 Find<IMessageQueueProvider>(provider)。
来源:framework/Zongsoft.Core/src/Messaging/MessageQueueFactoryBase.cs(节选;上下文见源文件)。
因此,Find 的关键不是“按类型拿第一个服务”,而是让每个候选服务自己判断“我是否适合这个参数”。当没有实现 IMatchable 或 IMatcher<T> 时,框架才会尝试用服务的 Name 属性做默认匹配。
获取标签集
标签适合把服务按“用途”组织起来,而不是按类型或名称选择单个实现。Zongsoft.Diagnostics.Protocols.Server 和 Zongsoft.Web.Grpc 就用标签把 gRPC 服务注册与端点映射解耦。
在 Listener.Metrics.cs 中,Metrics 静态成员被标注为服务,并打上 gRPC 标签:
静态成员及其 Service 注解见上文“静态成员注册”的真实源码片段。
ServiceCollectionExtension 扫描到这个注解时,会把 Metrics 成员值注册为服务,并把该服务类型归入 gRPC 标签。到了 GrpcInitializer,初始化器不需要知道有哪些诊断或业务 gRPC 服务,只要读取标签下的服务类型即可:
来源:framework/Zongsoft.Web/grpc/GrpcInitializer.cs(节选;上下文见源文件)。
这个例子的适用场景很明确:插件或模块负责声明“我是一个 gRPC 服务”,Web gRPC 初始化器负责统一映射所有带 gRPC 标签的服务。双方不需要互相引用具体实现。
按标签解析
GetTags(tag) 返回标签下的服务类型,适合 GrpcInitializer 这类“只需要类型,不需要实例”的场景。Resolves(tag) 和 Resolves(Type, tag) 则会进一步从服务容器中解析实例,适合需要直接调用标签下服务对象的场景。
框架 TaggedServiceTest 提供了真实的实例解析测试。ITaggedContractA、ITaggedContractB、TaggedService 与 ProviderScope 均定义在同一测试文件;两个契约应解析到同一个对象。
来源:framework/Zongsoft.Core/test/Services/TaggedServiceTest.cs(节选;上下文见源文件)。
测试通过下列工厂扫描测试程序集,再创建框架服务提供器。解析前必须完成登记;标签不依靠调用方临时附加到对象上。
来源:framework/Zongsoft.Core/test/Services/TaggedServiceTest.cs(节选;上下文见源文件)。
实际映射 gRPC 端点时选择 GetTags("gRPC") 更合适,因为 MapGrpcService<TService>() 需要的是服务类型;需要执行服务对象行为时,再使用 Resolves(...) 解析实例。
模块服务域
模块服务域用于解决“同一个契约在不同模块里可能有不同实现”的问题。模块的 Services 属性会基于应用服务容器创建一个带模块名的服务提供器;解析时会先尝试模块化服务,再回退到应用服务容器。
模块化服务的来源通常是带 ApplicationModuleAttribute 的服务类型。框架扫描到服务契约时,会额外登记带模块名的包装服务,使模块容器能优先取到本模块实现。
来源:src/Module.cs(节选;上下文见源文件)。
Discussions 在程序集上声明 ApplicationModule,并把 Module.NAME 定义为 Discussions。MessageSendCommand 的属性注入明确选择同一模块服务域。
当对象位于某个模块或插件树节点下时,框架会尽量根据对象所属模块选择服务容器。这样业务插件可以声明自己的模块服务,同时仍能复用应用级公共服务。
属性注入
构造函数注入仍然是首选。属性或字段注入主要用于插件构件、运行时反射构建对象、可选依赖或需要按模块名选择服务的场景。
ServiceDependencyAttribute 支持三类选择:
不指定 Provider
使用注入目标所在模块的服务容器;找不到时回退到应用服务容器。
Provider = "/" 或 Provider = "*"
直接使用应用服务容器。
Provider = "模块名"
使用指定模块的服务容器;找不到时回退到应用服务容器。
来源:src/Services/Commands/MessageSendCommand.cs(节选;上下文见源文件)。
如果设置 ServiceName,注入器会改用 IServiceProvider<T> 的 GetService(string) 查找命名服务。ServiceName = "~" 或 ServiceName = "." 表示把注入目标所在模块名作为服务名。
插件中的服务发现
插件文件里的 {service:...} 表达式由 Zongsoft.Plugins 中的 ServicesParser 处理。它不是单纯从全局容器取对象,而会结合当前构件位置和显式容器名选择服务域。
{service:@}
返回应用默认服务容器。
{service:@模块名}
返回名为 模块名 的模块服务容器;模块不存在则返回空。
{service:服务名}
从当前构件所属模块容器或应用容器解析名为 服务名 的服务。
{service:服务名@模块名}
从 模块名 模块服务容器解析名为 服务名 的服务。
{service:~}
按当前目标成员类型解析一个服务。
{service:*}
按当前目标成员类型解析所有服务。
{service:~@}、{service:*@}
强制从应用默认服务容器按目标成员类型解析。
{service:~@模块名}、{service:*@模块名}
从指定模块服务容器按目标成员类型解析。
上表的“模块名”“服务名”是语法占位符,并非已注册的服务。Discussions.plugin 实际使用 static 与 path 表达式挂载模块、访问器和过滤器,可对照构件与服务阅读。
所有格式还可以在服务对象后继续访问属性或字段,例如 {service:服务名.属性名@模块名}。这适合在插件构件属性中引用已注册服务的某个配置对象或子对象。
插件宿主的构建顺序
Zongsoft.Plugins.Hosting 中的应用构建器把服务注册和插件加载编排在一起。以 Application.Daemon(...)、Application.Terminal(...) 为入口时,构建过程大致如下:
创建 .NET Host 构建器,并把容器工厂设置为
Zongsoft.Services.ServiceProviderFactory。按应用名加载宿主
.option配置。创建
PluginOptions,把插件配置源加入应用配置。调用
PluginTree.Get(options).Load()加载插件树。扫描宿主引用程序集、宿主程序集和插件清单程序集,执行服务注册。
注册默认
System.Net.Http.HttpClient服务。把
/Workspace/Environment/Services下的构件注册为单例服务。构建 Host,并通过
Initialize()初始化PluginApplicationContext。
Daemon 和 Terminal 会先注册各自的应用上下文实现,再映射为 PluginApplicationContext 和 IApplicationContext。这意味着应用代码通常只依赖 IApplicationContext,而插件宿主内部仍能使用更具体的插件上下文访问插件树和工作台。
使用建议
宿主专属、生命周期敏感、需要立即配置的服务,用代码注册。
插件程序集中的普通业务服务,用
ServiceAttribute或IServiceRegistration注册。需要在插件文件中声明、可由配置或路径表达式组装的对象,放到
/Workspace/Environment/Services。需要被其他插件按扩展点发现的对象,优先挂到约定插件树路径,而不是强行放进 DI 容器。
同一契约有多个实现时,用
Find<T>(argument)、IMatchable、IMatcher<T>或标签组织,不要把实现选择逻辑写死在调用方。模块内部服务尽量标注
ApplicationModuleAttribute,让模块容器可以优先解析本模块实现。
子命名空间
Zongsoft.Services.Distributing
分布式锁、锁令牌和分布式协作基础类型。
相关资源
关于按契约解析、按名称匹配、提供者定位以及共享实例所有权,参见服务定位与所有权。表达式实现的语言与并发差异见脚本与表达式。
最后更新于