diff --git a/Directory.Packages.props b/Directory.Packages.props index e27e8283f..c30db430f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -30,6 +30,7 @@ + diff --git a/docfx/analyzers/StreamJsonRpc0008.md b/docfx/analyzers/StreamJsonRpc0008.md index f839a2ab5..a9cbb125c 100644 --- a/docfx/analyzers/StreamJsonRpc0008.md +++ b/docfx/analyzers/StreamJsonRpc0008.md @@ -3,7 +3,7 @@ RPC interfaces attributed with or should also be attributed for PolyType method shape generation. This ensures the interface can be used for RPC target objects in NativeAOT environments and with formatters prepared for those environments such as . -This diagnostic may be disabled when running in a trimmed application is not in scope and not using a formatter that requires it, such as the . +This diagnostic may be disabled when running in a trimmed application is not in scope and not using a formatter that requires it, as would. ## Example violation diff --git a/docfx/analyzers/StreamJsonRpc0009.md b/docfx/analyzers/StreamJsonRpc0009.md new file mode 100644 index 000000000..ecb325a10 --- /dev/null +++ b/docfx/analyzers/StreamJsonRpc0009.md @@ -0,0 +1,18 @@ +# StreamJsonRpc0009: Use GenerateShapeAttribute on optional marshalable interface + +RPC interfaces attributed with where is `true` should also be attributed for PolyType method shape generation using . +This ensures the interface can be used for RPC target objects in NativeAOT environments and with formatters prepared for those environments such as . + +This diagnostic may be disabled when running in a trimmed application is not in scope and not using a formatter that requires it, as would. + +## Example violation + +The following interface serves as an optional RPC marshalable interface but uses instead of : + +[!code-csharp[](../../samples/Analyzers/StreamJsonRpc0009.cs#Violation)] + +## Resolution + +Switch to : + +[!code-csharp[](../../samples/Analyzers/StreamJsonRpc0009.cs#Fix)] diff --git a/docfx/analyzers/index.md b/docfx/analyzers/index.md index e9914ffed..06e96b566 100644 --- a/docfx/analyzers/index.md +++ b/docfx/analyzers/index.md @@ -12,8 +12,9 @@ Some of these diagnostics will include a suggested code fix that can apply the c | [StreamJsonRpc0004](StreamJsonRpc0004.md) | Usage | Warning | Use interfaces for proxies | | [StreamJsonRpc0005](StreamJsonRpc0005.md) | Usage | Error | RpcMarshalable interfaces must be IDisposable | | [StreamJsonRpc0006](StreamJsonRpc0006.md) | Usage | Error | All interfaces in a proxy group must be attributed | -| [StreamJsonRpc0007](StreamJsonRpc0007.md) | Usage | Error | Use RpcMarshalableAttribute on optional marshalable interface | +| [StreamJsonRpc0007](StreamJsonRpc0007.md) | Usage | Error | Use RpcMarshalableAttribute on optional marshalable interface | | [StreamJsonRpc0008](StreamJsonRpc0008.md) | Usage | Warning | Add methods to PolyType shape for RPC contract interface | +| [StreamJsonRpc0009](StreamJsonRpc0009.md) | Usage | Warning | Use GenerateShapeAttribute on optional marshalable interface | | [StreamJsonRpc0011](StreamJsonRpc0011.md) | Usage | Error | RPC methods use supported return types | | [StreamJsonRpc0012](StreamJsonRpc0012.md) | Usage | Error | Unsupported member | | [StreamJsonRpc0013](StreamJsonRpc0013.md) | Usage | Error | No generic methods | diff --git a/docfx/analyzers/toc.yml b/docfx/analyzers/toc.yml index 56ffba886..ac38962cc 100644 --- a/docfx/analyzers/toc.yml +++ b/docfx/analyzers/toc.yml @@ -8,6 +8,7 @@ items: - href: StreamJsonRpc0006.md - href: StreamJsonRpc0007.md - href: StreamJsonRpc0008.md +- href: StreamJsonRpc0009.md - href: StreamJsonRpc0011.md - href: StreamJsonRpc0012.md - href: StreamJsonRpc0013.md diff --git a/samples/Analyzers/StreamJsonRpc0007.cs b/samples/Analyzers/StreamJsonRpc0007.cs index 341ee65e7..95c870ad6 100644 --- a/samples/Analyzers/StreamJsonRpc0007.cs +++ b/samples/Analyzers/StreamJsonRpc0007.cs @@ -24,7 +24,7 @@ partial interface IMyObject : IDisposable { } - [RpcMarshalable(IsOptional = true), TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [RpcMarshalable(IsOptional = true), GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMyObject2 : IDisposable { } diff --git a/samples/Analyzers/StreamJsonRpc0009.cs b/samples/Analyzers/StreamJsonRpc0009.cs new file mode 100644 index 000000000..ba3d81a69 --- /dev/null +++ b/samples/Analyzers/StreamJsonRpc0009.cs @@ -0,0 +1,21 @@ +namespace StreamJsonRpc0009.Violation +{ +#pragma warning disable StreamJsonRpc0009 + #region Violation + [RpcMarshalable(IsOptional = true), TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + partial interface IMyObject : IDisposable + { + } + #endregion +#pragma warning restore StreamJsonRpc0009 +} + +namespace StreamJsonRpc0009.Fix +{ + #region Fix + [RpcMarshalable(IsOptional = true), GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + partial interface IMyObject : IDisposable + { + } + #endregion +} diff --git a/samples/Analyzers/StreamJsonRpc0050.cs b/samples/Analyzers/StreamJsonRpc0050.cs index 9c440a014..eebb8ec8f 100644 --- a/samples/Analyzers/StreamJsonRpc0050.cs +++ b/samples/Analyzers/StreamJsonRpc0050.cs @@ -9,7 +9,7 @@ partial interface IMyObject : IDisposable { } - [TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] [RpcMarshalable(IsOptional = true)] partial interface IMyObject2 : IDisposable { @@ -35,7 +35,7 @@ partial interface IMyObject : IDisposable { } - [TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] [RpcMarshalable(IsOptional = true)] partial interface IMyObject2 : IDisposable { diff --git a/samples/NativeAOT/SystemTextJson.cs b/samples/NativeAOT/SystemTextJson.cs index a61758145..8c88aa1fa 100644 --- a/samples/NativeAOT/SystemTextJson.cs +++ b/samples/NativeAOT/SystemTextJson.cs @@ -13,8 +13,7 @@ static async Task Main(string[] args) JsonRpc serverRpc = new(new HeaderDelimitedMessageHandler(serverPipe, CreateFormatter())); JsonRpc clientRpc = new(new HeaderDelimitedMessageHandler(clientPipe, CreateFormatter())); - RpcTargetMetadata.RegisterEventArgs(); - var targetMetadata = RpcTargetMetadata.FromInterface(new RpcTargetMetadata.InterfaceCollection(typeof(IServer))); + var targetMetadata = RpcTargetMetadata.FromShape(); serverRpc.AddLocalRpcTarget(targetMetadata, new Server(), null); serverRpc.StartListening(); diff --git a/src/StreamJsonRpc.Analyzers/AnalyzerReleases.Unshipped.md b/src/StreamJsonRpc.Analyzers/AnalyzerReleases.Unshipped.md index 86b52a344..4bddd2a2a 100644 --- a/src/StreamJsonRpc.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/StreamJsonRpc.Analyzers/AnalyzerReleases.Unshipped.md @@ -13,6 +13,7 @@ StreamJsonRpc0005 | Usage | Error | RpcMarshalable interfaces must be IDisposabl StreamJsonRpc0006 | Usage | Error | All interfaces in a proxy group must be attributed StreamJsonRpc0007 | Usage | Error | Use RpcMarshalableAttribute on optional marshalable interface StreamJsonRpc0008 | Usage | Warning | Add methods to PolyType shape for RPC contract interface +StreamJsonRpc0009 | Usage | Warning | Use GenerateShapeAttribute on optional marshalable interface StreamJsonRpc0011 | Usage | Error | Unsupported RPC method return type StreamJsonRpc0012 | Usage | Error | RPC contracts may not include this type of member StreamJsonRpc0013 | Usage | Error | RPC contracts may not include generic methods diff --git a/src/StreamJsonRpc.Analyzers/JsonRpcContractAnalyzer.cs b/src/StreamJsonRpc.Analyzers/JsonRpcContractAnalyzer.cs index 5672a5053..b95f951e7 100644 --- a/src/StreamJsonRpc.Analyzers/JsonRpcContractAnalyzer.cs +++ b/src/StreamJsonRpc.Analyzers/JsonRpcContractAnalyzer.cs @@ -42,6 +42,11 @@ public class JsonRpcContractAnalyzer : DiagnosticAnalyzer /// public const string GeneratePolyTypeMethodsOnRpcContractInterfaceId = "StreamJsonRpc0008"; + /// + /// Diagnostic ID for StreamJsonRpc0009: Use GenerateShapeAttribute on optional marshalable interface. + /// + public const string UseGenerateShapeOnOptionalMarshalableInterfaceId = "StreamJsonRpc0009"; + /// /// Diagnostic ID for StreamJsonRpc0011: RPC methods use supported return types. /// @@ -132,6 +137,18 @@ public class JsonRpcContractAnalyzer : DiagnosticAnalyzer isEnabledByDefault: true, helpLinkUri: AnalyzerUtilities.GetHelpLink(GeneratePolyTypeMethodsOnRpcContractInterfaceId)); + /// + /// Diagnostic for StreamJsonRpc0009: Use GenerateShapeAttribute on optional marshalable interface. + /// + public static readonly DiagnosticDescriptor UseGenerateShapeOnOptionalMarshalableInterface = new( + id: UseGenerateShapeOnOptionalMarshalableInterfaceId, + title: Strings.StreamJsonRpc0009_Title, + messageFormat: Strings.StreamJsonRpc0009_MessageFormat, + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + helpLinkUri: AnalyzerUtilities.GetHelpLink(UseGenerateShapeOnOptionalMarshalableInterfaceId)); + /// /// Diagnostic for StreamJsonRpc0011: RPC methods use supported return types. /// @@ -213,6 +230,7 @@ public class JsonRpcContractAnalyzer : DiagnosticAnalyzer RpcMarshableDisposable, UseRpcMarshalableAttributeOnOptionalInterfaces, GeneratePolyTypeMethodsOnRpcContractInterface, + UseGenerateShapeOnOptionalMarshalableInterface, UnsupportedReturnType, UnsupportedMemberType, NoGenericMethods, @@ -294,6 +312,7 @@ private void InspectSymbol(SymbolStartAnalysisContext context, KnownSymbols know } bool isRpcMarshalable = SymbolEqualityComparer.Default.Equals(rpcContractAttribute.AttributeClass, knownSymbols.RpcMarshalableAttribute); + bool isRpcMarshalableOptional = isRpcMarshalable && rpcContractAttribute.NamedArguments.FirstOrDefault(a => a.Key == Types.RpcMarshalableAttribute.IsOptional).Value.Value is true; bool isCallScopedLifetime = rpcContractAttribute.NamedArguments.FirstOrDefault(a => a.Key == Types.RpcMarshalableAttribute.CallScopedLifetime).Value.Value is true; ImmutableList diagnostics = []; Location typeLocation = namedType.Locations.FirstOrDefault() ?? Location.None; @@ -303,16 +322,26 @@ private void InspectSymbol(SymbolStartAnalysisContext context, KnownSymbols know // GenerateShapeAttribute is ineffective on open generic types, so ignore it in that case. AttributeData? generateShapeAttribute = hasGenericTypeParameters ? null : namedType.GetAttributes().FirstOrDefault(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, knownSymbols.GenerateShapeAttribute)); AttributeData? typeShapeAttribute = namedType.GetAttributes().FirstOrDefault(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, knownSymbols.TypeShapeAttribute)); + bool preferGenerateShape = generateShapeAttribute is not null || !isRpcMarshalable || isRpcMarshalableOptional; + string proposedShapeFix = preferGenerateShape ? "[GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)]" : "[TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)]"; if (!this.IncludesPublicMethods(typeShapeAttribute) && !this.IncludesPublicMethods(generateShapeAttribute)) { - bool preferGenerateShape = generateShapeAttribute is not null || !isRpcMarshalable; diagnostics = diagnostics.Add(Diagnostic.Create( GeneratePolyTypeMethodsOnRpcContractInterface, typeLocation, [(generateShapeAttribute ?? typeShapeAttribute)?.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken)?.GetLocation() ?? Location.None], ImmutableDictionary.Empty.Add("PreferGenerateShape", preferGenerateShape ? "true" : "false"), namedType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), - preferGenerateShape ? "[GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)]" : "[TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)]")); + proposedShapeFix)); + } + else if (generateShapeAttribute is null && isRpcMarshalableOptional) + { + diagnostics = diagnostics.Add(Diagnostic.Create( + UseGenerateShapeOnOptionalMarshalableInterface, + typeShapeAttribute?.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken)?.GetLocation() ?? typeLocation ?? Location.None, + ImmutableDictionary.Empty.Add("PreferGenerateShape", preferGenerateShape ? "true" : "false"), + namedType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), + proposedShapeFix)); } AttributeData[] optionalIfaceAttrs = [.. namedType.GetAttributes().Where(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, knownSymbols.RpcMarshalableOptionalInterface))]; diff --git a/src/StreamJsonRpc.Analyzers/Strings.resx b/src/StreamJsonRpc.Analyzers/Strings.resx index 740d1f801..6f52df9cb 100644 --- a/src/StreamJsonRpc.Analyzers/Strings.resx +++ b/src/StreamJsonRpc.Analyzers/Strings.resx @@ -1,17 +1,17 @@  - @@ -201,6 +201,12 @@ The RPC contract type '{0}' should also have the {1} applied so that it works with more formatters. + + Use GenerateShapeAttribute on optional marshalable interface + + + The type '{0}' should use {1} since it is an optional RPC marshalable contract type. + JsonRpcProxyAttribute<T> should be applied only to generic interfaces @@ -225,4 +231,4 @@ Use IClientProxy.Is, JsonRpcExtensions.As or the source-generated extension methods by the same names on your RPC marshalable interfaces to cast or type check between RpcMarshalable interfaces {0} and {1}. - \ No newline at end of file + diff --git a/src/StreamJsonRpc/FormatterBase.cs b/src/StreamJsonRpc/FormatterBase.cs index 53194da4f..ae7367c78 100644 --- a/src/StreamJsonRpc/FormatterBase.cs +++ b/src/StreamJsonRpc/FormatterBase.cs @@ -19,8 +19,6 @@ namespace StreamJsonRpc; /// public abstract class FormatterBase : IJsonRpcFormatterState, IJsonRpcInstanceContainer, IDisposable { - private readonly ProxyFactory proxyFactory; - private JsonRpc? rpc; /// @@ -57,17 +55,10 @@ public abstract class FormatterBase : IJsonRpcFormatterState, IJsonRpcInstanceCo /// /// Initializes a new instance of the class. /// - [RequiresDynamicCode(RuntimeReasons.RefEmit), RequiresUnreferencedCode(RuntimeReasons.RefEmit)] public FormatterBase() - : this(ProxyFactory.Default) { } - private protected FormatterBase(ProxyFactory proxyFactory) - { - this.proxyFactory = proxyFactory; - } - /// /// An interface implemented by all the -derived nested types (, , ) to allow them to carry arbitrary top-level properties on behalf of the application. /// @@ -99,7 +90,7 @@ JsonRpc IJsonRpcInstanceContainer.Rpc this.rpc = value; this.formatterProgressTracker = new MessageFormatterProgressTracker(value, this); - this.rpcMarshaledContextTracker = new MessageFormatterRpcMarshaledContextTracker(value, this.proxyFactory, this); + this.rpcMarshaledContextTracker = this.CreateMessageFormatterRpcMarshaledContextTracker(value); this.enumerableTracker = new MessageFormatterEnumerableTracker(value, this, this.rpcMarshaledContextTracker); this.duplexPipeTracker = new MessageFormatterDuplexPipeTracker(value, this) { MultiplexingStream = this.MultiplexingStream }; } @@ -219,6 +210,8 @@ protected virtual void Dispose(bool disposing) /// A value to dispose of when serialization has completed. protected SerializationTracking TrackSerialization(JsonRpcMessage message) => new(this, message); + private protected abstract MessageFormatterRpcMarshaledContextTracker CreateMessageFormatterRpcMarshaledContextTracker(JsonRpc rpc); + private protected void TryHandleSpecialIncomingMessage(JsonRpcMessage message) { switch (message) diff --git a/src/StreamJsonRpc/JsonMessageFormatter.cs b/src/StreamJsonRpc/JsonMessageFormatter.cs index 78a8e5c40..1d4d153e2 100644 --- a/src/StreamJsonRpc/JsonMessageFormatter.cs +++ b/src/StreamJsonRpc/JsonMessageFormatter.cs @@ -34,6 +34,8 @@ public class JsonMessageFormatter : FormatterBase, IJsonRpcAsyncMessageTextForma /// internal const string ExceptionDataKey = "JToken"; + private static readonly ProxyFactory ProxyFactory = ProxyFactory.Default; + /// /// JSON parse settings. /// @@ -366,6 +368,9 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } + /// + private protected override MessageFormatterRpcMarshaledContextTracker CreateMessageFormatterRpcMarshaledContextTracker(JsonRpc rpc) => new MessageFormatterRpcMarshaledContextTracker.Dynamic(rpc, ProxyFactory, this); + private static IReadOnlyDictionary PartiallyParseNamedArguments(JObject args) { Requires.NotNull(args, nameof(args)); diff --git a/src/StreamJsonRpc/JsonRpc.cs b/src/StreamJsonRpc/JsonRpc.cs index f43781545..389561e26 100644 --- a/src/StreamJsonRpc/JsonRpc.cs +++ b/src/StreamJsonRpc/JsonRpc.cs @@ -1456,8 +1456,6 @@ internal void AddLocalRpcMethod(MethodInfo handler, object? target, JsonRpcMetho JsonRpcTargetOptions? options, bool requestRevertOption) { - RpcTargetMetadata.EnableDynamicEventHandlerCreation(); - options ??= JsonRpcTargetOptions.Default; RpcTargetMetadata mapping = exposingMembersOn.IsInterface ? RpcTargetMetadata.FromInterface(exposingMembersOn) : diff --git a/src/StreamJsonRpc/MessagePackFormatter.cs b/src/StreamJsonRpc/MessagePackFormatter.cs index dc101e142..e722b08ce 100644 --- a/src/StreamJsonRpc/MessagePackFormatter.cs +++ b/src/StreamJsonRpc/MessagePackFormatter.cs @@ -30,6 +30,8 @@ namespace StreamJsonRpc; [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] public class MessagePackFormatter : FormatterBase, IJsonRpcMessageFormatter, IJsonRpcFormatterTracingCallbacks, IJsonRpcMessageFactory { + private static readonly ProxyFactory ProxyFactory = ProxyFactory.Default; + /// /// The constant "jsonrpc", in its various forms. /// @@ -248,6 +250,9 @@ void IJsonRpcFormatterTracingCallbacks.OnSerializationComplete(JsonRpcMessage me } } + /// + private protected override MessageFormatterRpcMarshaledContextTracker CreateMessageFormatterRpcMarshaledContextTracker(JsonRpc rpc) => new MessageFormatterRpcMarshaledContextTracker.Dynamic(rpc, ProxyFactory, this); + private static ReadOnlySequence GetSliceForNextToken(ref MessagePackReader reader) { SequencePosition startingPosition = reader.Position; diff --git a/src/StreamJsonRpc/NerdbankMessagePackFormatter.cs b/src/StreamJsonRpc/NerdbankMessagePackFormatter.cs index a147896f7..497e0a97d 100644 --- a/src/StreamJsonRpc/NerdbankMessagePackFormatter.cs +++ b/src/StreamJsonRpc/NerdbankMessagePackFormatter.cs @@ -72,6 +72,8 @@ public partial class NerdbankMessagePackFormatter : FormatterBase, IJsonRpcMessa ], }.WithObjectConverter(); + private static readonly ProxyFactory ProxyFactory = ProxyFactory.NoDynamic; + private static readonly JsonRpcProxyOptions DefaultRpcMarshalableProxyOptions = new JsonRpcProxyOptions(JsonRpcProxyOptions.Default) { AcceptProxyWithExtraInterfaces = true, IsFrozen = true }; /// @@ -101,7 +103,6 @@ public partial class NerdbankMessagePackFormatter : FormatterBase, IJsonRpcMessa /// Initializes a new instance of the class. /// public NerdbankMessagePackFormatter() - : base(ProxyFactory.NoDynamic) { // Set up initial options for our own message types. this.envelopeSerializer = DefaultSerializer with @@ -212,6 +213,8 @@ void IJsonRpcFormatterTracingCallbacks.OnSerializationComplete(JsonRpcMessage me } } + private protected override MessageFormatterRpcMarshaledContextTracker CreateMessageFormatterRpcMarshaledContextTracker(JsonRpc rpc) => new MessageFormatterRpcMarshaledContextTracker.PolyTypeShape(rpc, ProxyFactory, this, this.TypeShapeProvider); + private static MessagePackConverter GetRpcMarshalableConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] T>(ITypeShape shape) where T : class { diff --git a/src/StreamJsonRpc/ProxyGeneration.cs b/src/StreamJsonRpc/ProxyGeneration.cs index 1a0cdb255..e09fc21a9 100644 --- a/src/StreamJsonRpc/ProxyGeneration.cs +++ b/src/StreamJsonRpc/ProxyGeneration.cs @@ -70,10 +70,6 @@ internal static TypeInfo Get(ProxyInputs inputs) ReadOnlySpan additionalContractInterfaces = inputs.AdditionalContractInterfaces.Span; ReadOnlySpan<(Type Type, int Code)> implementedOptionalInterfaces = inputs.ImplementedOptionalInterfaces.Span; - // Dynamic proxy generation requires the ability to generate dynamic event handlers. - // Not a problem, since by calling into this method the user has already committed to running on a runtime that supports dynamic code. - RpcTargetMetadata.EnableDynamicEventHandlerCreation(); - VerifySupported(contractInterface.IsInterface, Resources.ClientProxyTypeArgumentMustBeAnInterface, contractInterface); foreach (TypeInfo additionalContract in additionalContractInterfaces) { diff --git a/src/StreamJsonRpc/Reflection/MessageFormatterRpcMarshaledContextTracker.cs b/src/StreamJsonRpc/Reflection/MessageFormatterRpcMarshaledContextTracker.cs index c3607823a..b4a82b421 100644 --- a/src/StreamJsonRpc/Reflection/MessageFormatterRpcMarshaledContextTracker.cs +++ b/src/StreamJsonRpc/Reflection/MessageFormatterRpcMarshaledContextTracker.cs @@ -19,7 +19,7 @@ namespace StreamJsonRpc.Reflection; /// /// Tracks objects that get marshaled using the general marshaling protocol. /// -internal partial class MessageFormatterRpcMarshaledContextTracker +internal abstract partial class MessageFormatterRpcMarshaledContextTracker { private static readonly IReadOnlyCollection<(Type ImplicitlyMarshaledType, JsonRpcProxyOptions ProxyOptions, JsonRpcTargetOptions TargetOptions, RpcMarshalableAttribute Attribute)> ImplicitlyMarshaledTypes = [ @@ -73,7 +73,7 @@ internal partial class MessageFormatterRpcMarshaledContextTracker /// private ImmutableDictionary> outboundRequestIdMarshalMap = ImmutableDictionary>.Empty; - internal MessageFormatterRpcMarshaledContextTracker(JsonRpc jsonRpc, ProxyFactory proxyFactory, IJsonRpcFormatterState formatterState) + protected MessageFormatterRpcMarshaledContextTracker(JsonRpc jsonRpc, ProxyFactory proxyFactory, IJsonRpcFormatterState formatterState) { this.jsonRpc = jsonRpc; this.proxyFactory = proxyFactory; @@ -276,7 +276,7 @@ internal MarshalToken GetToken( optionalInterfacesCodes.Add(attribute.OptionalInterfaceCode); this.jsonRpc.AddRpcInterfaceToTargetInternal( - RpcTargetMetadata.FromInterface(attribute.OptionalInterface), + this.GetRpcTargetMetadata(attribute.OptionalInterface), context.Proxy, new JsonRpcTargetOptions(context.JsonRpcTargetOptions) { @@ -421,6 +421,8 @@ internal MarshalToken GetToken( } } + protected abstract RpcTargetMetadata GetRpcTargetMetadata([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type interfaceType); + private static bool TryGetMarshalOptionsForTypeHelper( Type type, JsonRpcProxyOptions defaultProxyOptions, @@ -598,6 +600,17 @@ public MarshalToken(int marshaled, long handle, string? lifetime = null, int[]? public int[]? OptionalInterfacesCodes { get; set; } } + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] + internal class Dynamic(JsonRpc jsonRpc, ProxyFactory proxyFactory, IJsonRpcFormatterState formatterState) : MessageFormatterRpcMarshaledContextTracker(jsonRpc, proxyFactory, formatterState) + { + protected override RpcTargetMetadata GetRpcTargetMetadata([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type interfaceType) => RpcTargetMetadata.FromInterface(interfaceType); + } + + internal class PolyTypeShape(JsonRpc jsonRpc, ProxyFactory proxyFactory, IJsonRpcFormatterState formatterState, ITypeShapeProvider typeShapeProvider) : MessageFormatterRpcMarshaledContextTracker(jsonRpc, proxyFactory, formatterState) + { + protected override RpcTargetMetadata GetRpcTargetMetadata([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type interfaceType) => RpcTargetMetadata.FromShape(typeShapeProvider.GetTypeShapeOrThrow(interfaceType)); + } + /// /// Defines the values of the "lifetime" property in the . /// diff --git a/src/StreamJsonRpc/Reflection/RpcTargetInfo.cs b/src/StreamJsonRpc/Reflection/RpcTargetInfo.cs index 2b7f7c1ce..baf324a2d 100644 --- a/src/StreamJsonRpc/Reflection/RpcTargetInfo.cs +++ b/src/StreamJsonRpc/Reflection/RpcTargetInfo.cs @@ -408,7 +408,7 @@ private class EventReceiver : IDisposable { private readonly JsonRpc jsonRpc; private readonly object server; - private readonly EventInfo eventInfo; + private readonly Action removeEventHandler; private readonly Delegate registeredHandler; private readonly string rpcEventName; @@ -422,17 +422,17 @@ internal EventReceiver(JsonRpc jsonRpc, object server, RpcTargetMetadata.EventMe this.jsonRpc = jsonRpc; this.server = server; - this.eventInfo = eventMetadata.Event; + this.removeEventHandler = eventMetadata.RemoveEventHandler; this.rpcEventName = options.EventNameTransform is not null ? options.EventNameTransform(eventMetadata.Name) : eventMetadata.Name; this.registeredHandler = eventMetadata.CreateEventHandler(jsonRpc, this.rpcEventName); - eventMetadata.Event.AddEventHandler(server, this.registeredHandler); + eventMetadata.AddEventHandler(server, this.registeredHandler); } public void Dispose() { - this.eventInfo.RemoveEventHandler(this.server, this.registeredHandler); + this.removeEventHandler(this.server, this.registeredHandler); } } } diff --git a/src/StreamJsonRpc/RpcTargetMetadata.cs b/src/StreamJsonRpc/RpcTargetMetadata.cs index 8d3829577..58dbd61f8 100644 --- a/src/StreamJsonRpc/RpcTargetMetadata.cs +++ b/src/StreamJsonRpc/RpcTargetMetadata.cs @@ -25,8 +25,6 @@ public class RpcTargetMetadata private static readonly ConcurrentDictionary Interfaces = []; private static readonly ConcurrentDictionary PublicClass = []; private static readonly ConcurrentDictionary NonPublicClass = []; - private static readonly MethodInfo RegisterEventArgsMethodInfo = typeof(RpcTargetMetadata).GetMethod(nameof(RegisterEventArgs), BindingFlags.Public | BindingFlags.Static) ?? throw Assumes.NotReachable(); - private static Action? dynamicEventHandlerFactoryRegistration; /// /// Represents a method that creates a delegate to handle a specified JSON-RPC event. @@ -68,25 +66,6 @@ private interface IEventHandlerFactory /// public required Type TargetType { get; init; } - /// - /// Enables dynamic generation of event handlers for delegates - /// where TEventArgs is a value type. - /// - /// - /// This method is not safe to use in NativeAOT applications. - /// Such applications should either call directly for each value-type type argument, - /// or rely on source generation to do so. - /// - [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] - [UnconditionalSuppressMessage("Trimming", "IL2060", Justification = "The generic method we construct has no dynamic member access requirements.")] - public static void EnableDynamicEventHandlerCreation() - { - dynamicEventHandlerFactoryRegistration ??= (type) => - { - RegisterEventArgsMethodInfo.MakeGenericMethod(type).Invoke(null, null); - }; - } - /// /// Creates an instance of RpcTargetMetadata that describes the specified RPC contract interface. /// @@ -104,6 +83,7 @@ public static void EnableDynamicEventHandlerCreation() /// For a smaller trimmed application, use instead. /// /// + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] public static RpcTargetMetadata FromInterface([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type rpcContract) { Requires.NotNull(rpcContract); @@ -131,6 +111,7 @@ public static RpcTargetMetadata FromInterface([DynamicallyAccessedMembers(Dynami /// /// /// Thrown if does not represent all the interfaces that the target interface derives from. + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] public static RpcTargetMetadata FromInterface(InterfaceCollection interfaces) { Requires.NotNull(interfaces); @@ -149,6 +130,7 @@ public static RpcTargetMetadata FromInterface(InterfaceCollection interfaces) WalkInterface(interfaces[i]); } + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] void WalkInterface([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicEvents)] Type iface) { AddMethods(builder, iface.GetMethods(BindingFlags.Public | BindingFlags.Instance)); @@ -175,6 +157,7 @@ void WalkInterface([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Pu /// For a smaller trimmed application, use instead. /// /// + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] public static RpcTargetMetadata FromClass([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type classType) => FromClass(classType, ClassAndInterfaces.Create(classType)); @@ -195,6 +178,7 @@ public static RpcTargetMetadata FromClass([DynamicallyAccessedMembers(Dynamicall /// /// Thrown if the does not match the in the provided metadata. /// + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] public static RpcTargetMetadata FromClass([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicEvents)] Type classType, ClassAndInterfaces metadata) { Requires.NotNull(classType); @@ -236,6 +220,7 @@ public static RpcTargetMetadata FromClass([DynamicallyAccessedMembers(Dynamicall /// For a smaller trimmed application, use instead. /// /// + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] public static RpcTargetMetadata FromClassNonPublic([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type classType) => FromClassNonPublic(classType, ClassAndInterfaces.Create(classType)); @@ -256,6 +241,7 @@ public static RpcTargetMetadata FromClassNonPublic([DynamicallyAccessedMembers(D /// /// Thrown if the does not match the in the provided metadata. /// + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] public static RpcTargetMetadata FromClassNonPublic([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.NonPublicEvents)] Type classType, ClassAndInterfaces metadata) { Requires.NotNull(classType); @@ -321,20 +307,11 @@ public static RpcTargetMetadata FromShape(ITypeShape shape) Builder builder = new(shape); AddMethods(builder, shape.Methods); + AddEvents(builder, shape.Events); return builder.ToImmutable(); } - /// - /// Creates an event handler factory that supports for a given . - /// - /// - /// The type argument used in . - /// Only structs are supported because only value types need registration. Reference types work without registration. - /// - public static void RegisterEventArgs() - where TEventArgs : struct => EventHandlerFactories.TryAdd(typeof(TEventArgs), new EventHandlerFactory()); - private static void AddMethods(Builder builder, IReadOnlyList methods) { foreach (IMethodShape shape in methods) @@ -378,6 +355,7 @@ private static bool TryAddCandidateMethod(Builder builder, MethodInfo method, IM return true; } + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] private static void AddEvents(Builder builder, IEnumerable events) { foreach (EventInfo @event in events) @@ -386,6 +364,26 @@ private static void AddEvents(Builder builder, IEnumerable events) } } + private static void AddEvents(Builder builder, IReadOnlyList events) + { + foreach (IEventShape shape in events) + { + TryAddCandidateEvent(builder, shape); + } + } + + private static bool TryAddCandidateEvent(Builder builder, IEventShape shape) + { + if (shape.Accept(EventShapeVisitor.Instance, builder) is EventMetadata eventMetadata) + { + builder.Events.Add(eventMetadata); + return true; + } + + return false; + } + + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] private static bool TryAddCandidateEvent(Builder builder, EventInfo @event) { if (@event.EventHandlerType is null) @@ -415,10 +413,11 @@ private static bool TryAddCandidateEvent(Builder builder, EventInfo @event) builder.Events.Add(new EventMetadata { - Event = @event, Name = @event.Name, EventHandlerType = @event.EventHandlerType, CreateEventHandler = createEventHandler, + AddEventHandler = (target, handler) => @event.AddEventHandler(target, handler), + RemoveEventHandler = (target, handler) => @event.RemoveEventHandler(target, handler), }); return true; @@ -433,21 +432,10 @@ CreateEventHandlerDelegate CreateEventDelegate(Type argType) return Delegate.CreateDelegate(@event.EventHandlerType, d.Target, d.Method); }; } - else if (EventHandlerFactories.TryGetValue(argType, out IEventHandlerFactory? factory)) - { - return (jsonRpc, eventName) => factory.CreateEventHandler(jsonRpc, eventName, @event.EventHandlerType); - } else { - if (dynamicEventHandlerFactoryRegistration is not null) - { - dynamicEventHandlerFactoryRegistration(argType); - Assumes.True(EventHandlerFactories.TryGetValue(argType, out factory)); - return (jsonRpc, eventName) => factory.CreateEventHandler(jsonRpc, eventName, @event.EventHandlerType); - } - - // We don't have a factory registered for this value type. - throw new NotSupportedException($"{@event.DeclaringType}.{@event.Name} event uses {argType} as its second parameter. Structs used as event args must be registered beforehand using {nameof(RpcTargetMetadata)}.{nameof(RegisterEventArgs)}()."); + IEventHandlerFactory factory = EventHandlerFactories.GetOrAdd(argType, static t => (IEventHandlerFactory)Activator.CreateInstance(typeof(EventHandlerFactory<>).MakeGenericType(t))!); + return (jsonRpc, eventName) => factory.CreateEventHandler(jsonRpc, eventName, @event.EventHandlerType); } } @@ -697,11 +685,6 @@ internal static InterfaceCollection Create([DynamicallyAccessedMembers(Dynamical /// public class EventMetadata { - /// - /// Gets the event for which this metadata is describing the handler. - /// - public required EventInfo Event { get; init; } - /// /// Gets the name of the RPC method that this event will invoke when raised. /// @@ -716,6 +699,16 @@ public class EventMetadata /// Gets a factory method that creates a delegate to handle the event. /// public required CreateEventHandlerDelegate CreateEventHandler { get; init; } + + /// + /// Gets a function that will add an event handler for this event to a given target object. + /// + public required Action AddEventHandler { get; init; } + + /// + /// Gets a function that will remove an event handler for this event to a given target object. + /// + public required Action RemoveEventHandler { get; init; } } /// @@ -943,4 +936,65 @@ private ImmutableDictionary> Generat return aliasedMethods.ToImmutable(); } } + + private class EventShapeVisitor : TypeShapeVisitor + { + internal static readonly EventShapeVisitor Instance = new(); + + public override object? VisitEvent(IEventShape eventShape, object? state = null) + { + if (eventShape is not { IsStatic: false } || + eventShape.HandlerType.Accept(this) is not CreateEventHandlerDelegate createEventHandlerDelegate) + { + return null; + } + + Setter addHandler = eventShape.GetAddHandler(); + Setter removeHandler = eventShape.GetRemoveHandler(); + + return new EventMetadata + { + Name = eventShape.Name, + EventHandlerType = eventShape.HandlerType.Type, + CreateEventHandler = createEventHandlerDelegate, + AddEventHandler = (target, handler) => + { + TDeclaringType? typedTarget = (TDeclaringType?)target; + addHandler(ref typedTarget, (TEventHandler)(object)handler!); + }, + RemoveEventHandler = (target, handler) => + { + TDeclaringType? typedTarget = (TDeclaringType?)target; + removeHandler(ref typedTarget, (TEventHandler)(object)handler!); + }, + }; + } + + public override object? VisitFunction(IFunctionTypeShape functionShape, object? state = null) + { + if (functionShape is not { IsVoidLike: true, IsAsync: false, Parameters: [{ Name: "sender" }, { } parameterShape] }) + { + return null; + } + + Type[] argTypes = [parameterShape.ParameterType.Type]; + var argGetter = (Getter)parameterShape.Accept(this, state)!; + return new CreateEventHandlerDelegate((rpc, name) => + { + return (Delegate)(object)functionShape.FromDelegate((ref TArgumentState argState) => + { + object? arg = argGetter(ref argState); + rpc.NotifyAsync(name, [arg], argTypes).Forget(); + return default!; + })!; + }); + } + + public override object? VisitParameter(IParameterShape parameterShape, object? state = null) + { + // Return a delegate that boxes the argument as an object. + Getter argGetter = parameterShape.GetGetter(); + return new Getter((ref TArgumentState argState) => argGetter(ref argState)); + } + } } diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index 54c71b1ca..652d59742 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -27,6 +27,8 @@ namespace StreamJsonRpc; [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] public partial class SystemTextJsonFormatter : FormatterBase, IJsonRpcMessageFormatter, IJsonRpcMessageTextFormatter, IJsonRpcInstanceContainer, IJsonRpcMessageFactory, IJsonRpcFormatterTracingCallbacks { + private static readonly ProxyFactory ProxyFactory = ProxyFactory.Default; + private static readonly JsonWriterOptions WriterOptions = new() { }; private static readonly JsonDocumentOptions DocumentOptions = new() { }; @@ -345,6 +347,9 @@ void IJsonRpcFormatterTracingCallbacks.OnSerializationComplete(JsonRpcMessage me Protocol.JsonRpcResult IJsonRpcMessageFactory.CreateResultMessage() => new JsonRpcResult(this); + /// + private protected override MessageFormatterRpcMarshaledContextTracker CreateMessageFormatterRpcMarshaledContextTracker(JsonRpc rpc) => new MessageFormatterRpcMarshaledContextTracker.Dynamic(rpc, ProxyFactory, this); + private JsonSerializerOptions MassageUserDataSerializerOptions(JsonSerializerOptions options) { // This is required for $/cancelRequest messages. diff --git a/test/NativeAOTCompatibility.Test/NerdbankMessagePack.cs b/test/NativeAOTCompatibility.Test/NerdbankMessagePack.cs index ca863194b..909bbf614 100644 --- a/test/NativeAOTCompatibility.Test/NerdbankMessagePack.cs +++ b/test/NativeAOTCompatibility.Test/NerdbankMessagePack.cs @@ -15,8 +15,7 @@ internal static async Task RunAsync() JsonRpc serverRpc = new JsonRpc(new LengthHeaderMessageHandler(serverPipe, serverPipe, CreateFormatter())); JsonRpc clientRpc = new JsonRpc(new LengthHeaderMessageHandler(clientPipe, clientPipe, CreateFormatter())); - RpcTargetMetadata.RegisterEventArgs(); - var targetMetadata = RpcTargetMetadata.FromInterface(new RpcTargetMetadata.InterfaceCollection(typeof(IServer))); + var targetMetadata = RpcTargetMetadata.FromShape(); serverRpc.AddLocalRpcTarget(targetMetadata, new Server(), null); serverRpc.StartListening(); diff --git a/test/NativeAOTCompatibility.Test/SystemTextJson.cs b/test/NativeAOTCompatibility.Test/SystemTextJson.cs index c62ea04ec..6a1726b02 100644 --- a/test/NativeAOTCompatibility.Test/SystemTextJson.cs +++ b/test/NativeAOTCompatibility.Test/SystemTextJson.cs @@ -13,8 +13,7 @@ internal static async Task RunAsync() JsonRpc serverRpc = new JsonRpc(new HeaderDelimitedMessageHandler(serverPipe, CreateFormatter())); JsonRpc clientRpc = new JsonRpc(new HeaderDelimitedMessageHandler(clientPipe, CreateFormatter())); - RpcTargetMetadata.RegisterEventArgs(); - var targetMetadata = RpcTargetMetadata.FromInterface(new RpcTargetMetadata.InterfaceCollection(typeof(IServer))); + var targetMetadata = RpcTargetMetadata.FromShape(); serverRpc.AddLocalRpcTarget(targetMetadata, new Server(), null); serverRpc.StartListening(); diff --git a/test/StreamJsonRpc.Analyzer.Tests/JsonRpcContractAnalyzerTests.cs b/test/StreamJsonRpc.Analyzer.Tests/JsonRpcContractAnalyzerTests.cs index 76faea5db..dff31285a 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/JsonRpcContractAnalyzerTests.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/JsonRpcContractAnalyzerTests.cs @@ -337,6 +337,23 @@ partial interface IMarshalableSubType2 : IDisposable """); } + [Fact] + public async Task RpcMarshalable_WithOptionalInterface_UsingTypeShape() + { + await VerifyCS.VerifyAnalyzerAsync(""" + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [RpcMarshalableOptionalInterface(1, typeof(IMarshalableSubType1))] + partial interface IMyRpc : IDisposable + { + } + + [RpcMarshalable(IsOptional = true), {|StreamJsonRpc0009:TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)|}] + partial interface IMarshalableSubType1 : IDisposable + { + } + """); + } + [Fact] public async Task RpcMarshalable_WithOptionalInterface() { @@ -347,7 +364,7 @@ partial interface IMyRpc : IDisposable { } - [RpcMarshalable(IsOptional = true), TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [RpcMarshalable(IsOptional = true), GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMarshalableSubType1 : IDisposable { } diff --git a/test/StreamJsonRpc.Tests.NoInterceptors/StreamJsonRpc.Tests.NoInterceptors.csproj b/test/StreamJsonRpc.Tests.NoInterceptors/StreamJsonRpc.Tests.NoInterceptors.csproj index ef757ccc1..46bcf0e26 100644 --- a/test/StreamJsonRpc.Tests.NoInterceptors/StreamJsonRpc.Tests.NoInterceptors.csproj +++ b/test/StreamJsonRpc.Tests.NoInterceptors/StreamJsonRpc.Tests.NoInterceptors.csproj @@ -11,6 +11,8 @@ $(NoWarn);xUnit1051 $(NoWarn);CS0436 + + $(NoWarn);PT0022 true $(DefineConstants);NO_INTERCEPTORS diff --git a/test/StreamJsonRpc.Tests/JsonRpcProxyGenerationTests.cs b/test/StreamJsonRpc.Tests/JsonRpcProxyGenerationTests.cs index e12e5ec34..2820c9bf5 100644 --- a/test/StreamJsonRpc.Tests/JsonRpcProxyGenerationTests.cs +++ b/test/StreamJsonRpc.Tests/JsonRpcProxyGenerationTests.cs @@ -184,8 +184,7 @@ public partial interface IReferenceAnUnreachableAssembly Task TakeAsync(UnreachableAssembly.SomeUnreachableClass obj); } - [JsonRpcContract] - [SuppressMessage("Usage", "StreamJsonRpc0008", Justification = "Blocked by https://github.com/eiriktsarpalis/PolyType/issues/233")] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] internal partial interface IServerInternal : ExAssembly.ISomeInternalProxyInterface, IServerInternalWithInternalTypesFromOtherAssemblies, diff --git a/test/StreamJsonRpc.Tests/MarshalableProxyTests.cs b/test/StreamJsonRpc.Tests/MarshalableProxyTests.cs index 5b2d49ed5..38783a526 100644 --- a/test/StreamJsonRpc.Tests/MarshalableProxyTests.cs +++ b/test/StreamJsonRpc.Tests/MarshalableProxyTests.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using MessagePack; @@ -164,7 +163,7 @@ public partial interface IMarshalableWithOptionalInterfaces2 : IMarshalableWithO { } - [RpcMarshalable(IsOptional = true), TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [RpcMarshalable(IsOptional = true), GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IMarshalableNonExtendingBase : IDisposable { Task GetPlusFourAsync(int value); @@ -178,7 +177,7 @@ public interface IMarshalableSubTypeIntermediateInterface : IMarshalableWithOpti Task GetPlusTwoAsync(int value); } - [RpcMarshalable(IsOptional = true), TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [RpcMarshalable(IsOptional = true), GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IMarshalableSubTypeWithIntermediateInterface : IMarshalableSubTypeIntermediateInterface { new Task GetPlusTwoAsync(int value); @@ -186,13 +185,13 @@ public partial interface IMarshalableSubTypeWithIntermediateInterface : IMarshal Task GetPlusThreeAsync(int value); } - [RpcMarshalable(IsOptional = true), TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [RpcMarshalable(IsOptional = true), GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IMarshalableSubTypeWithIntermediateInterface2 : IMarshalableSubTypeIntermediateInterface { new Task GetPlusTwoAsync(int value); } - [RpcMarshalable(IsOptional = true), TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [RpcMarshalable(IsOptional = true), GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IMarshalableSubType1 : IMarshalableWithOptionalInterfaces2 { Task GetPlusOneAsync(int value); @@ -200,7 +199,7 @@ public partial interface IMarshalableSubType1 : IMarshalableWithOptionalInterfac Task GetMinusOneAsync(int value); } - [RpcMarshalable(IsOptional = true), TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [RpcMarshalable(IsOptional = true), GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IMarshalableSubType1Extended : IMarshalableSubType1 { new Task GetAsync(int value); @@ -216,14 +215,13 @@ public partial interface IMarshalableSubType1Extended : IMarshalableSubType1 Task GetMinusTwoAsync(int value); } - [RpcMarshalable(IsOptional = true)] - [SuppressMessage("Usage", "StreamJsonRpc0008", Justification = "Blocked by https://github.com/eiriktsarpalis/PolyType/issues/233")] + [RpcMarshalable(IsOptional = true), GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IMarshalableSubTypesCombined : IMarshalableSubType1Extended, IMarshalableSubType2, IMarshalableNonExtendingBase { Task GetPlusFiveAsync(int value); } - [RpcMarshalable(IsOptional = true), TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [RpcMarshalable(IsOptional = true), GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] [RpcMarshalableOptionalInterface(1, typeof(IMarshalableSubType2Extended))] public partial interface IMarshalableSubType2 : IMarshalableWithOptionalInterfaces2 { @@ -232,7 +230,7 @@ public partial interface IMarshalableSubType2 : IMarshalableWithOptionalInterfac Task GetMinusTwoAsync(int value); } - [RpcMarshalable(IsOptional = true), TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [RpcMarshalable(IsOptional = true), GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IMarshalableSubType2Extended : IMarshalableSubType2 { Task GetPlusThreeAsync(int value); diff --git a/test/StreamJsonRpc.Tests/RpcTargetMetadataTests.cs b/test/StreamJsonRpc.Tests/RpcTargetMetadataTests.cs index 0ff43f8c0..e4b47d9fd 100644 --- a/test/StreamJsonRpc.Tests/RpcTargetMetadataTests.cs +++ b/test/StreamJsonRpc.Tests/RpcTargetMetadataTests.cs @@ -70,7 +70,7 @@ public void FromInterface_ReturnsInheritedMembers() RpcTargetMetadata metadata = RpcTargetMetadata.FromInterface(rpcContract); Assert.Contains(metadata.Methods, m => m.Key == nameof(IRpcContractBase.MethodBaseAsync)); - Assert.Contains(metadata.Events, e => e.Event.Name == nameof(IRpcContractBase.BaseEvent)); + Assert.Contains(metadata.Events, e => e.Name == nameof(IRpcContractBase.BaseEvent)); } [Fact] @@ -81,7 +81,7 @@ public void FromInterface_ReturnsDirectMembers() RpcTargetMetadata metadata = RpcTargetMetadata.FromInterface(rpcContract); Assert.Contains(metadata.Methods, m => m.Key == nameof(IRpcContractDerived.MethodDerivedAsync)); - Assert.Contains(metadata.Events, e => e.Event.Name == nameof(IRpcContractDerived.DerivedEvent)); + Assert.Contains(metadata.Events, e => e.Name == nameof(IRpcContractDerived.DerivedEvent)); } [Fact] diff --git a/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj b/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj index 702e8bd6c..ca140cca7 100644 --- a/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj +++ b/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj @@ -13,7 +13,7 @@ $(NoWarn);VSTHRD012 - $(NoWarn);PT0021 + $(NoWarn);PT0021;PT0022 diff --git a/test/StreamJsonRpc.Tests/TargetObjectEventsNerdbankMessagePackTests.cs b/test/StreamJsonRpc.Tests/TargetObjectEventsNerdbankMessagePackTests.cs index 3334ab106..045c8b992 100644 --- a/test/StreamJsonRpc.Tests/TargetObjectEventsNerdbankMessagePackTests.cs +++ b/test/StreamJsonRpc.Tests/TargetObjectEventsNerdbankMessagePackTests.cs @@ -11,6 +11,14 @@ protected override void InitializeFormattersAndHandlers() this.clientMessageHandler = new LengthHeaderMessageHandler(this.clientStream, this.clientStream, clientMessageFormatter); } - [GenerateShapeFor] + protected override JsonRpc CreateJsonRpcWithTargetObject(IJsonRpcMessageHandler messageHandler, T targetObject, JsonRpcTargetOptions? options) + { + JsonRpc jsonRpc = new(messageHandler); + jsonRpc.AddLocalRpcTarget(RpcTargetMetadata.FromShape(Witness.GeneratedTypeShapeProvider), targetObject, options); + return jsonRpc; + } + + [GenerateShapeFor(IncludeMethods = MethodShapeFlags.PublicInstance)] + [GenerateShapeFor(IncludeMethods = MethodShapeFlags.PublicInstance)] private partial class Witness; } diff --git a/test/StreamJsonRpc.Tests/TargetObjectEventsTests.cs b/test/StreamJsonRpc.Tests/TargetObjectEventsTests.cs index fd84ae99c..da9d80282 100644 --- a/test/StreamJsonRpc.Tests/TargetObjectEventsTests.cs +++ b/test/StreamJsonRpc.Tests/TargetObjectEventsTests.cs @@ -39,14 +39,18 @@ public partial interface IFruit string Name { get; } } - public interface IServer + [JsonRpcContract] + [GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + public partial interface IServer { event EventHandler InterfaceEvent; event EventHandler ExplicitInterfaceImplementation_Event; } - public interface IServerDerived : IServer + [JsonRpcContract] + [GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + public partial interface IServerDerived : IServer { event EventHandler DerivedInterfaceEvent; } @@ -57,10 +61,9 @@ public interface IServerDerived : IServer public void ServerEventRespondsToOptions(bool registerOn) { var streams = FullDuplexStream.CreateStreams(); - var rpc = new JsonRpc(streams.Item1, streams.Item1); var options = new JsonRpcTargetOptions { NotifyClientOfEvents = registerOn }; var server = new Server(); - rpc.AddLocalRpcTarget(server, options); + var rpc = this.CreateJsonRpcWithTargetObject(streams.Item1, server, options); if (registerOn) { Assert.NotNull(server.ServerEventAccessor); @@ -172,8 +175,7 @@ public void EventsAreNotOfferedAsTargetMethods() return clrMethodName; }; - var serverRpc = new JsonRpc(this.serverStream, this.serverStream); - serverRpc.AddLocalRpcTarget(this.server, new JsonRpcTargetOptions { MethodNameTransform = methodNameTransform }); + this.CreateJsonRpcWithTargetObject(this.serverStream, this.server, new JsonRpcTargetOptions { MethodNameTransform = methodNameTransform }); } [Fact] @@ -198,11 +200,9 @@ public async Task NameTransformIsUsedWhenRaisingEvent() var clientWithoutTransform = new Client(); var clientWithTransform = new Client(); - this.serverRpc = new JsonRpc(this.serverStream, this.serverStream); + this.serverRpc = this.CreateJsonRpcWithTargetObject(this.serverStream, serverWithTransform, new JsonRpcTargetOptions { EventNameTransform = eventNameTransform }); this.clientRpc = new JsonRpc(this.clientStream, this.clientStream); - this.serverRpc.AddLocalRpcTarget(serverWithTransform, new JsonRpcTargetOptions { EventNameTransform = eventNameTransform }); - // We have to use MethodNameTransform here as Client uses methods to listen for events on server this.clientRpc.AddLocalRpcTarget(clientWithoutTransform); this.clientRpc.AddLocalRpcTarget(clientWithTransform, new JsonRpcTargetOptions { MethodNameTransform = eventNameTransform }); @@ -229,8 +229,7 @@ public async Task AddLocalRpcTarget_OfT_Interface() this.serverStream = streams.Item1; this.clientStream = streams.Item2; - this.serverRpc = new JsonRpc(this.serverStream); - this.serverRpc.AddLocalRpcTarget(this.server, null); + this.serverRpc = this.CreateJsonRpcWithTargetObject(this.serverStream, this.server); this.serverRpc.StartListening(); this.clientRpc = new JsonRpc(this.clientStream); @@ -262,8 +261,7 @@ public async Task AddLocalRpcTarget_OfT_DerivedInterface() this.serverStream = streams.Item1; this.clientStream = streams.Item2; - this.serverRpc = new JsonRpc(this.serverStream); - this.serverRpc.AddLocalRpcTarget(this.server, null); + this.serverRpc = this.CreateJsonRpcWithTargetObject(this.serverStream, this.server); this.serverRpc.StartListening(); this.clientRpc = new JsonRpc(this.clientStream); @@ -301,8 +299,7 @@ public async Task AddLocalRpcTarget_OfT_ActualClass() this.serverStream = streams.Item1; this.clientStream = streams.Item2; - this.serverRpc = new JsonRpc(this.serverStream); - this.serverRpc.AddLocalRpcTarget(this.server, null); + this.serverRpc = this.CreateJsonRpcWithTargetObject(this.serverStream, this.server); this.serverRpc.StartListening(); this.clientRpc = new JsonRpc(this.clientStream); @@ -340,6 +337,18 @@ protected override void Dispose(bool disposing) protected abstract void InitializeFormattersAndHandlers(); + protected virtual JsonRpc CreateJsonRpcWithTargetObject(Stream stream, T targetObject, JsonRpcTargetOptions? options = null) + where T : notnull + => this.CreateJsonRpcWithTargetObject(new HeaderDelimitedMessageHandler(stream), targetObject, options); + + protected virtual JsonRpc CreateJsonRpcWithTargetObject(IJsonRpcMessageHandler messageHandler, T targetObject, JsonRpcTargetOptions? options = null) + where T : notnull + { + JsonRpc jsonRpc = new(messageHandler); + jsonRpc.AddLocalRpcTarget(targetObject, options); + return jsonRpc; + } + private void ReinitializeRpcWithoutListening() { var streams = Nerdbank.FullDuplexStream.CreateStreams(); @@ -348,8 +357,8 @@ private void ReinitializeRpcWithoutListening() this.InitializeFormattersAndHandlers(); - this.serverRpc = new JsonRpc(this.serverMessageHandler, this.server); - this.clientRpc = new JsonRpc(this.clientMessageHandler, this.client); + this.serverRpc = this.CreateJsonRpcWithTargetObject(this.serverMessageHandler, this.server); + this.clientRpc = this.CreateJsonRpcWithTargetObject(this.clientMessageHandler, this.client); this.serverRpc.TraceSource = new TraceSource("Server", SourceLevels.Verbose); this.clientRpc.TraceSource = new TraceSource("Client", SourceLevels.Verbose); @@ -388,7 +397,7 @@ protected internal class MessageEventArgs : EventArgs public T? Message { get; set; } } - protected class Client + protected internal class Client { internal Action? ServerEventRaised { get; set; } @@ -411,12 +420,12 @@ protected class Client public void IFruitEvent(IFruit args) => this.ServerIFruitEventRaised?.Invoke(args); } - protected abstract class ServerBase + protected internal abstract class ServerBase { public abstract event EventHandler? AbstractBaseEvent; } - protected class Server : ServerBase, IServerDerived + protected internal class Server : ServerBase, IServerDerived { private EventHandler? explicitInterfaceImplementationEvent;