Container lifetimes for JSON-RPC services (AUS-981) - #152
Merged
Merged
Conversation
ServiceBinder.BindService(sessionId, serviceType, resolve) binds a type whose receiver is resolved per call from the RPC context, after the arguments are read and before the method runs. The core keeps no container dependency. AddJsonRpcService<T>(ServiceLifetime, sessionId) resolves scoped and transient services from HttpContext.RequestServices on HTTP and from a scope the raw connection handler opens per document, published as IServiceProvidersFeature and disposed once the document is answered; a batch shares one scope. JsonRpcOptions.ServiceProviderSelector locates the provider from a custom context. Lifetime conflicts with the container, a non-singleton JsonRpcService subclass and a ContextFactory without a selector are refused at registration or startup. LifetimeBenchmarks measures the opt-in cost.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the five dependency-injection decisions (Linear AUS-981): opt-in scoped and transient services, with the singleton default and its constant-target invoker untouched.
What changes
Core seam (D1, D3).
ServiceBinder.BindService(sessionId, serviceType, resolve)binds a type instead of an instance. Discovery reflects over the type; for each instance method the compiled invoker reads the arguments, then calls the resolver once with the RPC context (Handler.RpcContext()) and invokes the method on what it returns. Static methods keep a null receiver. A request the serializer refuses (-32602) never resolves anything. A null or foreign result is anInvalidOperationExceptionnaming the service type, answered as-32603and visible to the error handler. The core still references no container; the resolver is a plain delegate, so a hand-written factory, Autofac or anything else fits.BindInterfacestays instance-bound.AspNetCore (D1, D2, D3, D5).
AddJsonRpcService<T>(ServiceLifetime, sessionId)and the matchingAddJsonRpcServicesFromAssemblyoverload. Singletons are resolved at startup as before. Scoped and transient services resolve nothing at startup; each call resolves from the request's provider:HttpContext.RequestServices.IServiceScopeFactory, published asIServiceProvidersFeatureon the connection and removed and disposed (asynchronously on the async path) once the document is answered, before the flush. Never connection-long. With singletons only, nothing changes on the connection.JsonRpcOptions.ServiceProviderSelector(Func<object, IServiceProvider>) locates the provider from a custom context; selectors from per-endpointMapJsonRpcoptions are tried before the global one, the built-inHttpContext/raw-feature selection last, and the root provider is never a fallback.Validation: a lifetime that disagrees with a descriptor already in the collection throws from
AddJsonRpcService; a conflicting registration added later fails at startup (the binder compares against the collection the host was built from). A non-singletonJsonRpcServicesubclass is refused at registration. AContextFactorywithout a selector, with a non-singleton service registered, fails at startup for the global options and atMapJsonRpcfor per-endpoint options. No startup probe; the docs point atValidateOnBuild/ValidateScopes.No parameter injection (D4). Documented: dependencies go in the constructor of a scoped service; a singleton uses
IDbContextFactory<T>or capturesRequestServicesbefore its first await.Docs. README (Classes, Kestrel HTTP endpoint), the AspNetCore README ("Services and lifetime" rewritten,
ServiceProviderSelectorin the options table), the micro-benchmark README and the changelog.Cost
LifetimeBenchmarks(--job short, on a busy machine so compare within the run):The resolver indirection itself costs nothing measurable and allocates nothing; the scoped rows pay for the container's scope (128 B) and the service instance. The existing numeric rows are untouched.
Tests
1184 tests pass on net8.0 and net10.0 (28 new). Over HTTP and raw TCP, synchronous and
EnableAsyncMethods: a new scope per request/document, the batch sharing one scope with transients per call, disposal before the response is flushed, the feature present only during a document and absent when only singletons are registered,ContextFactorywith a global and a per-endpoint selector, a selector finding nothing (-32603, handler sees the exception, static methods still work), and every startup refusal. The core seam alone: the resolver receives the context on all three serializers, runs once per call and never for static methods, unknown methods or binding errors, derived types, async methods resolving on the invoking thread before the first await, and the argument checks.benchmarks/charts/render.py --checkandsite/build.pypass.