Use source-generated local proxy in ServiceJsonRpcDescriptor.Construc…#514
Draft
olegtk wants to merge 2 commits into
Draft
Use source-generated local proxy in ServiceJsonRpcDescriptor.Construc…#514olegtk wants to merge 2 commits into
olegtk wants to merge 2 commits into
Conversation
…tLocalProxy ServiceJsonRpcDescriptor.ConstructLocalProxy unconditionally went through LocalProxyGeneration, which emits a dynamic 'localRpcProxies_*' assembly via Reflection.Emit and JIT's a fresh proxy method per RPC method. The vs-servicehub source generator has been emitting [LocalProxyMappingAttribute]-annotated ProxyBase subclasses for every [JsonRpcContract] interface for a while, but only ServiceJsonRpcPolyTypeDescriptor consulted them; the legacy descriptor never did, so contracts that opted into [JsonRpcContract] (e.g. RpcContracts.* in DevDiv/VS) still paid the JIT cost on hot startup paths. Make the legacy descriptor try ProxyBase.TryCreateProxy first, falling back to LocalProxyGeneration when no compatible source-generated proxy is registered or when the target lacks one of the additional service interfaces. The fallback path preserves the existing ServiceCompositionException-wrapped failure mode. Also fix two pre-existing correctness gaps in ProxyBase that this change exposed via the legacy descriptor's test suite: (1) IJsonRpcLocalProxy.ConstructLocalProxy<T>() now returns null when the wrapped target doesn't implement T (matching the IL-emit proxy contract) instead of throwing InvalidCastException; (2) the INotifyDisposable.Disposed event uses an Interlocked sentinel pattern so that handlers added after Dispose() fire immediately and are released for GC.
There was a problem hiding this comment.
Pull request overview
This pull request updates the legacy ServiceJsonRpcDescriptor local-proxy construction path to prefer existing source-generated ProxyBase proxies (registered via LocalProxyMappingAttribute on [JsonRpcContract] interfaces), falling back to the existing Reflection.Emit-based proxy generation when source-gen isn’t applicable.
Changes:
- Updated
ServiceJsonRpcDescriptor.ConstructLocalProxy<T>to tryProxyBase.TryCreateProxyfirst (with compatibility checks for any additional service interfaces), then fall back toLocalProxyGeneration. - Fixed
ProxyBase.IJsonRpcLocalProxy.ConstructLocalProxy<T>()to returnnull(instead of throwing) when the wrapped target doesn’t implementT. - Improved
ProxyBasedisposal notification semantics by using an interlocked sentinel-backed handler store so handlers added after disposal fire immediately and aren’t retained.
Show a summary per file
| File | Description |
|---|---|
test/Microsoft.ServiceHub.Framework.Tests/ServiceJsonRpcDescriptor_ProxyTests.cs |
Adds tests validating source-generated proxy selection and dynamic-proxy fallback behavior. |
src/Microsoft.ServiceHub.Framework/ServiceJsonRpcDescriptor.cs |
Prefers source-generated local proxies when available, preserving fallback behavior to dynamic proxy generation. |
src/Microsoft.ServiceHub.Framework/Reflection/ProxyBase.cs |
Aligns ConstructLocalProxy<T>() with documented null-return semantics and strengthens Disposed event handler lifetime/ordering behavior. |
Copilot's findings
- Files reviewed: 3/3 changed files
- Comments generated: 0
AArnott
requested changes
Jun 15, 2026
AArnott
left a comment
Member
There was a problem hiding this comment.
This looks good, but it exposes a problem in that source generatored proxies may implement too many interfaces, leading to casts passing that ought to fail.
We have a similar issue in StreamJsonRpc which we've designed to solve, but that solution isn't present in this repo. I'm using Copilot to add to your PR to address this.
AArnott
approved these changes
Jun 15, 2026
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.
Startup.SingleFile perf test indicates 20 methods are being jitted in localRpcProxies_8869a195-770f-62ef-3da5-47c5c92f39b3, which are RPC contract interfaces:

They do have source generator setup ([JsonRpcContract]), but still cause jitting because ServiceJsonRpcDescriptor.ConstructLocalProxy ignores it.
ServiceJsonRpcDescriptor.ConstructLocalProxy unconditionally went through LocalProxyGeneration, which emits a dynamic 'localRpcProxies_' assembly via Reflection.Emit and JIT's a fresh proxy method per RPC method. The vs-servicehub source generator has been emitting [LocalProxyMappingAttribute]-annotated ProxyBase subclasses for every [JsonRpcContract] interface for a while, but only ServiceJsonRpcPolyTypeDescriptor consulted them; the legacy descriptor never did, so contracts that opted into [JsonRpcContract] (e.g. RpcContracts. in DevDiv/VS) still paid the JIT cost on hot startup paths.
Make the legacy descriptor try ProxyBase.TryCreateProxy first, falling back to LocalProxyGeneration when no compatible source-generated proxy is registered or when the target lacks one of the additional service interfaces. The fallback path preserves the existing ServiceCompositionException-wrapped failure mode.
The Disposed (and the ConstructLocalProxy() null-return) changes only exist because, once you route the legacy descriptor through ProxyBase, the existing ServiceJsonRpcDescriptor_ProxyTests test class fails on three tests that exercise behavior ProxyBase didn't implement correctly:
INotifyDisposable_HandlerAddedAfterDisposal — handler added after Dispose() must fire immediately
INotifyDisposable_DisposalReleasesRef — handlers must be released for GC after Dispose()
TargetImplementsIJsonRpcLocalProxy — ConstructLocalProxy() returns null when target lacks T
Also fix two pre-existing correctness gaps in ProxyBase that this change exposed via the legacy descriptor's test suite: (1) IJsonRpcLocalProxy.ConstructLocalProxy() now returns null when the wrapped target doesn't implement T (matching the IL-emit proxy contract) instead of throwing InvalidCastException; (2) the INotifyDisposable.Disposed event uses an Interlocked sentinel pattern so that handlers added after Dispose() fire immediately and are released for GC.