For the complete documentation index, see llms.txt. This page is also available as Markdown.

Zongsoft.Services

Understand the application context, service registration, service resolution and plugin service discovery of Zongsoft.Services.

Zongsoft.Services is the service model of Zongsoft runtime. It connects standard .NET dependency injection, application context, application modules, and plugin trees, allowing hosts, plugin assemblies, and declarative builtins to register capabilities into the same runtime and resolve them by application, module, or plugin tree location.

It is not intended to replace .NET DI, but rather complements these capabilities on top of IServiceCollection Source, System.IServiceProvider, and IServiceProviderFactory<TContainerBuilder> Source:

  • application context: Use IApplicationContext to represent the current application instance, uniformly exposing configuration, environment, modules, services, events, workers and lifecycle.

  • Application module: Use IApplicationModule to represent a subsystem or plugin module, and provide the module with its own service resolution domain.

  • Service discovery: Find services by service name, tags, matching parameters, module names, and plugin tree expressions, rather than just parsing by type.

  • Service building: String declarative registration, code registration and property injection through ServiceAttribute, IServiceRegistration, ServiceDependencyAttribute and ServiceProviderFactory.

  • Distributed collaboration: Basic abstractions such as distributed lock and lock tokens are provided in Zongsoft.Services.Distributing.

Runtime Structure

In plugin-based applications, the service model usually forms three layers:

Hierarchy
entrance
function

Application service container

ApplicationContext.Current.Services

The global default container hosts services registered by hosts, plugins, and frameworks.

module service container

ApplicationContext.Current.Modules["模块名"].Services

The service is resolved based on the module name boundary; if it is not found within the module, it will fallback to the application service container.

plugin tree builtin

/Workspace/Environment/Services、/Workbench/...

Declarative objects and extension points; where service nodes are registered with the application service container, other nodes are usually discovered through paths or builtin resolvers.

The design intention of this structure is to separate "process-level infrastructure" and "business capabilities provided by plugins": the host is responsible for establishing the Host and service container, the plugin is responsible for declaring assembly, builtin and extension point, and the application context is responsible for organizing the two into a queryable runtime.

Not all objects in the plugin tree are DI services. Only objects that are scanned and registered by assembly, explicitly registered by code, or are hung under /Workspace/Environment/Services and added to the service collection by the host build process will enter the service container.

Registration Source

Services typically enter an application from four sources.

Code Registration

Hosts or plugins can add services directly to the services collection during the Host build phase. Below is the actual register for the gRPC extension in the framework, configuring protocol services and reflection support. Normal .NET service lifecycle still takes effect according to DI rules, such as singleton, scope and transient.

Source: framework/Zongsoft.Web/grpc/GrpcInitializer.cs (excerpt; see source for context).

Code registration is suitable for hosting private services, starting portal-related services, or infrastructure services that require a clear lifecycle.

Assembly Scan

When the plugin host builds the application, it will call ServiceCollectionExtension.Register to scan the public types in the assembly:

  1. First scan the assembly referenced by the entry assembly.

  2. Then scan the entry assembly itself.

  3. Then recursively scan the assembly declared in the loaded plugin manifest; the same assembly is only registered once.

There are two registration modes when scanning:

mode
Applicable scenarios
behavior

Implement IServiceRegistration

An assembly needs to centrally register multiple services, complex lifecycles, or conditional registrations.

The framework creates a registrar instance and calls Register(services, configuration); the registrar takes over service registration of this type.

Mark ServiceAttribute

The type itself is a service, suitable for simple and conventional registration.

The framework registers the implementation type as a singleton and additionally registers it by contract, name, label, and static member rules.

Source: src/Services/ForumService.cs (excerpt; see source for context).

When ServiceAttribute specifies a name, the framework will register the name. When the name ends with Service, the short name without the suffix will also be registered. Therefore, the ForumService of Discussions will register the ForumService name and the Forum short name.

Static Member Registration

If the service object is inherently a static property or field, the member value can be exposed through ServiceAttribute.Members. The framework reads the specified public static members and registers them as singleton instances by member type and explicit contract.

Source: framework/Zongsoft.Diagnostics/protocols/server/src/Listener.Metrics.cs (excerpt; see source for context).

This method is suitable for objects that are stateless, globally unique, and already provided by the framework or third-party library. For services that require dependency injection of construction parameters, ordinary type registration is preferred.

Plugin Tree Service Node

The plugin host will also look for the /Workspace/Environment/Services node and register each child builtin under the node as a singleton service into the application service collection. The service type is ValueType from the builtin, and the service instance is created or unpacked by the builtin at parse time.

The Discussions plugin declaration hangs the module in /Workbench/Modules, and then exposes data accessor, events and properties. It does not declare the /Workspace/Environment/Services node; see plugin manifest for a complete list. Do not treat any module node as this dedicated service registration node.

This method is suitable for services that require slave plugin file declaration and are assembled with {option:...}, {path:...} or {service:...}. Its lifecycle is registered as a singleton in the current host implementation, so don't put request-level state, session state, or objects that must be frequently rebuilt here.

Service Analysis

Service resolution is divided into four categories: by type, by name, by label, and by matching parameters.

way
API
Description

type resolution

Resolve<T>()、ResolveRequired<T>()、ResolveAll<T>()

Semantic encapsulation of standards GetService, GetRequiredService, GetServices.

name resolution

Resolve("name")、ResolveRequired("name")

Name resolution service registered under ServiceAttribute.Name.

Match parsing

Find<T>(argument)、FindAll<T>(argument)

Find matches among multiple implementations of the same contract. The service can implement IMatchable or IMatcher<T>, and can also use the Name attribute and parameters to ignore case matching.

Tag parsing

Resolves(tag)、GetTags(tag)

Organizing service collections according to ServiceAttribute.Tags is suitable for grouping a group of similar extensions into the same purpose.

Parse by Type

Parsing by type is suitable for ordinary dependencies: the caller knows which contract is required and does not care who implements it. Resolve<T>() is suitable for optional dependencies, ResolveRequired<T>() is suitable for strong dependencies that fail if missing, and ResolveAll<T>() is suitable for multi-implementation collections such as initializers, handlers, and filters.

There are two typical usages in ApplicationContext:

  • Exit(...) finds the current IHost Source via Resolve<IHost>(). Only when the host and lifecycle services exist and the host has not been stopped, the process will be stopped; then System.Environment.Exit will still be called to exit the process. Therefore this method cannot be treated as a "do nothing without a host" query.

  • Initialize() collects all application initializers through ResolveAll<IApplicationInitializer>() and then executes them one by one.

Source: framework/Zongsoft.Core/src/Services/ApplicationContext.cs (excerpt; see source for context).

Source: framework/Zongsoft.Core/src/Services/ApplicationContext.cs (excerpt; see source for context).

These two examples illustrate the boundaries of type resolution: Resolve<T>() for a single optional object, and ResolveAll<T>() for a collection of multiple extension points. If the caller must get the service before continuing, use ResolveRequired<T>() instead, which exposes the missing registration as an explicit exception.

Parse by Name

Resolution by name is suitable for scenarios where the text configuration ultimately points to a service instance. MessageQueueConverter in the core library is an example of this: it converts a string to IMessageQueue.

Its parsing order is:

  1. If the text is in the shape of queue@provider, first use Find<IMessageQueueProvider>(provider) to find the corresponding queue provider, and then get the queue from the provider.

  2. If no provider is specified, ResolveAll<IMessageQueueProvider>() is traversed to find the first provider containing the queue name.

  3. If all providers are not found, the queue instance is finally resolved by service name using Resolve(text).

Source: framework/Zongsoft.Core/src/Messaging/MessageQueueConverter.cs (excerpt; see source for context).

Resolve(text) here is a fallback solution, suitable for situations where a queue object itself has been registered as a naming service through ServiceAttribute.Name. If the configuration must hit the naming service, you can use ResolveRequired(name) to cause configuration errors to fail directly in the parsing phase.

Search by Parameters

Find<T>(argument) and FindAll<T>(argument) are suitable for scenarios where there are multiple implementations of the same contract and the caller only knows one "selection parameter". A large number of basic types in the core library already implement IMatchable or IMatchable<string> for this mode.

For example, ExpressionEvaluatorBase represents the expression evaluator name with Name, and IMatchable supports ignoring case matching. The caller only needs to pass in the name to find the target evaluator among multiple IExpressionEvaluator implementations.

Source: framework/Zongsoft.Core/src/Expressions/ExpressionEvaluatorBase.cs (excerpt; see source for context).

The actual call to match by name and get the queue is shown in the MessageQueueConverter above. See Scripts and Expressions for an execution example of the expression evaluator. Whether the name is available depends on whether the corresponding extension package is registered.

TextRegular treats the matching parameters as text to be verified. In other words, Find<ITextRegular>(text) does not find regular rules by name, but finds rules that can match the text in a set of text rule services.

Source: framework/Zongsoft.Core/src/Text/TextRegular.cs (excerpt; see source for context).

Message queue providers are also typical scenarios. MessageQueueFactoryBase matches the queue factory by name, and Find<IMessageQueueProvider>(provider) is called when MessageQueueConverter parses queue@provider.

Source: framework/Zongsoft.Core/src/Messaging/MessageQueueFactoryBase.cs (excerpt; see source for context).

Therefore, the key to Find is not to "get the first service by type", but to let each candidate service determine for itself "whether I fit this parameter." When IMatchable or IMatcher<T> is not implemented, the framework will try to use the Name attribute of the service for default matching.

Get Label Set

Tags are good for organizing services by "purpose" rather than selecting individual implementations by type or name. Zongsoft.Diagnostics.Protocols.Server and Zongsoft.Web.Grpc use labels to decouple gRPC service registration from endpoint mapping.

In Listener.Metrics.cs, the Metrics static member is marked as a service and labeled gRPC:

For static members and their Service annotations, see the real source code snippet of "Static Member Registration" above.

When ServiceCollectionExtension scans this annotation, it will register the Metrics member value as a service and classify the service type into the gRPC tag. By GrpcInitializer, the initializer does not need to know which diagnostics or business gRPC services there are, it only needs to read the service type under the tag:

Source: framework/Zongsoft.Web/grpc/GrpcInitializer.cs (excerpt; see source for context).

The applicable scenario of this example is clear: the plugin or module is responsible for declaring "I am a gRPC service", and the Web gRPC initializer is responsible for uniformly mapping all services with the gRPC label. Both parties do not need to reference each other's specific implementations.

Parse by Tag

GetTags(tag) returns the service type under the label, which is suitable for scenarios such as GrpcInitializer that "only require types, no instances". Resolves(tag) and Resolves(Type, tag) will further parse the instance from the service container, which is suitable for scenarios where the service object under the label needs to be directly called.

The framework TaggedServiceTest provides real instance parsing testing. ITaggedContractA, ITaggedContractB, TaggedService and ProviderScope are all defined in the same test file; the two contracts should resolve to the same object.

Source: framework/Zongsoft.Core/test/Services/TaggedServiceTest.cs (excerpt; see source for context).

The test scans the test assembly through the following factory and then creates the framework service provider. Registration must be completed before parsing; tags do not rely on the caller to be temporarily attached to the object.

Source: framework/Zongsoft.Core/test/Services/TaggedServiceTest.cs (excerpt; see source for context).

It is more appropriate to choose GetTags("gRPC") when actually mapping the gRPC endpoint, because MapGrpcService<TService>() requires the service type; when you need to perform the service object behavior, use Resolves(...) to resolve the instance.

Module Service Domain

The module service domain is used to solve the problem of "the same contract may have different implementations in different modules". The Services attribute of the module will create a service provider with the module name based on the application service container; when parsing, it will first try the modular service and then fallback to the application service container.

The source of modular services is usually a service type with ApplicationModuleAttribute. When the framework scans the service contract, it will additionally register the packaging service with the module name, so that the module container can obtain the implementation of this module first.

Source: src/Module.cs (excerpt; see source for context).

Discussions Declare the ApplicationModule on the assembly and define Module.NAME as Discussions. Property injection for MessageSendCommand explicitly selects the same module service domain.

When an object is located under a module or plugin tree node, the framework will try to select a service container based on the module to which the object belongs. In this way, business plugins can declare their own module services while still reusing application-level public services.

Property Injection

Constructor injection is still preferred. Property or field injection is mainly used for plugin builtin, runtime reflection construction objects, optional dependencies, or scenarios where services need to be selected by module name.

ServiceDependencyAttribute supports three types of choices:

settings
meaning

Do not specify Provider

Use the service container of the module where the injection target is located; fallback to the application service container when it is not found.

Provider = "/" or Provider = "*"

Use the application service container directly.

Provider = "模块名"

Use the service container of the specified module; fallback to the application service container when it is not found.

Source: src/Services/Commands/MessageSendCommand.cs (excerpt; see source for context).

If ServiceName is set, the injector will instead use the GetService(string) lookup naming service for IServiceProvider<T>. ServiceName = "~" or ServiceName = "." indicates that the module name where the injection target is located is used as the service name.

Service Discovery in Plugins

The {service:...} expression in the plugin manifest is processed by ServicesParser in Zongsoft.Plugins. Instead of simply fetching objects from the global container, it selects the service domain based on the current builtin location and explicit container name.

expression
result

{service:@}

Return the application default service container.

{service:@模块名}

Returns the module service container named 模块名; if the module does not exist, it returns empty.

{service:服务名}

Resolve a service named 服务名 from the module container or application container to which the current builtin belongs.

{service:服务名@模块名}

Resolve a service named 服务名 from the 模块名 module service container.

{service:~}

Resolve a service based on the current target member type.

{service:*}

Resolve all services by current target member type.

{service:~@}、{service:*@}

Forces resolution by target member type from the application's default service container.

{service:~@模块名}、{service:*@模块名}

Resolves by target member type from the specified module service container.

The "module name" and "service name" in the above table are syntax placeholders, not registered services. Discussions.plugin actually uses static and path expressions to mount modules, accessors and filters. You can read it with reference to Builtins and Services.

All formats can also continue to access properties or fields after the service object, such as {service:服务名.属性名@模块名}. This is suitable for referencing some configuration object or sub-object of a registered service in the plugin builtin property.

When @模块名 is not explicitly written, the service resolver will try to use the current builtin parent node name to match the module name; only if the match fails, the application default service container will be used. If the plugin path naming can be consistent with the module name, the service expression will be more natural.

Plugin Host Build Order

The application builder in Zongsoft.Plugins.Hosting orchestrates service registration and plugin loading. When using Application.Daemon(...) and Application.Terminal(...) as the entry points, the construction process is roughly as follows:

  1. Create a .NET Host builder and set the container factory to Zongsoft.Services.ServiceProviderFactory.

  2. Load host .option configuration by application name.

  3. Create PluginOptions and add the plugin configuration source to the application configuration.

  4. Call PluginTree.Get(options).Load() to load the plugin tree.

  5. Scan the host reference assembly, host assembly and plugin manifest assembly, and perform service registration.

  6. Register the default System.Net.Http.HttpClient service.

  7. Register the builtin under /Workspace/Environment/Services as a singleton service.

  8. Build the Host and initialize PluginApplicationContext through Initialize().

  9. When the application starts, open the workbench and load the worker under /Workbench/Startup.

Daemon and Terminal will first register their respective application context implementations and then map to PluginApplicationContext and IApplicationContext. This means that application code typically only depends on IApplicationContext, while the plugin host still has access to the plugin tree and workbench using a more specific plugin context.

Usage Suggestions

  • Services that are host-specific, lifecycle-sensitive, and require immediate configuration are registered with code.

  • Common business services in plugin assembly are registered with ServiceAttribute or IServiceRegistration.

  • Objects that need to be declared in the plugin manifest and can be assembled by configuration or path expressions are placed in /Workspace/Environment/Services.

  • Objects that need to be discovered by other plugins according to the extension point are first hung in the agreed plugin tree path instead of being forcibly put into the DI container.

  • When there are multiple implementations of the same contract, use Find<T>(argument), IMatchable, IMatcher<T> or tag organization. Do not hard-code the implementation selection logic on the caller.

  • The internal services of the module should be marked with ApplicationModuleAttribute as much as possible so that the module container can prioritize the implementation of this module.

Sub Namespace

namespace
Description

Zongsoft.Services.Distributing

distributed lock, lock token, and distributed collaboration base types.

For information on parsing by contract, matching by name, provider positioning, and shared instance ownership, see Service Resolution and Ownership. For the language and concurrency differences of expression implementation, see Scripts and Expressions.

Last updated