diff --git a/azure-pipelines/build.yml b/azure-pipelines/build.yml index 88b7adc3f..cd3e53e1e 100644 --- a/azure-pipelines/build.yml +++ b/azure-pipelines/build.yml @@ -251,10 +251,10 @@ jobs: BuildRequiresAccessToken: ${{ parameters.RealSign }} # Real signing on non-Windows machines requires passing through access token to build steps that sign osRID: linux - ${{ if parameters.EnableDotNetFormatCheck }}: - - script: dotnet format --verify-no-changes --exclude test/NativeAOTCompatibility.Test + - script: | + dotnet build # workaround for https://github.com/dotnet/sdk/issues/50499 + dotnet format --verify-no-changes --exclude test/NativeAOTCompatibility.Test displayName: 💅 Verify formatted code - env: - dotnetformat: true # part of a workaround for https://github.com/dotnet/sdk/issues/44951 - ${{ if parameters.EnableMacOSBuild }}: - job: macOS diff --git a/docfx/analyzers/StreamJsonRpc0007.md b/docfx/analyzers/StreamJsonRpc0007.md index ffc5862e7..cc2a7b36a 100644 --- a/docfx/analyzers/StreamJsonRpc0007.md +++ b/docfx/analyzers/StreamJsonRpc0007.md @@ -1,6 +1,6 @@ # StreamJsonRpc0007: Use RpcMarshalableAttribute on optional marshalable interface -An interface specified as an argument to must itself be attributed with . +An interface specified as an argument to must itself be attributed with with set to true. ## Example violation @@ -11,6 +11,7 @@ The other is designated as optional and thus needs the attribute. ## Resolution -Add to the optional interface. +Add to the optional interface, taking care to set to true. +We also add as required by [StreamJsonRpc0008](StreamJsonRpc0008.md). [!code-csharp[](../../samples/Analyzers/StreamJsonRpc0007.cs#Fix)] diff --git a/docfx/analyzers/StreamJsonRpc0008.md b/docfx/analyzers/StreamJsonRpc0008.md new file mode 100644 index 000000000..f839a2ab5 --- /dev/null +++ b/docfx/analyzers/StreamJsonRpc0008.md @@ -0,0 +1,18 @@ +# StreamJsonRpc0008: Add methods to PolyType shape for RPC contract interface + +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 . + +## Example violation + +The following interface serves as an RPC interface but has no PolyType shape with methods included: + +[!code-csharp[](../../samples/Analyzers/StreamJsonRpc0008.cs#Violation)] + +## Resolution + +Add a to the interface and set its named argument to (or a superset of that). + +[!code-csharp[](../../samples/Analyzers/StreamJsonRpc0008.cs#Fix)] diff --git a/docfx/analyzers/StreamJsonRpc0030.md b/docfx/analyzers/StreamJsonRpc0030.md new file mode 100644 index 000000000..006df61d2 --- /dev/null +++ b/docfx/analyzers/StreamJsonRpc0030.md @@ -0,0 +1,15 @@ +# StreamJsonRpc0030: `[JsonRpcProxy]` should be applied only to generic interfaces + +The attribute is intended for closing open generic interfaces that serve as RPC contracts in order to ensure a proxy is available at runtime that implements particular closed instances of those open generic interfaces. + +## Example violation + +The following RPC contract is not generic, yet applies this attribute: + +[!code-csharp[](../../samples/Analyzers/StreamJsonRpc0030.cs#Violation)] + +## Resolution + +Drop the unnecessary attribute: + +[!code-csharp[](../../samples/Analyzers/StreamJsonRpc0030.cs#Fix)] diff --git a/docfx/analyzers/StreamJsonRpc0031.md b/docfx/analyzers/StreamJsonRpc0031.md new file mode 100644 index 000000000..472055a1a --- /dev/null +++ b/docfx/analyzers/StreamJsonRpc0031.md @@ -0,0 +1,15 @@ +# StreamJsonRpc0031: `[JsonRpcProxy]` type argument should be a closed instance of the applied type + +The attribute is intended for closing open generic interfaces that serve as RPC contracts in order to ensure a proxy is available at runtime that implements particular closed instances of those open generic interfaces. + +## Example violation + +The following RPC contract has an with a type argument that does not match the interface itself: + +[!code-csharp[](../../samples/Analyzers/StreamJsonRpc0031.cs#Violation)] + +## Resolution + +Adjust the type argument to match the applied interface: + +[!code-csharp[](../../samples/Analyzers/StreamJsonRpc0031.cs#Fix)] diff --git a/docfx/analyzers/StreamJsonRpc0032.md b/docfx/analyzers/StreamJsonRpc0032.md new file mode 100644 index 000000000..c477d161e --- /dev/null +++ b/docfx/analyzers/StreamJsonRpc0032.md @@ -0,0 +1,15 @@ +# StreamJsonRpc0032: `[JsonRpcProxy]` should be accompanied by another contract attribute + +The attribute is intended for closing open generic interfaces that serve as RPC contracts in order to ensure a proxy is available at runtime that implements particular closed instances of those open generic interfaces. + +## Example violation + +The following interface has an applied but no RPC contract attribute: + +[!code-csharp[](../../samples/Analyzers/StreamJsonRpc0032.cs#Violation)] + +## Resolution + +Add either or to the interface: + +[!code-csharp[](../../samples/Analyzers/StreamJsonRpc0032.cs#Fix)] diff --git a/docfx/analyzers/StreamJsonRpc0050.md b/docfx/analyzers/StreamJsonRpc0050.md new file mode 100644 index 000000000..b8a5f9102 --- /dev/null +++ b/docfx/analyzers/StreamJsonRpc0050.md @@ -0,0 +1,22 @@ +# StreamJsonRpc0050: Use `IClientProxy.Is` or `JsonRpcExtensions.As` + +When casting or type checking between two -annotated interfaces, it is preferable to use the or methods rather than a direct cast or traditional type check. +This is because the interface may be implemented by a proxy object that implements more interfaces than the marshaled object actually implements. +Using these methods informs the caller as to the actual interfaces that are supported by the remote object. + +## Example violation + +The following code uses various traditional casts and type checks between two marshalable interfaces: + +[!code-csharp[](../../samples/Analyzers/StreamJsonRpc0050.cs#Violation)] + +## Resolution + +Use the or methods instead of traditional casts and type checks. +In fact these are exposed as extension methods on each of the marshalable interfaces, so you can call them directly. + +[!code-csharp[](../../samples/Analyzers/StreamJsonRpc0050.cs#Fix)] + +These extension methods are implemented to use the interface on the same object to test for interface availability on the server, but will gracefully fallback to traditional type checks if is not implemented by the same object, making these extension methods safe to call even when the interfaces are implemented by a user-defined type. + +If proxies are generated for these interfaces by any other system, that proxy should also implement to participate in similar dynamic type checking. diff --git a/docfx/analyzers/index.md b/docfx/analyzers/index.md index 4f23ba484..e9914ffed 100644 --- a/docfx/analyzers/index.md +++ b/docfx/analyzers/index.md @@ -13,9 +13,14 @@ Some of these diagnostics will include a suggested code fix that can apply the c | [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 | +| [StreamJsonRpc0008](StreamJsonRpc0008.md) | Usage | Warning | Add methods to PolyType shape for RPC contract 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 | | [StreamJsonRpc0014](StreamJsonRpc0014.md) | Usage | Error | CancellationToken as last parameter | | [StreamJsonRpc0015](StreamJsonRpc0015.md) | Usage | Error | No generic interfaces | | [StreamJsonRpc0016](StreamJsonRpc0016.md) | Usage | Error | Unsupported event delegate type | +| [StreamJsonRpc0030](StreamJsonRpc0030.md) | Usage | Error | should be applied only to generic interfaces +| [StreamJsonRpc0031](StreamJsonRpc0031.md) | Usage | Error | type argument should be a closed instance of the applied type +| [StreamJsonRpc0032](StreamJsonRpc0032.md) | Usage | Error | should be accompanied by another contract attribute +| [StreamJsonRpc0050](StreamJsonRpc0050.md) | Usage | Warning | Use or diff --git a/docfx/analyzers/toc.yml b/docfx/analyzers/toc.yml index 2347b420b..56ffba886 100644 --- a/docfx/analyzers/toc.yml +++ b/docfx/analyzers/toc.yml @@ -7,9 +7,14 @@ items: - href: StreamJsonRpc0005.md - href: StreamJsonRpc0006.md - href: StreamJsonRpc0007.md +- href: StreamJsonRpc0008.md - href: StreamJsonRpc0011.md - href: StreamJsonRpc0012.md - href: StreamJsonRpc0013.md - href: StreamJsonRpc0014.md - href: StreamJsonRpc0015.md - href: StreamJsonRpc0016.md +- href: StreamJsonRpc0030.md +- href: StreamJsonRpc0031.md +- href: StreamJsonRpc0032.md +- href: StreamJsonRpc0050.md diff --git a/docfx/docfx.json b/docfx/docfx.json index 664728e79..c99971ec3 100644 --- a/docfx/docfx.json +++ b/docfx/docfx.json @@ -31,6 +31,8 @@ } ], "xref": [ + "https://eiriktsarpalis.github.io/PolyType/xrefmap.yml", + "https://aarnott.github.io/Nerdbank.MessagePack/xrefmap.yml", "https://dotnet.github.io/Nerdbank.Streams/xrefmap.yml", "https://microsoft.github.io/vs-threading/xrefmap.yml", "https://microsoft.github.io/vs-validation/xrefmap.yml", diff --git a/docfx/docs/proxies.md b/docfx/docs/proxies.md index 704656ea4..9546907e9 100644 --- a/docfx/docs/proxies.md +++ b/docfx/docs/proxies.md @@ -43,6 +43,8 @@ Applying the to all RPC interfaces 1. Enables analyzers to report warnings due to violations of the above rules at compile-time instead of throwing at runtime. 1. Enables source generated proxies to be used at runtime instead of proxies created dynamically at runtime, leading to faster startup and [NativeAOT](nativeAOT.md) compatibility. +Interfaces with either or applied should also apply with its set to . + ### Server-side concerns On the server side, these same methods may be simple and naturally synchronous. diff --git a/docfx/exotic_types/rpc_marshalable_objects.md b/docfx/exotic_types/rpc_marshalable_objects.md index 02ec6dad7..291698e8a 100644 --- a/docfx/exotic_types/rpc_marshalable_objects.md +++ b/docfx/exotic_types/rpc_marshalable_objects.md @@ -1,7 +1,7 @@ # `RpcMarshalableAttribute` StreamJsonRpc typically *serializes* values that are passed in arguments or return values of RPC methods, which effectively transmits the data of an object or struct to the remote party. -By applying the `RpcMarshalableAttribute` to an interface, it a proxy can be sent to effectively marshal *behavior* to the remote party instead of data, similar to other [exotic types](index.md). +By applying the to an interface, it a proxy can be sent to effectively marshal *behavior* to the remote party instead of data, similar to other [exotic types](index.md). StreamJsonRpc allows transmitting marshalable objects (i.e., objects implementing a marshalable interface) in arguments and return values. @@ -13,7 +13,7 @@ Marshalable interfaces must: The object that implements a marshalable interface may include properties and events as well as other additional members but only the methods defined by the marshalable interface will be available on the proxy, and the data will not be serialized. -The `RpcMarshalableAttribute` must be applied directly to the interface used as the return type, parameter type, or member type within a return type or parameter type's object graph. +The must be applied directly to the interface used as the return type, parameter type, or member type within a return type or parameter type's object graph. The attribute is not inherited. In fact different interfaces in a type hierarchy can have this attribute applied with distinct settings, and only the settings on the attribute applied directly to the interface used will apply. @@ -275,27 +275,34 @@ These resources are released and the `IDisposable.Dispose()` method is invoked o * The receiver calls `IDisposable.Dispose()` on the proxy. * The JSON-RPC connection is closed. -## `RpcMarshalableOptionalInterfaceAttribute` +## -StreamJsonRpc provides the `RpcMarshalableOptionalInterfaceAttribute` to specify that marshalable objects implementing an RPC interface can optionally implement additional interfaces. +StreamJsonRpc provides the to specify that marshalable objects implementing an RPC interface can optionally implement additional interfaces. -`RpcMarshalableOptionalInterfaceAttribute` is applied to the interface used in the RPC contract. -Such an interface must also apply the `RpcMarshalableAttribute`. + is applied to the interface used in the RPC contract. +Such an interface must also apply the . -An interface that `RpcMarshalableOptionalInterfaceAttribute` references must have the `RpcMarshalableAttribute` attribute and must adhere to all the requirements of marshalable interfaces as described earlier in this document. +An interface that references must have the attribute applied, set to `true`, and must adhere to all the requirements of marshalable interfaces as described earlier in this document. -The proxy object created for the receiver of a marshaled object will implement all the interfaces that are both implemented by the original object and that are identified with `RpcMarshalableOptionalInterfaceAttribute` on the interface type used in the RPC contract. -The receiver of the proxy can use the [is](https://learn.microsoft.com/dotnet/csharp/language-reference/operators/is) operator to check if an optional interface is implemented without throwing an `InvalidCastException` when the interface is not implemented. +The proxy object created for the receiver of a marshaled object will implement all the interfaces that are both implemented by the original object and that are identified with on the interface type used in the RPC contract. + +The receiver of the proxy should avoid using standard type check or cast operators such as [is](https://learn.microsoft.com/dotnet/csharp/language-reference/operators/is) to check if an optional interface is implemented because a proxy *may* implement more interfaces than the marshaled object does. +Instead, test proxies for optional interfaces using the method or extension method. + +An interface that also carries attributes will lead to generation of extension methods for itself and for each of the optional interfaces. +These extension methods expose the and methods so they are conveniently accessible via these RPC interfaces more directly so that conditional casting to is not required at each callsite. + +The `PublicRpcMarshalableInterfaceExtensions` MSBuild property can be set to `true` in the project file to cause these extension methods to be declares on a `public` extension class rather than an `internal` one, so that referencing assemblies that use these proxies can have the same level of convenience in testing for optional interfaces. ### Use cases -`RpcMarshalableOptionalInterfaceAttribute` is useful in a few scenarios: + is useful in a few scenarios: 1. when adding new methods to an existing marshalable interface would break backward compatibility for another scenario, 1. when the host of the marshaled object may deem it appropriate to expose different behaviors based on the scenario, 1. when an RPC method returns marshalable objects which may optionally implement additional functionality based on the arguments passed to that method. -For example, the following code shows how `RpcMarshalableOptionalInterfaceAttribute` can be added to the `ICounter` interface from the earlier sample: +For example, the following code shows how can be added to the `ICounter` interface from the earlier sample: ```cs [RpcMarshalable] @@ -331,8 +338,8 @@ The proxy receiver will be able to perceive through type checks which interfaces #### Backward compatibility -The `optionalInterfaceCode` values used in `RpcMarshalableOptionalInterfaceAttribute` are used as part of the wire protocol. -While it can be backward compatible to remove an `RpcMarshalableOptionalInterfaceAttribute` from an interface, its `optionalInterfaceCode` value should never be reused to add a different interface to the same interface declaration to avoid an older remote party misinterpreting the value as identifying the older interface. +The `optionalInterfaceCode` values used in are used as part of the wire protocol. +While it can be backward compatible to remove an from an interface, its `optionalInterfaceCode` value should never be reused to add a different interface to the same interface declaration to avoid an older remote party misinterpreting the value as identifying the older interface. #### Method name conflicts and non-marshalable interfaces @@ -388,7 +395,7 @@ Method call | Invoked server-side method | Conditions `((IBaz)proxy).DoBazAsync()` | `DoBazAsync` as defined by `IBaz` | If the marshalable object implements `IBaz` `((IBaz2)proxy).DoBazAsync()` | `DoBazAsync` as defined by `IBaz2` | If the marshalable object implements `IBaz2` -⚠️ An attempt to cast proxy to `IBar` would fail, even if the original object implemented that interface, if that object did not also implement `IBaz` or `IBaz2`, since `IBar` is not listed in an `RpcMarshalableOptionalInterfaceAttribute` of `IFoo`. +⚠️ An attempt to cast proxy to `IBar` would fail, even if the original object implemented that interface, if that object did not also implement `IBaz` or `IBaz2`, since `IBar` is not listed in an of `IFoo`. A call to `((IBar)proxy).DoFooAsync()` would result in the following behavior: diff --git a/samples/Analyzers/NoProxyAttribute.cs b/samples/Analyzers/NoProxyAttribute.cs index fc79342f7..6bfb26a44 100644 --- a/samples/Analyzers/NoProxyAttribute.cs +++ b/samples/Analyzers/NoProxyAttribute.cs @@ -9,3 +9,9 @@ namespace Samples.Analyzers.NoProxy; /// [AttributeUsage(AttributeTargets.Interface)] internal class JsonRpcContractAttribute : Attribute; + +[AttributeUsage(AttributeTargets.Interface)] +internal class GenerateShapeAttribute : Attribute +{ + public MethodShapeFlags IncludeMethods { get; init; } +} diff --git a/samples/Analyzers/StreamJsonRpc0001.cs b/samples/Analyzers/StreamJsonRpc0001.cs index e4806aeaf..48ab06d46 100644 --- a/samples/Analyzers/StreamJsonRpc0001.cs +++ b/samples/Analyzers/StreamJsonRpc0001.cs @@ -1,6 +1,8 @@ +#pragma warning disable StreamJsonRpc0008 + namespace StreamJsonRpc0001.Violation { -#pragma warning disable StreamJsonRpc0001 +#pragma warning disable StreamJsonRpc0001, PT0005 #region Violation internal partial class Wrapper { @@ -11,7 +13,7 @@ private partial interface IMyService } } #endregion -#pragma warning restore StreamJsonRpc0001 +#pragma warning restore StreamJsonRpc0001, PT0005 } namespace StreamJsonRpc0001.Fix diff --git a/samples/Analyzers/StreamJsonRpc0002.cs b/samples/Analyzers/StreamJsonRpc0002.cs index 31b3472cf..b9608550e 100644 --- a/samples/Analyzers/StreamJsonRpc0002.cs +++ b/samples/Analyzers/StreamJsonRpc0002.cs @@ -1,3 +1,5 @@ +#pragma warning disable StreamJsonRpc0008 + namespace StreamJsonRpc0002.Violation { #pragma warning disable StreamJsonRpc0002 diff --git a/samples/Analyzers/StreamJsonRpc0003.cs b/samples/Analyzers/StreamJsonRpc0003.cs index 7eb2d5349..e106e1f8f 100644 --- a/samples/Analyzers/StreamJsonRpc0003.cs +++ b/samples/Analyzers/StreamJsonRpc0003.cs @@ -25,7 +25,7 @@ namespace StreamJsonRpc0003.Fix partial class Wrapper { #region Fix - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] internal partial interface IMyService { } diff --git a/samples/Analyzers/StreamJsonRpc0004.cs b/samples/Analyzers/StreamJsonRpc0004.cs index 2a2d8c012..0f8bb60f1 100644 --- a/samples/Analyzers/StreamJsonRpc0004.cs +++ b/samples/Analyzers/StreamJsonRpc0004.cs @@ -25,7 +25,7 @@ namespace StreamJsonRpc0004.Fix partial class Wrapper { #region Fix - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] internal partial interface IMyService { } diff --git a/samples/Analyzers/StreamJsonRpc0005.cs b/samples/Analyzers/StreamJsonRpc0005.cs index c6978fd05..26507d1cc 100644 --- a/samples/Analyzers/StreamJsonRpc0005.cs +++ b/samples/Analyzers/StreamJsonRpc0005.cs @@ -2,7 +2,7 @@ namespace StreamJsonRpc0005.Violation { #pragma warning disable StreamJsonRpc0005 #region Violation - [RpcMarshalable] + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMyObject { } @@ -13,7 +13,7 @@ partial interface IMyObject namespace StreamJsonRpc0005.Fix1 { #region Fix1 - [RpcMarshalable] + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMyObject : IDisposable { } @@ -23,7 +23,7 @@ partial interface IMyObject : IDisposable namespace StreamJsonRpc0005.Fix2 { #region Fix2 - [RpcMarshalable(CallScopedLifetime = true)] + [RpcMarshalable(CallScopedLifetime = true), TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMyObject { } diff --git a/samples/Analyzers/StreamJsonRpc0006.cs b/samples/Analyzers/StreamJsonRpc0006.cs index 9c7830365..f3083be5d 100644 --- a/samples/Analyzers/StreamJsonRpc0006.cs +++ b/samples/Analyzers/StreamJsonRpc0006.cs @@ -17,13 +17,13 @@ partial interface IMyService2 namespace StreamJsonRpc0006.Fix { #region Fix - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] [JsonRpcProxyInterfaceGroup(typeof(IMyService2))] partial interface IMyService { } - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMyService2 { } diff --git a/samples/Analyzers/StreamJsonRpc0007.cs b/samples/Analyzers/StreamJsonRpc0007.cs index 84643f3c6..341ee65e7 100644 --- a/samples/Analyzers/StreamJsonRpc0007.cs +++ b/samples/Analyzers/StreamJsonRpc0007.cs @@ -2,7 +2,7 @@ namespace StreamJsonRpc0007.Violation { #pragma warning disable StreamJsonRpc0007 #region Violation - [RpcMarshalable] + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] [RpcMarshalableOptionalInterface(1, typeof(IMyObject2))] // StreamJsonRpc0007 reported here partial interface IMyObject : IDisposable { @@ -18,13 +18,13 @@ partial interface IMyObject2 : IDisposable namespace StreamJsonRpc0007.Fix { #region Fix - [RpcMarshalable] + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] [RpcMarshalableOptionalInterface(1, typeof(IMyObject2))] partial interface IMyObject : IDisposable { } - [RpcMarshalable] + [RpcMarshalable(IsOptional = true), TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMyObject2 : IDisposable { } diff --git a/samples/Analyzers/StreamJsonRpc0008.cs b/samples/Analyzers/StreamJsonRpc0008.cs new file mode 100644 index 000000000..7cf40d998 --- /dev/null +++ b/samples/Analyzers/StreamJsonRpc0008.cs @@ -0,0 +1,21 @@ +namespace StreamJsonRpc0008.Violation +{ +#pragma warning disable StreamJsonRpc0008 + #region Violation + [JsonRpcContract] + partial interface IMyObject + { + } + #endregion +#pragma warning restore StreamJsonRpc0008 +} + +namespace StreamJsonRpc0008.Fix +{ + #region Fix + [JsonRpcContract, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + partial interface IMyObject + { + } + #endregion +} diff --git a/samples/Analyzers/StreamJsonRpc0011.cs b/samples/Analyzers/StreamJsonRpc0011.cs index ac33a8464..abfae9922 100644 --- a/samples/Analyzers/StreamJsonRpc0011.cs +++ b/samples/Analyzers/StreamJsonRpc0011.cs @@ -2,7 +2,7 @@ namespace StreamJsonRpc0011.Violation { #pragma warning disable StreamJsonRpc0011 #region Violation - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMyService { int Add(int a, int b); // StreamJsonRpc0011 @@ -14,7 +14,7 @@ partial interface IMyService namespace StreamJsonRpc0011.Fix { #region Fix - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMyService { Task Add(int a, int b); diff --git a/samples/Analyzers/StreamJsonRpc0013.cs b/samples/Analyzers/StreamJsonRpc0013.cs index a7a14646a..cf13a04dd 100644 --- a/samples/Analyzers/StreamJsonRpc0013.cs +++ b/samples/Analyzers/StreamJsonRpc0013.cs @@ -1,3 +1,5 @@ +// Suppress PolyType so it doesn't complain too. +using GenerateShapeAttribute = Samples.Analyzers.NoProxy.GenerateShapeAttribute; // Suppress the proxy generation because this interface is invalid. using JsonRpcContractAttribute = Samples.Analyzers.NoProxy.JsonRpcContractAttribute; @@ -5,7 +7,7 @@ namespace StreamJsonRpc0013.Violation { #pragma warning disable StreamJsonRpc0013 #region Violation - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMyService { Task AddThis(T item); // StreamJsonRpc0013 @@ -17,7 +19,7 @@ partial interface IMyService namespace StreamJsonRpc0013.Fix { #region Fix - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMyService { Task AddThis(MyItem item); diff --git a/samples/Analyzers/StreamJsonRpc0014.cs b/samples/Analyzers/StreamJsonRpc0014.cs index 9f428c4f6..cdd58902e 100644 --- a/samples/Analyzers/StreamJsonRpc0014.cs +++ b/samples/Analyzers/StreamJsonRpc0014.cs @@ -2,7 +2,7 @@ namespace StreamJsonRpc0014.Violation { #pragma warning disable StreamJsonRpc0014 #region Violation - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMyService { Task AddAsync(int item, CancellationToken cancellationToken, int before); // StreamJsonRpc0014 @@ -14,7 +14,7 @@ partial interface IMyService namespace StreamJsonRpc0014.Fix { #region Fix - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMyService { Task AddAsync(int item, int before, CancellationToken cancellationToken); diff --git a/samples/Analyzers/StreamJsonRpc0015.cs b/samples/Analyzers/StreamJsonRpc0015.cs index e98398b17..b2901ffa1 100644 --- a/samples/Analyzers/StreamJsonRpc0015.cs +++ b/samples/Analyzers/StreamJsonRpc0015.cs @@ -1,22 +1,24 @@ +// Suppress PolyType so it doesn't complain too. +using GenerateShapeAttribute = Samples.Analyzers.NoProxy.GenerateShapeAttribute; // Suppress the proxy generation because this interface is invalid. using JsonRpcContractAttribute = Samples.Analyzers.NoProxy.JsonRpcContractAttribute; namespace StreamJsonRpc0015.Violation { -#pragma warning disable StreamJsonRpc0015 +#pragma warning disable StreamJsonRpc0015, PT0004 #region Violation - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMyService { } #endregion -#pragma warning restore StreamJsonRpc0015 +#pragma warning restore StreamJsonRpc0015, PT0004 } namespace StreamJsonRpc0015.Fix { #region Fix - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMyService { } diff --git a/samples/Analyzers/StreamJsonRpc0016.cs b/samples/Analyzers/StreamJsonRpc0016.cs index 38dac3919..c012c0c3b 100644 --- a/samples/Analyzers/StreamJsonRpc0016.cs +++ b/samples/Analyzers/StreamJsonRpc0016.cs @@ -5,7 +5,7 @@ namespace StreamJsonRpc0016.Violation { #pragma warning disable StreamJsonRpc0016 #region Violation - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMyService { event MyDelegate MyEvent; // StreamJsonRpc0016 @@ -21,7 +21,7 @@ partial interface IMyService namespace StreamJsonRpc0016.Fix { #region Fix - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMyService { event EventHandler MyEvent; diff --git a/samples/Analyzers/StreamJsonRpc0030.cs b/samples/Analyzers/StreamJsonRpc0030.cs new file mode 100644 index 000000000..d14090465 --- /dev/null +++ b/samples/Analyzers/StreamJsonRpc0030.cs @@ -0,0 +1,22 @@ +namespace StreamJsonRpc0030.Violation +{ +#pragma warning disable StreamJsonRpc0030 + #region Violation + [JsonRpcContract, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [JsonRpcProxy] + partial interface IMyService + { + } + #endregion +#pragma warning restore StreamJsonRpc0030 +} + +namespace StreamJsonRpc0030.Fix +{ + #region Fix + [JsonRpcContract, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + partial interface IMyService + { + } + #endregion +} diff --git a/samples/Analyzers/StreamJsonRpc0031.cs b/samples/Analyzers/StreamJsonRpc0031.cs new file mode 100644 index 000000000..88207f692 --- /dev/null +++ b/samples/Analyzers/StreamJsonRpc0031.cs @@ -0,0 +1,23 @@ +namespace StreamJsonRpc0031.Violation +{ +#pragma warning disable StreamJsonRpc0031 + #region Violation + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [JsonRpcProxy] + partial interface IMyService : IDisposable + { + } + #endregion +#pragma warning restore StreamJsonRpc0031 +} + +namespace StreamJsonRpc0031.Fix +{ + #region Fix + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [JsonRpcProxy>] + partial interface IMyService : IDisposable + { + } + #endregion +} diff --git a/samples/Analyzers/StreamJsonRpc0032.cs b/samples/Analyzers/StreamJsonRpc0032.cs new file mode 100644 index 000000000..2daba1a3c --- /dev/null +++ b/samples/Analyzers/StreamJsonRpc0032.cs @@ -0,0 +1,24 @@ +namespace StreamJsonRpc0032.Violation +{ +#pragma warning disable StreamJsonRpc0032 + #region Violation + [TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [JsonRpcProxy>] + partial interface IMyService : IDisposable + { + } + #endregion +#pragma warning restore StreamJsonRpc0032 +} + +namespace StreamJsonRpc0032.Fix +{ + #region Fix + [RpcMarshalable] + [TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [JsonRpcProxy>] + partial interface IMyService : IDisposable + { + } + #endregion +} diff --git a/samples/Analyzers/StreamJsonRpc0050.cs b/samples/Analyzers/StreamJsonRpc0050.cs new file mode 100644 index 000000000..9c440a014 --- /dev/null +++ b/samples/Analyzers/StreamJsonRpc0050.cs @@ -0,0 +1,51 @@ +namespace StreamJsonRpc0050.Violation +{ +#pragma warning disable StreamJsonRpc0050 + #region Violation + [TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [RpcMarshalable] + [RpcMarshalableOptionalInterface(1, typeof(IMyObject2))] + partial interface IMyObject : IDisposable + { + } + + [TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [RpcMarshalable(IsOptional = true)] + partial interface IMyObject2 : IDisposable + { + } + + class OneWay + { + bool IsOperator(IMyObject o) => o is IMyObject2; + IMyObject2? AsOperator(IMyObject o) => o as IMyObject2; + IMyObject2 CastOperator(IMyObject o) => (IMyObject2)o; + } + #endregion +#pragma warning restore StreamJsonRpc0050 +} + +namespace StreamJsonRpc0050.Fix +{ + #region Fix + [TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [RpcMarshalable] + [RpcMarshalableOptionalInterface(1, typeof(IMyObject2))] + partial interface IMyObject : IDisposable + { + } + + [TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [RpcMarshalable(IsOptional = true)] + partial interface IMyObject2 : IDisposable + { + } + + class OneWay + { + bool IsOperator(IMyObject o) => o.Is(typeof(IMyObject2)); + IMyObject2? AsOperator(IMyObject o) => o.As(); + IMyObject2 CastOperator(IMyObject o) => o.As() ?? throw new InvalidCastException(); + } + #endregion +} diff --git a/samples/Analyzers/Usings.cs b/samples/Analyzers/Usings.cs index 9ce21a845..9c8fb0060 100644 --- a/samples/Analyzers/Usings.cs +++ b/samples/Analyzers/Usings.cs @@ -1 +1,2 @@ +global using PolyType; global using StreamJsonRpc; diff --git a/samples/NativeAOT/NerdbankMessagePack.cs b/samples/NativeAOT/NerdbankMessagePack.cs index 8ca8e57b2..8feb85759 100644 --- a/samples/NativeAOT/NerdbankMessagePack.cs +++ b/samples/NativeAOT/NerdbankMessagePack.cs @@ -25,7 +25,7 @@ static async Task Main(string[] args) TypeShapeProvider = Witness.ShapeProvider, }; - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] internal partial interface IServer { Task AddAsync(int a, int b); diff --git a/samples/NativeAOT/SystemTextJson.cs b/samples/NativeAOT/SystemTextJson.cs index 8715a0601..a61758145 100644 --- a/samples/NativeAOT/SystemTextJson.cs +++ b/samples/NativeAOT/SystemTextJson.cs @@ -38,7 +38,7 @@ static async Task Main(string[] args) [JsonSerializable(typeof(int))] partial class SourceGenerationContext : JsonSerializerContext; - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] internal partial interface IServer { event EventHandler Added; diff --git a/samples/Proxies.cs b/samples/Proxies.cs index 66b0eed81..64cc8c5b5 100644 --- a/samples/Proxies.cs +++ b/samples/Proxies.cs @@ -20,7 +20,7 @@ static async Task WithProxies(Stream rpcStream) } } - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] internal partial interface ICalculator { Task AddAsync(int a, int b); diff --git a/src/StreamJsonRpc.Analyzers.CodeFixes/JsonRpcContractCodeFixProvider.cs b/src/StreamJsonRpc.Analyzers.CodeFixes/JsonRpcContractCodeFixProvider.cs index 5acf702b9..5a767e680 100644 --- a/src/StreamJsonRpc.Analyzers.CodeFixes/JsonRpcContractCodeFixProvider.cs +++ b/src/StreamJsonRpc.Analyzers.CodeFixes/JsonRpcContractCodeFixProvider.cs @@ -5,6 +5,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Formatting; @@ -30,6 +31,7 @@ public class JsonRpcContractCodeFixProvider : CodeFixProvider /// public override ImmutableArray FixableDiagnosticIds => [ JsonRpcContractAnalyzer.RpcMarshableDisposableId, + JsonRpcContractAnalyzer.GeneratePolyTypeMethodsOnRpcContractInterfaceId, ]; /// @@ -50,35 +52,73 @@ public override Task RegisterCodeFixesAsync(CodeFixContext context) equivalenceKey: nameof(AddIDisposableBaseTypeAsync)), diagnostic); break; + case JsonRpcContractAnalyzer.GeneratePolyTypeMethodsOnRpcContractInterfaceId when diagnostic.AdditionalLocations is [{ } only] && only.Equals(Location.None): + context.RegisterCodeFix( + CodeAction.Create( + title: Strings.GeneratePolyTypeMethodsOnRpcContractInterface_FixTitle, + createChangedDocument: AddMethodShapesAsync, + equivalenceKey: nameof(AddMethodShapesAsync)), + diagnostic); + break; } - async Task AddIDisposableBaseTypeAsync(CancellationToken cancellation) + async Task<(SyntaxNode Root, BaseTypeDeclarationSyntax TypeDeclaration)?> FindSyntax(CancellationToken cancellation) { - Document document = context.Document; - SyntaxNode? root = await document.GetSyntaxRootAsync(cancellation); + SyntaxNode? root = await context.Document.GetSyntaxRootAsync(cancellation); if (root is null) { - return document; + return null; } BaseTypeDeclarationSyntax? typeDecl = root.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf(); if (typeDecl is null) { - return document; + return null; } - BaseTypeDeclarationSyntax modifiedTypeDecl = typeDecl.AddBaseListTypes(SimpleBaseType( + return (root, typeDecl); + } + + async Task FinalizeDocument((SyntaxNode Root, BaseTypeDeclarationSyntax TypeDeclaration) nodes, BaseTypeDeclarationSyntax modifiedTypeDecl, CancellationToken cancellation) + { + Document modifiedDocument = await AddImportAndSimplifyAsync(context.Document.WithSyntaxRoot(nodes.Root.ReplaceNode(nodes.TypeDeclaration, modifiedTypeDecl)), cancellation); + return modifiedDocument; + } + + async Task AddIDisposableBaseTypeAsync(CancellationToken cancellation) + { + if (await FindSyntax(cancellation) is not { } nodes) + { + return context.Document; + } + + BaseTypeDeclarationSyntax modifiedTypeDecl = nodes.TypeDeclaration.AddBaseListTypes(SimpleBaseType( ParseTypeName("global::System.IDisposable").WithAdditionalAnnotations(Simplifier.AddImportsAnnotation))); // Move the new line for better formatting, if necessary. - if (typeDecl.BaseList is null && typeDecl.Identifier.HasTrailingTrivia) + if (nodes.TypeDeclaration.BaseList is null && nodes.TypeDeclaration.Identifier.HasTrailingTrivia) { modifiedTypeDecl = modifiedTypeDecl.WithIdentifier(modifiedTypeDecl.Identifier.WithTrailingTrivia(SyntaxTriviaList.Empty)); } - Document modifiedDocument = await AddImportAndSimplifyAsync(document.WithSyntaxRoot(root.ReplaceNode(typeDecl, modifiedTypeDecl)), cancellation); + return await FinalizeDocument(nodes, modifiedTypeDecl, cancellation); + } - return modifiedDocument; + async Task AddMethodShapesAsync(CancellationToken cancellation) + { + if (await FindSyntax(cancellation) is not { } nodes) + { + return context.Document; + } + + // Add a whole new TypeShapeAttribute. + bool preferGenerateShape = diagnostic.Properties.TryGetValue("PreferGenerateShape", out string? value) && value == "true"; + BaseTypeDeclarationSyntax modifiedTypeDecl = nodes.TypeDeclaration + .AddAttributeLists(AttributeList(SingletonSeparatedList(Attribute(ParseName(preferGenerateShape ? "PolyType.GenerateShape" : "PolyType.TypeShape")).AddArgumentListArguments( + AttributeArgument(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, IdentifierName("MethodShapeFlags"), IdentifierName("PublicInstance"))).WithNameEquals(NameEquals(IdentifierName("IncludeMethods")))))) + .WithAdditionalAnnotations(Simplifier.AddImportsAnnotation, Formatter.Annotation)); + + return await FinalizeDocument(nodes, modifiedTypeDecl, cancellation); } } diff --git a/src/StreamJsonRpc.Analyzers.CodeFixes/Strings.resx b/src/StreamJsonRpc.Analyzers.CodeFixes/Strings.resx index 09bd9ea71..65e4fc768 100644 --- a/src/StreamJsonRpc.Analyzers.CodeFixes/Strings.resx +++ b/src/StreamJsonRpc.Analyzers.CodeFixes/Strings.resx @@ -1,77 +1,96 @@  + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + text/microsoft-resx + 2.0 + System.Resources.ResXResourceReader, System.Windows.Forms, ... + System.Resources.ResXResourceWriter, System.Windows.Forms, ... + this is my long stringthis is a comment + Blue + + [base64 mime encoded serialized .NET Framework object] + + + [base64 mime encoded string representing a byte array form of the .NET Framework object] + This is a comment + + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + + + + + + + + + + + + + + + + + + - + + @@ -90,14 +109,17 @@ text/microsoft-resx - 1.3 + 2.0 - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Add PolyType method shapes + Add IDisposable base type diff --git a/src/StreamJsonRpc.Analyzers/AnalyzerReleases.Unshipped.md b/src/StreamJsonRpc.Analyzers/AnalyzerReleases.Unshipped.md index 9d2eb5186..86b52a344 100644 --- a/src/StreamJsonRpc.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/StreamJsonRpc.Analyzers/AnalyzerReleases.Unshipped.md @@ -12,9 +12,14 @@ StreamJsonRpc0004 | Usage | Error | Use interfaces for proxies StreamJsonRpc0005 | Usage | Error | RpcMarshalable interfaces must be IDisposable 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 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 StreamJsonRpc0014 | Usage | Error | CancellationToken may only appear as the last parameter StreamJsonRpc0015 | Usage | Error | RPC contracts may not be generic interfaces StreamJsonRpc0016 | Usage | Error | Unsupported event delegate type +StreamJsonRpc0030 | Usage | Error | `JsonRpcProxyAttribute` should be applied only to generic interfaces. +StreamJsonRpc0031 | Usage | Error | `JsonRpcProxyAttribute` type argument should be a closed instance of the applied type. +StreamJsonRpc0032 | Usage | Error | `JsonRpcProxyAttribute` should be accompanied by another contract attribute. +StreamJsonRpc0050 | Usage | Warning | Use IClientProxy.Is or JsonRpcExtensions.As. diff --git a/src/StreamJsonRpc.Analyzers/GeneratorModels/Container.cs b/src/StreamJsonRpc.Analyzers/GeneratorModels/Container.cs index 0cf693361..51b022fe1 100644 --- a/src/StreamJsonRpc.Analyzers/GeneratorModels/Container.cs +++ b/src/StreamJsonRpc.Analyzers/GeneratorModels/Container.cs @@ -22,6 +22,11 @@ internal abstract record Container(string Name, Container? Parent = null) internal string FullName => this.Parent is null ? this.Name : $"{this.Parent.FullName}.{this.Name}"; + /// + /// Gets the first namespace in the chain of containers, starting with this one. + /// + internal Namespace? ThisOrContainingNamespace => this is Namespace ns ? ns : this.Parent?.ThisOrContainingNamespace; + internal static Container? CreateFor(INamespaceOrTypeSymbol? symbol, CancellationToken cancellationToken) { if (symbol is null or INamespaceSymbol { IsGlobalNamespace: true }) diff --git a/src/StreamJsonRpc.Analyzers/GeneratorModels/EventModel.cs b/src/StreamJsonRpc.Analyzers/GeneratorModels/EventModel.cs index ae9b0afcf..dd53bf4d3 100644 --- a/src/StreamJsonRpc.Analyzers/GeneratorModels/EventModel.cs +++ b/src/StreamJsonRpc.Analyzers/GeneratorModels/EventModel.cs @@ -5,12 +5,12 @@ namespace StreamJsonRpc.Analyzers.GeneratorModels; -internal record EventModel(string Name, string DelegateType, string EventArgsType) : FormattableModel +internal record EventModel(string DeclaringType, string Name, string DelegateType, string EventArgsType) : FormattableModel { internal override void WriteHookupStatements(SourceWriter writer) { writer.WriteLine($""" - this.JsonRpc.AddLocalRpcMethod(this.Options.EventNameTransform("{this.Name}"), this.On{this.Name}); + this.JsonRpc.AddLocalRpcMethod(this.TransformEventName("{this.Name}", typeof({this.DeclaringType})), this.On{this.Name}); """); } @@ -31,6 +31,6 @@ internal override void WriteEvents(SourceWriter writer) return null; } - return new EventModel(evt.Name, evt.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), invokeMethod.Parameters[1].Type.ToDisplayString(ProxyGenerator.FullyQualifiedWithNullableFormat)); + return new EventModel(evt.ContainingType.ToDisplayString(ProxyGenerator.FullyQualifiedWithNullableFormat), evt.Name, evt.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), invokeMethod.Parameters[1].Type.ToDisplayString(ProxyGenerator.FullyQualifiedWithNullableFormat)); } } diff --git a/src/StreamJsonRpc.Analyzers/GeneratorModels/FullModel.cs b/src/StreamJsonRpc.Analyzers/GeneratorModels/FullModel.cs index 6e9b458a9..405a83a3b 100644 --- a/src/StreamJsonRpc.Analyzers/GeneratorModels/FullModel.cs +++ b/src/StreamJsonRpc.Analyzers/GeneratorModels/FullModel.cs @@ -8,10 +8,11 @@ namespace StreamJsonRpc.Analyzers.GeneratorModels; internal record FullModel { - internal FullModel(ImmutableEquatableSet proxies, ImmutableEquatableArray attachUses, bool publicProxies, bool interceptorsEnabled) + internal FullModel(ImmutableEquatableSet proxies, ImmutableEquatableArray attachUses, ImmutableEquatableSet optionalInterfacesOrTheirPrimaries, bool interceptorsEnabled) { // Generate a proxy for attributed interfaces in this assembly, and for interfaces used by Attach methods. this.Proxies = [.. proxies.Concat(attachUses.Where(a => a.Contracts is not null).Select(a => new ProxyModel(a.Contracts!, a.ExternalProxyName))).Distinct()]; + this.OptionalInterfacesOrTheirPrimaries = optionalInterfacesOrTheirPrimaries; if (interceptorsEnabled) { @@ -21,15 +22,17 @@ group use by (use.Contracts, use.Signature, use.ExternalProxyName) into attachBy let proxy = attachByProxy.Key.Contracts is null ? null : new ProxyModel(attachByProxy.Key.Contracts, attachByProxy.Key.ExternalProxyName) select new InterceptionModel(proxy, attachByProxy.Key.Signature, [.. from attach in attachByProxy select attach.InterceptableLocation])]; } - - this.PublicProxies = publicProxies; } + internal required bool PublicRpcMarshalableInterfaceExtensions { get; init; } + internal ImmutableEquatableArray Proxies { get; } + internal ImmutableEquatableSet OptionalInterfacesOrTheirPrimaries { get; } + internal ImmutableEquatableArray Interceptions { get; } = []; - internal bool PublicProxies { get; } + internal required bool PublicProxies { get; init; } internal void GenerateSource(SourceProductionContext context) { @@ -44,6 +47,8 @@ internal void GenerateSource(SourceProductionContext context) { this.GenerateInterceptor(context, this.Interceptions); } + + this.GenerateOptionalInterfaceExtensionMethods(context); } catch (Exception) when (AnalyzerUtilities.LaunchDebugger()) { @@ -51,12 +56,94 @@ internal void GenerateSource(SourceProductionContext context) } } + private void GenerateOptionalInterfaceExtensionMethods(SourceProductionContext context) + { + SourceWriter writer = new(); + writer.WriteLine(""" + // + + #nullable enable + #pragma warning disable CS0436 // prefer local types to imported ones + + using StreamJsonRpc; + + """); + + IEnumerable> interfacesByNamespace = + from iface in this.OptionalInterfacesOrTheirPrimaries + where iface.DeclaredInThisCompilation && iface.IsFullyPartial + group iface by iface.Container?.ThisOrContainingNamespace into ifaceGroup + select ifaceGroup; + + bool nonEmptyFile = false; + foreach (IGrouping ifaceGroup in interfacesByNamespace) + { + if (ifaceGroup.Key is null) + { + WriteGroup(writer); + } + else + { + ifaceGroup.Key.WriteWithin(writer, writer => WriteGroup(writer)); + } + + void WriteGroup(SourceWriter writer) + { + bool publicClass = this.PublicRpcMarshalableInterfaceExtensions && ifaceGroup.Any(i => i.IsPublic); + writer.WriteLine($$""" + /// Extension methods for interfaces acting as optional interfaces on proxies. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{ThisAssembly.AssemblyName}}", "{{ThisAssembly.AssemblyFileVersion}}")] + """); + + if (publicClass) + { + writer.WriteLine(""" + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + """); + } + + writer.WriteLine($$""" + {{(publicClass ? "public" : "internal")}} static partial class StreamJsonRpcOptionalInterfaceAccessors + { + """); + writer.Indentation++; + + bool first = true; + foreach (InterfaceModel iface in ifaceGroup) + { + string visibility = iface.IsPublic ? "public" : "internal"; + writer.WriteLine($$""" + /// + {{visibility}} static bool Is(this {{iface.FullName}} self, global::System.Type type) => self is global::StreamJsonRpc.IClientProxy proxy ? proxy.Is(type) : type.IsAssignableFrom(self.GetType()); + /// + {{visibility}} static T? As(this {{iface.FullName}} self) where T : class => self is global::StreamJsonRpc.IClientProxy proxy ? proxy.As() : self as T; + """); + if (first) + { + writer.WriteLine(); + first = false; + } + + nonEmptyFile = true; + } + + writer.Indentation--; + writer.WriteLine("}"); + } + } + + if (nonEmptyFile) + { + context.AddSource("OptionalInterfaceExtensions.g.cs", writer.ToSourceText()); + } + } + private void GenerateInterceptor(SourceProductionContext context, ImmutableEquatableArray interceptions) { SourceWriter writer = new(); writer.WriteLine($$""" // - + #nullable enable namespace System.Runtime.CompilerServices diff --git a/src/StreamJsonRpc.Analyzers/GeneratorModels/InterfaceModel.cs b/src/StreamJsonRpc.Analyzers/GeneratorModels/InterfaceModel.cs index a085a6dc3..96b43572b 100644 --- a/src/StreamJsonRpc.Analyzers/GeneratorModels/InterfaceModel.cs +++ b/src/StreamJsonRpc.Analyzers/GeneratorModels/InterfaceModel.cs @@ -28,6 +28,8 @@ internal record InterfaceModel(string FullName, string Name, ImmutableEquatableA internal required bool DeclaredInThisCompilation { get; init; } + internal required ImmutableEquatableSet PrescribedTypeArgs { get; init; } + internal static InterfaceModel Create(INamedTypeSymbol iface, KnownSymbols symbols, bool declaredInThisCompilation, CancellationToken cancellationToken) { bool hasUnsupportedMemberTypes = false; @@ -38,6 +40,9 @@ internal static InterfaceModel Create(INamedTypeSymbol iface, KnownSymbols symbo { switch (member) { + case { IsStatic: true }: + // Ignore all static members. + break; case IMethodSymbol method when SymbolEqualityComparer.Default.Equals(method.ContainingType, symbols.IDisposable): // We don't map this special Dispose method. break; @@ -50,12 +55,25 @@ internal static InterfaceModel Create(INamedTypeSymbol iface, KnownSymbols symbo case IEventSymbol evt when EventModel.Create(evt, symbols) is EventModel evtModel: events.Add(evtModel); break; + case INamedTypeSymbol nestedType: + // We ignore these. + break; default: hasUnsupportedMemberTypes = true; break; } } + HashSet prescribedTypeArgs = []; + foreach (AttributeData attr in iface.GetAttributes()) + { + if (attr is { AttributeClass: { TypeArguments: [INamedTypeSymbol { TypeArguments: { Length: > 0 } typeArgs }] } attrClass } + && SymbolEqualityComparer.Default.Equals(attrClass.ConstructUnboundGenericType(), symbols.JsonRpcProxyAttribute)) + { + prescribedTypeArgs.Add(string.Join(", ", typeArgs.Select(ta => ta.ToDisplayString(ProxyGenerator.FullyQualifiedNoGlobalWithNullableFormat)))); + } + } + return new InterfaceModel( iface.ToDisplayString(ProxyGenerator.FullyQualifiedNoGlobalWithNullableFormat), iface.Name, @@ -68,6 +86,7 @@ [.. iface.TypeParameters.Select(tp => tp.Name)], IsPartial = iface.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(cancellationToken) is InterfaceDeclarationSyntax syntax && syntax.Modifiers.Any(SyntaxKind.PartialKeyword), IsPublic = iface.IsActuallyPublic(), DeclaredInThisCompilation = declaredInThisCompilation, + PrescribedTypeArgs = prescribedTypeArgs.ToImmutableEquatableSet(), }; } } diff --git a/src/StreamJsonRpc.Analyzers/GeneratorModels/MethodModel.cs b/src/StreamJsonRpc.Analyzers/GeneratorModels/MethodModel.cs index d1e754ad0..34df3e7a3 100644 --- a/src/StreamJsonRpc.Analyzers/GeneratorModels/MethodModel.cs +++ b/src/StreamJsonRpc.Analyzers/GeneratorModels/MethodModel.cs @@ -114,7 +114,7 @@ internal override void WriteMethods(SourceWriter writer) writer.WriteLine($$""" - {{this.ReturnType}} global::{{this.DeclaringInterfaceName}}.{{this.Name}}({{string.Join(", ", this.Parameters.Select(p => $"{p.Type} {p.Name}"))}}) + {{this.ReturnType}} {{this.DeclaringInterfaceName}}.{{this.Name}}({{string.Join(", ", this.Parameters.Select(p => $"{p.Type} {p.Name}"))}}) { """); @@ -132,7 +132,7 @@ internal override void WriteMethods(SourceWriter writer) if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("{{this.Name}}"); - string rpcMethodName = this.{{this.TransformedMethodNameFieldName}} ??= this.Options.MethodNameTransform("{{this.RpcMethodName}}"); + string rpcMethodName = this.{{this.TransformedMethodNameFieldName}} ??= this.TransformMethodName("{{this.RpcMethodName}}", typeof({{this.DeclaringInterfaceName}})); global::System.Threading.Tasks.Task{{returnTypeArg}} result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.{{namedArgsInvocationMethodName}}(rpcMethodName, {{namedArgs}}(), {{this.NamedTypesFieldName}}{{cancellationArg}}) : this.JsonRpc.{{positionalArgsInvocationMethodName}}(rpcMethodName, {{positionalArgs}}, {{this.PositionalTypesFieldName}}{{cancellationArg}}); @@ -166,28 +166,25 @@ internal override void WriteMethods(SourceWriter writer) internal static MethodModel Create(IMethodSymbol method, KnownSymbols symbols) { + AttributeData? methodShapeAttribute = method.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, symbols.MethodShapeAttribute)); + AttributeData? jsonRpcMethodAttribute = method.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, symbols.JsonRpcMethodAttribute)); + string rpcMethodName = method.Name; - if (method.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, symbols.MethodShapeAttribute)) is { } methodShapeAttribute) + + // If the method has a MethodShape attribute, prefer its name. + if (methodShapeAttribute?.NamedArguments.FirstOrDefault(kv => kv.Key == Types.MethodShapeAttribute.NameProperty).Value is { Value: string methodShapeName }) { - // If the method has a MethodShape attribute, use its name. - if (methodShapeAttribute.NamedArguments.FirstOrDefault(a => a.Key == Types.MethodShapeAttribute.NameProperty).Value.Value is string name) - { - rpcMethodName = name; - } + rpcMethodName = methodShapeName; } - if (method.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, symbols.JsonRpcMethodAttribute)) is { } rpcMethodAttribute) + // If the method has a JsonRpcMethod attribute, prefer its name above all. + if (jsonRpcMethodAttribute is { ConstructorArguments: [{ Value: string jsonRpcMethodName }, ..] }) { - // If the method has a JsonRpcMethod attribute, use its name. - if (rpcMethodAttribute.ConstructorArguments.Length > 0 && - rpcMethodAttribute.ConstructorArguments[0].Value is string name) - { - rpcMethodName = name; - } + rpcMethodName = jsonRpcMethodName; } return new MethodModel( - method.ContainingType.ToDisplayString(ProxyGenerator.FullyQualifiedNoGlobalWithNullableFormat), + method.ContainingType.ToDisplayString(ProxyGenerator.FullyQualifiedWithNullableFormat), method.Name, method.ReturnType.ToDisplayString(ProxyGenerator.FullyQualifiedWithNullableFormat), ProxyGenerator.ClassifySpecialType(method.ReturnType, symbols), diff --git a/src/StreamJsonRpc.Analyzers/GeneratorModels/ProxyModel.cs b/src/StreamJsonRpc.Analyzers/GeneratorModels/ProxyModel.cs index 2b23bac08..fa0857956 100644 --- a/src/StreamJsonRpc.Analyzers/GeneratorModels/ProxyModel.cs +++ b/src/StreamJsonRpc.Analyzers/GeneratorModels/ProxyModel.cs @@ -69,6 +69,8 @@ internal ProxyModel(ImmutableEquatableSet interfaces, string? ex internal bool HasInvalidInterfaces { get; } + internal bool HasOptionalInterfaces { get; init; } + internal void WriteInterfaceMapping(SourceWriter writer, InterfaceModel iface) { string genericTypeParameters = iface.TypeParameters.Length > 0 @@ -76,6 +78,16 @@ internal void WriteInterfaceMapping(SourceWriter writer, InterfaceModel iface) : string.Empty; writer.WriteLine($$""" [global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof({{ProxyGenerator.GenerationNamespace}}.{{this.Name}}{{this.GenericTypeDefinitionSuffix}}))] + """); + + foreach (string prescribedTypeArg in iface.PrescribedTypeArgs) + { + writer.WriteLine($$""" + [global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof({{ProxyGenerator.GenerationNamespace}}.{{this.Name}}<{{prescribedTypeArg}}>))] + """); + } + + writer.WriteLine($$""" partial interface {{iface.Name}}{{genericTypeParameters}} { } diff --git a/src/StreamJsonRpc.Analyzers/ImmutableEquatableSet.cs b/src/StreamJsonRpc.Analyzers/ImmutableEquatableSet.cs index fa70d8ec6..8bb80faec 100644 --- a/src/StreamJsonRpc.Analyzers/ImmutableEquatableSet.cs +++ b/src/StreamJsonRpc.Analyzers/ImmutableEquatableSet.cs @@ -25,7 +25,7 @@ public static class ImmutableEquatableSet /// A new instance containing the specified values. public static ImmutableEquatableSet ToImmutableEquatableSet(this IEnumerable values) where T : IEquatable - => values is ICollection { Count: 0 } ? ImmutableEquatableSet.Empty : ImmutableEquatableSet.UnsafeCreateFromHashSet(new(values)); + => values is ICollection { Count: 0 } ? ImmutableEquatableSet.Empty : ImmutableEquatableSet.UnsafeCreateFromHashSet([.. values]); /// /// Creates a new instance from the specified values. diff --git a/src/StreamJsonRpc.Analyzers/JsonRpcContractAnalyzer.cs b/src/StreamJsonRpc.Analyzers/JsonRpcContractAnalyzer.cs index ad33bc325..5672a5053 100644 --- a/src/StreamJsonRpc.Analyzers/JsonRpcContractAnalyzer.cs +++ b/src/StreamJsonRpc.Analyzers/JsonRpcContractAnalyzer.cs @@ -37,6 +37,11 @@ public class JsonRpcContractAnalyzer : DiagnosticAnalyzer /// public const string UseRpcMarshalableAttributeOnOptionalInterfacesId = "StreamJsonRpc0007"; + /// + /// Diagnostic ID for StreamJsonRpc0008: Add methods to PolyType shape for RPC contract interface. + /// + public const string GeneratePolyTypeMethodsOnRpcContractInterfaceId = "StreamJsonRpc0008"; + /// /// Diagnostic ID for StreamJsonRpc0011: RPC methods use supported return types. /// @@ -115,6 +120,18 @@ public class JsonRpcContractAnalyzer : DiagnosticAnalyzer isEnabledByDefault: true, helpLinkUri: AnalyzerUtilities.GetHelpLink(UseRpcMarshalableAttributeOnOptionalInterfacesId)); + /// + /// Diagnostic for StreamJsonRpc0008: Add methods to PolyType shape for RPC contract interface. + /// + public static readonly DiagnosticDescriptor GeneratePolyTypeMethodsOnRpcContractInterface = new( + id: GeneratePolyTypeMethodsOnRpcContractInterfaceId, + title: Strings.StreamJsonRpc0008_Title, + messageFormat: Strings.StreamJsonRpc0008_MessageFormat, + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + helpLinkUri: AnalyzerUtilities.GetHelpLink(GeneratePolyTypeMethodsOnRpcContractInterfaceId)); + /// /// Diagnostic for StreamJsonRpc0011: RPC methods use supported return types. /// @@ -195,6 +212,7 @@ public class JsonRpcContractAnalyzer : DiagnosticAnalyzer PartialInterface, RpcMarshableDisposable, UseRpcMarshalableAttributeOnOptionalInterfaces, + GeneratePolyTypeMethodsOnRpcContractInterface, UnsupportedReturnType, UnsupportedMemberType, NoGenericMethods, @@ -258,6 +276,13 @@ private static void ReportMemberDiagnostic(SymbolStartAnalysisContext context, I } } + private bool IncludesPublicMethods(AttributeData? generateShapeAttribute) + { + const int PublicInstanceMethods = 1; // MethodShapeFlags.PublicInstance + return generateShapeAttribute?.NamedArguments.FirstOrDefault(na => na.Key == "IncludeMethods") is { Value: { Kind: TypedConstantKind.Enum, Value: int v } } + && (v & PublicInstanceMethods) == PublicInstanceMethods; + } + private void InspectSymbol(SymbolStartAnalysisContext context, KnownSymbols knownSymbols, INamedTypeSymbol namedType) { AttributeData? rpcContractAttribute = @@ -271,6 +296,24 @@ private void InspectSymbol(SymbolStartAnalysisContext context, KnownSymbols know bool isRpcMarshalable = SymbolEqualityComparer.Default.Equals(rpcContractAttribute.AttributeClass, knownSymbols.RpcMarshalableAttribute); bool isCallScopedLifetime = rpcContractAttribute.NamedArguments.FirstOrDefault(a => a.Key == Types.RpcMarshalableAttribute.CallScopedLifetime).Value.Value is true; ImmutableList diagnostics = []; + Location typeLocation = namedType.Locations.FirstOrDefault() ?? Location.None; + bool hasGenericTypeParameters = namedType.TypeArguments.Any(ta => ta is ITypeParameterSymbol); + + // All RPC contracts should have shapes generated for them that include methods. + // 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)); + 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)]")); + } AttributeData[] optionalIfaceAttrs = [.. namedType.GetAttributes().Where(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, knownSymbols.RpcMarshalableOptionalInterface))]; @@ -284,7 +327,7 @@ private void InspectSymbol(SymbolStartAnalysisContext context, KnownSymbols know } } - if (this.GetNonPartialElements(namedType, context.CancellationToken) is { Count: > 0 } nonPartialElements) + if (!hasGenericTypeParameters && this.GetNonPartialElements(namedType, context.CancellationToken) is { Count: > 0 } nonPartialElements) { Location[] additionalLocations = nonPartialElements.Select(e => e.Location).ToArray(); string nonPartialElementsList = string.Join(", ", nonPartialElements.Select(e => e.Symbol.ToDisplayString(GenerationHelpers.QualifiedNameOnlyFormat))); @@ -293,19 +336,21 @@ private void InspectSymbol(SymbolStartAnalysisContext context, KnownSymbols know if (namedType.IsGenericType && !isRpcMarshalable) { - diagnostics = diagnostics.Add(Diagnostic.Create(NoGenericInterface, namedType.Locations.FirstOrDefault() ?? Location.None)); + diagnostics = diagnostics.Add(Diagnostic.Create(NoGenericInterface, typeLocation)); } if (isRpcMarshalable && !isCallScopedLifetime && !knownSymbols.IDisposable.IsAssignableFrom(namedType)) { - diagnostics = diagnostics.Add(Diagnostic.Create(RpcMarshableDisposable, namedType.Locations.FirstOrDefault(), namedType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))); + diagnostics = diagnostics.Add(Diagnostic.Create(RpcMarshableDisposable, typeLocation, namedType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))); } foreach (AttributeData optionalIfaceAttr in optionalIfaceAttrs) { if (optionalIfaceAttr.ConstructorArguments is [_, { Kind: TypedConstantKind.Type, Value: INamedTypeSymbol ifaceType }]) { - if (!ifaceType.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, knownSymbols.RpcMarshalableAttribute))) + // TODO: check for IsOptional on the attribute + AttributeData? marshalableAttribute = ifaceType.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, knownSymbols.RpcMarshalableAttribute)); + if (marshalableAttribute is null || marshalableAttribute.NamedArguments.FirstOrDefault(na => na.Key is Types.RpcMarshalableAttribute.IsOptional).Value is not { Value: true }) { diagnostics = diagnostics.Add(Diagnostic.Create( UseRpcMarshalableAttributeOnOptionalInterfaces, @@ -324,6 +369,9 @@ private void InspectSymbol(SymbolStartAnalysisContext context, KnownSymbols know switch (member) { + case { IsStatic: true }: + // Ignore all static members, as they are not part of the RPC contract. + break; case IMethodSymbol { IsGenericMethod: true } method: ReportMemberDiagnostic(context, namedType, method, location, (loc, addl, memberFormat) => Diagnostic.Create(NoGenericMethods, loc, addl, namedType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), member.ToDisplayString(memberFormat))); break; diff --git a/src/StreamJsonRpc.Analyzers/JsonRpcProxyAnalyzer.cs b/src/StreamJsonRpc.Analyzers/JsonRpcProxyAnalyzer.cs new file mode 100644 index 000000000..345e062fd --- /dev/null +++ b/src/StreamJsonRpc.Analyzers/JsonRpcProxyAnalyzer.cs @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; +using System.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace StreamJsonRpc.Analyzers; + +/// +/// Analyzes use of the JsonRpcProxyAttribute. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class JsonRpcProxyAnalyzer : DiagnosticAnalyzer +{ + /// + /// Diagnostic ID for "JsonRpcProxyAttribute<T> should be applied only to generic interfaces". + /// + public const string GenericInterfaceRequiredId = "StreamJsonRpc0030"; + + /// + /// Diagnostic ID for "JsonRpcProxyAttribute<T> type argument should be a closed instance of the applied type". + /// + public const string TypeArgIsClosedInterfaceId = "StreamJsonRpc0031"; + + /// + /// Diagnostic ID for "JsonRpcProxyAttribute<T> should be accompanied by another contract attribute". + /// + public const string ContractAttributeRequiredId = "StreamJsonRpc0032"; + + /// + /// Diagnostic for StreamJsonRpc0030: JsonRpcProxyAttribute<T> should be applied only to generic interfaces. + /// + public static readonly DiagnosticDescriptor GenericInterfaceRequired = new( + id: GenericInterfaceRequiredId, + title: Strings.StreamJsonRpc0030_Title, + messageFormat: Strings.StreamJsonRpc0030_MessageFormat, + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + helpLinkUri: AnalyzerUtilities.GetHelpLink(GenericInterfaceRequiredId)); + + /// + /// Diagnostic for StreamJsonRpc0031: JsonRpcProxyAttribute<T> type argument should be a closed instance of the applied type. + /// + public static readonly DiagnosticDescriptor TypeArgIsClosedInterface = new( + id: TypeArgIsClosedInterfaceId, + title: Strings.StreamJsonRpc0031_Title, + messageFormat: Strings.StreamJsonRpc0031_MessageFormat, + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + helpLinkUri: AnalyzerUtilities.GetHelpLink(TypeArgIsClosedInterfaceId)); + + /// + /// Diagnostic for StreamJsonRpc0032: JsonRpcProxyAttribute<T> should be accompanied by another contract attribute. + /// + public static readonly DiagnosticDescriptor ContractAttributeRequired = new( + id: ContractAttributeRequiredId, + title: Strings.StreamJsonRpc0032_Title, + messageFormat: Strings.StreamJsonRpc0032_MessageFormat, + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + helpLinkUri: AnalyzerUtilities.GetHelpLink(ContractAttributeRequiredId)); + + /// + public override ImmutableArray SupportedDiagnostics => [ + GenericInterfaceRequired, + TypeArgIsClosedInterface, + ContractAttributeRequired, + ]; + + /// + public override void Initialize(AnalysisContext context) + { + if (!Debugger.IsAttached) + { + context.EnableConcurrentExecution(); + } + + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.ReportDiagnostics); + + context.RegisterCompilationStartAction( + context => + { + if (!KnownSymbols.TryCreate(context.Compilation, out KnownSymbols? knownSymbols)) + { + return; + } + + context.RegisterSymbolAction( + context => this.InspectSymbol(context, knownSymbols, (INamedTypeSymbol)context.Symbol), + SymbolKind.NamedType); + }); + } + + private void InspectSymbol(SymbolAnalysisContext context, KnownSymbols knownSymbols, INamedTypeSymbol namedType) + { + AttributeData[] attrs = [.. namedType.GetAttributes().Where(a => a is { AttributeClass: { TypeArguments: [INamedTypeSymbol] } attrClass } && SymbolEqualityComparer.Default.Equals(attrClass.ConstructUnboundGenericType(), knownSymbols.JsonRpcProxyAttribute))]; + if (attrs is []) + { + return; + } + + if (!namedType.IsGenericType) + { + Location bestLocation = attrs.FirstOrDefault(a => a.ApplicationSyntaxReference is not null)?.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation() ?? namedType.Locations.FirstOrDefault() ?? Location.None; + context.ReportDiagnostic(Diagnostic.Create( + GenericInterfaceRequired, + bestLocation, + namedType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))); + } + + if (!namedType.GetAttributes().Any(a => + SymbolEqualityComparer.Default.Equals(a.AttributeClass, knownSymbols.JsonRpcContractAttribute) + || SymbolEqualityComparer.Default.Equals(a.AttributeClass, knownSymbols.RpcMarshalableAttribute))) + { + Location bestLocation = attrs.FirstOrDefault(a => a.ApplicationSyntaxReference is not null)?.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation() ?? namedType.Locations.FirstOrDefault() ?? Location.None; + context.ReportDiagnostic(Diagnostic.Create( + ContractAttributeRequired, + bestLocation, + namedType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))); + } + + if (!namedType.IsGenericType) + { + // The rest of the diagnostics depend on the applied type being generic. + return; + } + + INamedTypeSymbol unboundNamedType = namedType.ConstructUnboundGenericType(); + foreach (AttributeData attr in attrs) + { + if (attr.AttributeClass!.TypeArguments[0] is not INamedTypeSymbol { IsGenericType: true } || + !SymbolEqualityComparer.Default.Equals(unboundNamedType, ((INamedTypeSymbol)attr.AttributeClass!.TypeArguments[0]).ConstructUnboundGenericType())) + { + Location bestLocation = attr.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation() ?? namedType.Locations.FirstOrDefault() ?? Location.None; + context.ReportDiagnostic(Diagnostic.Create( + TypeArgIsClosedInterface, + bestLocation, + namedType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), + attr.AttributeClass!.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))); + } + } + } +} diff --git a/src/StreamJsonRpc.Analyzers/KnownSymbols.cs b/src/StreamJsonRpc.Analyzers/KnownSymbols.cs index 2ca70b97f..958704293 100644 --- a/src/StreamJsonRpc.Analyzers/KnownSymbols.cs +++ b/src/StreamJsonRpc.Analyzers/KnownSymbols.cs @@ -12,11 +12,14 @@ internal record KnownSymbols( INamedTypeSymbol? ValueTask, INamedTypeSymbol? ValueTaskOfT, INamedTypeSymbol? IAsyncEnumerableOfT, + INamedTypeSymbol? GenerateShapeAttribute, + INamedTypeSymbol? TypeShapeAttribute, INamedTypeSymbol CancellationToken, INamedTypeSymbol IDisposable, INamedTypeSymbol RpcMarshalableAttribute, INamedTypeSymbol RpcMarshalableOptionalInterface, INamedTypeSymbol JsonRpcContractAttribute, + INamedTypeSymbol JsonRpcProxyAttribute, INamedTypeSymbol JsonRpcProxyInterfaceGroupAttribute, INamedTypeSymbol ExportRpcContractProxiesAttribute, INamedTypeSymbol JsonRpcProxyMappingAttribute, @@ -32,11 +35,14 @@ internal static bool TryCreate(Compilation compilation, [NotNullWhen(true)] out INamedTypeSymbol? valueTask = compilation.GetTypeByMetadataName("System.Threading.Tasks.ValueTask"); INamedTypeSymbol? valueTaskOfT = compilation.GetTypeByMetadataName("System.Threading.Tasks.ValueTask`1"); INamedTypeSymbol? asyncEnumerableOfT = compilation.GetTypeByMetadataName("System.Collections.Generic.IAsyncEnumerable`1"); + INamedTypeSymbol? generateShapeAttribute = compilation.GetTypeByMetadataName("PolyType.GenerateShapeAttribute"); + INamedTypeSymbol? typeShapeAttribute = compilation.GetTypeByMetadataName("PolyType.TypeShapeAttribute"); INamedTypeSymbol? cancellationToken = compilation.GetTypeByMetadataName("System.Threading.CancellationToken"); INamedTypeSymbol? idisposable = compilation.GetTypeByMetadataName("System.IDisposable"); INamedTypeSymbol? rpcMarshalableAttribute = compilation.GetTypeByMetadataName(Types.RpcMarshalableAttribute.FullName); INamedTypeSymbol? rpcMarshalableOptionalInterface = compilation.GetTypeByMetadataName(Types.RpcMarshalableOptionalInterfaceAttribute.FullName); INamedTypeSymbol? rpcContractAttribute = compilation.GetTypeByMetadataName(Types.JsonRpcContractAttribute.FullName); + INamedTypeSymbol? jsonRpcProxyAttribute = compilation.GetTypeByMetadataName(Types.JsonRpcProxyAttribute.FullName)?.ConstructUnboundGenericType(); INamedTypeSymbol? jsonRpcProxyInterfaceGroupAttribute = compilation.GetTypeByMetadataName(Types.JsonRpcProxyInterfaceGroupAttribute.FullName); INamedTypeSymbol? exportRpcContractProxiesAttribute = compilation.GetTypeByMetadataName(Types.ExportRpcContractProxiesAttribute.FullName); INamedTypeSymbol? rpcProxyMappingAttribute = compilation.GetTypeByMetadataName(Types.JsonRpcProxyMappingAttribute.FullName); @@ -49,6 +55,7 @@ internal static bool TryCreate(Compilation compilation, [NotNullWhen(true)] out rpcMarshalableAttribute is null || rpcMarshalableOptionalInterface is null || rpcContractAttribute is null || + jsonRpcProxyAttribute is null || jsonRpcProxyInterfaceGroupAttribute is null || exportRpcContractProxiesAttribute is null || rpcProxyMappingAttribute is null || @@ -61,7 +68,7 @@ systemIOStream is null || return false; } - symbols = new KnownSymbols(task, taskOfT, valueTask, valueTaskOfT, asyncEnumerableOfT, cancellationToken, idisposable, rpcMarshalableAttribute, rpcMarshalableOptionalInterface, rpcContractAttribute, jsonRpcProxyInterfaceGroupAttribute, exportRpcContractProxiesAttribute, rpcProxyMappingAttribute, jsonRpcMethodAttribute, methodShapeAttribute, systemType, systemIOStream); + symbols = new KnownSymbols(task, taskOfT, valueTask, valueTaskOfT, asyncEnumerableOfT, generateShapeAttribute, typeShapeAttribute, cancellationToken, idisposable, rpcMarshalableAttribute, rpcMarshalableOptionalInterface, rpcContractAttribute, jsonRpcProxyAttribute, jsonRpcProxyInterfaceGroupAttribute, exportRpcContractProxiesAttribute, rpcProxyMappingAttribute, jsonRpcMethodAttribute, methodShapeAttribute, systemType, systemIOStream); return true; } } diff --git a/src/StreamJsonRpc.Analyzers/OptionalInterfaceTypeCheckAnalyzer.cs b/src/StreamJsonRpc.Analyzers/OptionalInterfaceTypeCheckAnalyzer.cs new file mode 100644 index 000000000..029487189 --- /dev/null +++ b/src/StreamJsonRpc.Analyzers/OptionalInterfaceTypeCheckAnalyzer.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; +using System.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace StreamJsonRpc.Analyzers; + +/// +/// An analyzer that encourages the use of IClientProxy.Is and JsonRpcExtensions.As{T}(IClientProxy) +/// over C# `is` and `as` operators for checking for RPC-implemented interfaces. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class OptionalInterfaceTypeCheckAnalyzer : DiagnosticAnalyzer +{ + /// + /// Diagnostic ID for StreamJsonRpc0050: Use IClientProxy.Is or JsonRpcExtensions.As. + /// + public const string UseIClientProxyId = "StreamJsonRpc0050"; + + /// + /// * Diagnostic for StreamJsonRpc0050: Use IClientProxy.Is or JsonRpcExtensions.As. + /// + public static readonly DiagnosticDescriptor UseIClientProxy = new( + id: UseIClientProxyId, + title: Strings.StreamJsonRpc0050_Title, + messageFormat: Strings.StreamJsonRpc0050_MessageFormat, + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + helpLinkUri: AnalyzerUtilities.GetHelpLink(UseIClientProxyId)); + + /// + public override ImmutableArray SupportedDiagnostics => [ + UseIClientProxy, + ]; + + /// + public override void Initialize(AnalysisContext context) + { + if (!Debugger.IsAttached) + { + context.EnableConcurrentExecution(); + } + + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.ReportDiagnostics); + + context.RegisterCompilationStartAction( + context => + { + if (!KnownSymbols.TryCreate(context.Compilation, out KnownSymbols? knownSymbols)) + { + return; + } + + context.RegisterOperationAction( + context => this.AnalyzeOperation(context, knownSymbols), + OperationKind.IsType, + OperationKind.Conversion); // as and cast operators. + }); + } + + private void AnalyzeOperation(OperationAnalysisContext context, KnownSymbols knownSymbols) + { + (INamedTypeSymbol? targetType, INamedTypeSymbol? sourceType) = context.Operation switch + { + IIsTypeOperation { TypeOperand: INamedTypeSymbol target, ValueOperand.Type: INamedTypeSymbol src } isTypeOperation => (target, src), + IConversionOperation { IsTryCast: true, Type: INamedTypeSymbol target, Operand.Type: INamedTypeSymbol src } conversionOperation => (target, src), // as + IConversionOperation { IsTryCast: false, IsImplicit: false, OperatorMethod: null, Type: INamedTypeSymbol target, Operand.Type: INamedTypeSymbol src } => (target, src), // cast + _ => (null, null), + }; + + if (targetType is not { TypeKind: TypeKind.Interface } || sourceType is not { TypeKind: TypeKind.Interface }) + { + return; + } + + // If either type is not an RpcMarshalable interface, bail out. + if (!sourceType.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, knownSymbols.RpcMarshalableAttribute)) || + !targetType.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, knownSymbols.RpcMarshalableAttribute))) + { + return; + } + + // If the target type isn't attributed with [RpcMarshalable(IsOptional = true)], bail out. + AttributeData? targetAttr = targetType.GetAttributes().First(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, knownSymbols.RpcMarshalableAttribute)); + if (targetAttr.NamedArguments.FirstOrDefault(kvp => kvp.Key == nameof(Types.RpcMarshalableAttribute.IsOptional)).Value.Value is not true) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + UseIClientProxy, + context.Operation.Syntax.GetLocation(), + sourceType.ToDisplayString(GenerationHelpers.QualifiedNameOnlyFormat), + targetType.ToDisplayString(GenerationHelpers.QualifiedNameOnlyFormat))); + } +} diff --git a/src/StreamJsonRpc.Analyzers/ProxyGenerator.cs b/src/StreamJsonRpc.Analyzers/ProxyGenerator.cs index ad4e83f3b..4ec1a9584 100644 --- a/src/StreamJsonRpc.Analyzers/ProxyGenerator.cs +++ b/src/StreamJsonRpc.Analyzers/ProxyGenerator.cs @@ -70,9 +70,14 @@ ImmutableEquatableArray PrepareProxy(GeneratorAttributeSyntaxContext return []; } + bool hasOptionalInterfaces = iface.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, symbols.RpcMarshalableOptionalInterface)); + IEnumerable proxies = ExpandInterfaceToGroups(iface, symbols) .Select(group => new ProxyModel( - [.. group.Select(i => InterfaceModel.Create(i, symbols, declaredInThisCompilation: true, cancellationToken))])); + [.. group.Select(i => InterfaceModel.Create(i, symbols, declaredInThisCompilation: true, cancellationToken))]) + { + HasOptionalInterfaces = hasOptionalInterfaces, + }); return [.. proxies]; } @@ -107,22 +112,33 @@ ImmutableEquatableArray PrepareProxy(GeneratorAttributeSyntaxContext IncrementalValueProvider publicProxy = context.CompilationProvider.Select((c, token) => this.AreProxiesPublic(c)); IncrementalValueProvider interceptorsEnabled = context.AnalyzerConfigOptionsProvider.Select((provider, token) => AreInterceptorsEnabled(provider.GlobalOptions)); + IncrementalValueProvider publicRpcMarshalableInterfaceExtensions = context.AnalyzerConfigOptionsProvider.Select((provider, token) => IsOptionEnabled(provider.GlobalOptions, "PublicRpcMarshalableInterfaceExtensions")); - IncrementalValueProvider fullModel = proxyProvider.Combine(attachUseProvider.Collect()).Combine(publicProxy.Combine(interceptorsEnabled)).Select( + IncrementalValueProvider fullModel = proxyProvider.Combine(attachUseProvider.Collect()).Combine(publicProxy.Combine(interceptorsEnabled.Combine(publicRpcMarshalableInterfaceExtensions))).Select( (combined, attach) => { ImmutableEquatableSet proxies = combined.Left.Left; - ImmutableArray attachUses = combined.Left.Right; + ImmutableEquatableArray attachUses = combined.Left.Right.ToImmutableEquatableArray(); + ImmutableEquatableSet optionalInterfacesOrTheirPrimaries = proxies + .Where(p => p.HasOptionalInterfaces) + .SelectMany(p => p.Interfaces) + .ToImmutableEquatableSet(); bool publicProxies = combined.Right.Left; - bool interceptorsEnabled = combined.Right.Right; - return new FullModel(proxies, attachUses.ToImmutableEquatableArray(), publicProxies, interceptorsEnabled); + (bool interceptorsEnabled, bool publicRpcMarshalableInterfaceExtensions) = combined.Right.Right; + return new FullModel(proxies, attachUses, optionalInterfacesOrTheirPrimaries, interceptorsEnabled) + { + PublicProxies = publicProxies, + PublicRpcMarshalableInterfaceExtensions = publicRpcMarshalableInterfaceExtensions, + }; }); context.RegisterSourceOutput(fullModel, (context, model) => model.GenerateSource(context)); } - internal static bool AreInterceptorsEnabled(AnalyzerConfigOptions options) - => options.TryGetValue("build_property.EnableStreamJsonRpcInterceptors", out string? value) && string.Equals(value, "true", StringComparison.OrdinalIgnoreCase); + internal static bool AreInterceptorsEnabled(AnalyzerConfigOptions options) => IsOptionEnabled(options, "EnableStreamJsonRpcInterceptors"); + + internal static bool IsOptionEnabled(AnalyzerConfigOptions options, string optionName) + => options.TryGetValue($"build_property.{optionName}", out string? value) && string.Equals(value, "true", StringComparison.OrdinalIgnoreCase); internal static (AttachSignature Signature, INamedTypeSymbol[]? Interfaces)? AnalyzeAttachInvocation(InvocationExpressionSyntax invocation, SemanticModel semanticModel, KnownSymbols symbols, CancellationToken cancellationToken) { @@ -339,17 +355,27 @@ private static bool TryGetImplementingProxy(INamedTypeSymbol[] ifaces, KnownSymb /// JsonRpcProxyInterfaceGroupAttribute is present and specifies additional interfaces, each group will include the /// primary interface followed by those interfaces. If the attribute is not present, the primary interface is /// returned as its own group. - /// The primary interface symbol to expand into groups. This symbol is always included as the first element in each - /// group. - /// A container for well-known symbols, including the JsonRpcProxyInterfaceGroupAttribute used to identify interface - /// groups. - /// An enumerable collection of interface groups, where each group is an array of INamedTypeSymbol. If no groups are - /// defined, a single group containing only the primary interface is returned. + /// + /// The primary interface symbol to expand into groups. This symbol is always included as the first element in each group. + /// + /// + /// A container for well-known symbols, including the JsonRpcProxyInterfaceGroupAttribute used to identify interface groups. + /// + /// + /// An enumerable collection of interface groups, where each group is an array of INamedTypeSymbol. If no groups are + /// defined, a single group containing only the primary interface is returned. + /// private static IEnumerable ExpandInterfaceToGroups(INamedTypeSymbol primary, KnownSymbols symbols) { bool anyGroupsDefined = false; + List optionalMarshalableInterfaces = []; foreach (AttributeData att in primary.GetAttributes()) { + if (SymbolEqualityComparer.Default.Equals(att.AttributeClass, symbols.RpcMarshalableOptionalInterface) && att.ConstructorArguments is [_, { Value: INamedTypeSymbol optionalInterface }]) + { + optionalMarshalableInterfaces.Add(optionalInterface); + } + if (!SymbolEqualityComparer.Default.Equals(att.AttributeClass, symbols.JsonRpcProxyInterfaceGroupAttribute)) { continue; @@ -366,7 +392,11 @@ private static IEnumerable ExpandInterfaceToGroups(INamedTyp if (!anyGroupsDefined) { - yield return [primary]; // No groups defined, so just return the primary interface as its own group. + // No groups defined, so just return the primary interface as its own group. + yield return [primary]; + + // And if RpcMarshalable optional interfaces were specified, add them to another group. + yield return [primary, .. optionalMarshalableInterfaces]; } } diff --git a/src/StreamJsonRpc.Analyzers/Strings.resx b/src/StreamJsonRpc.Analyzers/Strings.resx index 7585ebef4..740d1f801 100644 --- a/src/StreamJsonRpc.Analyzers/Strings.resx +++ b/src/StreamJsonRpc.Analyzers/Strings.resx @@ -175,7 +175,7 @@ Use RpcMarshalableAttribute on optional marshalable interface - RpcMarshalable optional interface {0} must be attributed with RpcMarshalableAttribute. + RpcMarshalable optional interface {0} must be attributed with [RpcMarshalableAttribute(IsOptional = true)]. Interfaces in proxy group must be attributed @@ -195,4 +195,34 @@ Proxies may only be produced for interfaces that are not open-generics, but {0} is not an interface or is an open generic. + + Add methods to PolyType shape for RPC contract interface + + + The RPC contract type '{0}' should also have the {1} applied so that it works with more formatters. + + + JsonRpcProxyAttribute<T> should be applied only to generic interfaces + + + JsonRpcProxyAttribute<T> type argument should be a closed instance of the applied type + + + JsonRpcProxyAttribute<T> should be accompanied by JsonRpcContractAttribute or RpcMarshalableAttribute. + + + JsonRpcProxyAttribute<T> should be applied only to generic interfaces, but {0} is not a generic interface. + + + JsonRpcProxyAttribute<T> type argument should be a closed instance of the applied type, but {0} has this attribute applied with {1} as a type argument. + + + JsonRpcProxyAttribute<T> should be accompanied by either JsonRpcContractAttribute or RpcMarshalableAttribute, but {0} has neither of these applied. + + + Use IClientProxy.Is or JsonRpcExtensions.As + + + 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.Analyzers/Types.cs b/src/StreamJsonRpc.Analyzers/Types.cs index 51532d373..a2e8b8e79 100644 --- a/src/StreamJsonRpc.Analyzers/Types.cs +++ b/src/StreamJsonRpc.Analyzers/Types.cs @@ -10,6 +10,8 @@ internal static class RpcMarshalableAttribute internal const string FullName = "StreamJsonRpc.RpcMarshalableAttribute"; internal const string CallScopedLifetime = "CallScopedLifetime"; + + internal const string IsOptional = "IsOptional"; } internal static class RpcMarshalableOptionalInterfaceAttribute @@ -22,6 +24,11 @@ internal static class JsonRpcContractAttribute internal const string FullName = "StreamJsonRpc.JsonRpcContractAttribute"; } + internal static class JsonRpcProxyAttribute + { + internal const string FullName = "StreamJsonRpc.JsonRpcProxyAttribute`1"; + } + internal static class JsonRpcProxyInterfaceGroupAttribute { internal const string FullName = "StreamJsonRpc.JsonRpcProxyInterfaceGroupAttribute"; diff --git a/src/StreamJsonRpc.Analyzers/buildTransitive/StreamJsonRpc.targets b/src/StreamJsonRpc.Analyzers/buildTransitive/StreamJsonRpc.targets index 631c79bb3..a6d726ede 100644 --- a/src/StreamJsonRpc.Analyzers/buildTransitive/StreamJsonRpc.targets +++ b/src/StreamJsonRpc.Analyzers/buildTransitive/StreamJsonRpc.targets @@ -3,6 +3,6 @@ $(InterceptorsNamespaces);StreamJsonRpc.Generated - + diff --git a/src/StreamJsonRpc/FormatterBase.cs b/src/StreamJsonRpc/FormatterBase.cs index aa7223e93..53194da4f 100644 --- a/src/StreamJsonRpc/FormatterBase.cs +++ b/src/StreamJsonRpc/FormatterBase.cs @@ -19,6 +19,8 @@ namespace StreamJsonRpc; /// public abstract class FormatterBase : IJsonRpcFormatterState, IJsonRpcInstanceContainer, IDisposable { + private readonly ProxyFactory proxyFactory; + private JsonRpc? rpc; /// @@ -42,7 +44,7 @@ public abstract class FormatterBase : IJsonRpcFormatterState, IJsonRpcInstanceCo private MessageFormatterEnumerableTracker? enumerableTracker; /// - /// The helper for marshaling in RPC method arguments or return values. + /// The helper for marshaling in RPC method arguments or return values. /// private MessageFormatterRpcMarshaledContextTracker? rpcMarshaledContextTracker; @@ -55,8 +57,15 @@ 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; } /// @@ -90,7 +99,7 @@ JsonRpc IJsonRpcInstanceContainer.Rpc this.rpc = value; this.formatterProgressTracker = new MessageFormatterProgressTracker(value, this); - this.rpcMarshaledContextTracker = new MessageFormatterRpcMarshaledContextTracker(value, this); + this.rpcMarshaledContextTracker = new MessageFormatterRpcMarshaledContextTracker(value, this.proxyFactory, this); this.enumerableTracker = new MessageFormatterEnumerableTracker(value, this, this.rpcMarshaledContextTracker); this.duplexPipeTracker = new MessageFormatterDuplexPipeTracker(value, this) { MultiplexingStream = this.MultiplexingStream }; } @@ -161,7 +170,7 @@ protected MessageFormatterEnumerableTracker EnumerableTracker protected JsonRpcMethodAttribute? ApplicableMethodAttributeOnDeserializingMethod { get; private set; } /// - /// Gets the helper for marshaling in RPC method arguments or return values. + /// Gets the helper for marshaling in RPC method arguments or return values. /// private protected MessageFormatterRpcMarshaledContextTracker RpcMarshaledContextTracker { diff --git a/src/StreamJsonRpc/IClientProxy.cs b/src/StreamJsonRpc/IClientProxy.cs new file mode 100644 index 000000000..a724bd5e9 --- /dev/null +++ b/src/StreamJsonRpc/IClientProxy.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace StreamJsonRpc; + +/// +/// Indicates whether an RPC marshaled object implements an interface. +/// +/// +/// +/// When casting or type checking between two -annotated interfaces, +/// it is preferable to use the or methods rather than a direct cast or traditional type check. +/// This is because the interface may be implemented by a proxy object that implements more interfaces than the marshaled object actually implements. +/// Using these methods informs the caller as to the actual interfaces that are supported by the remote object. +/// +/// +/// This interface is implemented by RPC proxies returned from and its overloads +/// or as RPC marshalable objects. +/// If proxies are generated for these interfaces by any other system, that proxy should also implement this interface to participate in similar dynamic type checking. +/// +/// +public interface IClientProxy +{ + /// + /// Gets a value indicating whether a given interface was requested for this proxy + /// explicitly (as opposed to being included as an artifact of its implementation). + /// + /// A contract interface type. + /// if the proxy was created with the specified expressly listed as needing to be implemented in the proxy; otherwise . + bool Is(Type type); +} diff --git a/src/StreamJsonRpc/IJsonRpcClientProxy.cs b/src/StreamJsonRpc/IJsonRpcClientProxy.cs index 4b639fbc7..e8ebab6d2 100644 --- a/src/StreamJsonRpc/IJsonRpcClientProxy.cs +++ b/src/StreamJsonRpc/IJsonRpcClientProxy.cs @@ -7,28 +7,10 @@ namespace StreamJsonRpc; /// Implemented by proxies returned from and its overloads /// to provide access to additional JSON-RPC functionality. /// -public interface IJsonRpcClientProxy : IDisposable +public interface IJsonRpcClientProxy : IClientProxy, IDisposable { /// /// Gets the instance behind this proxy. /// JsonRpc JsonRpc { get; } - - /// - /// Gets a value indicating whether a given interface was requested for this proxy - /// explicitly (as opposed to being included as an artifact of its implementation). - /// - /// An RPC contract interface type. - /// - /// The receiving object, cast to the requested interface if the proxy implements it and the interface was requested at proxy instantiation time; - /// otherwise . - /// - /// Typically a simple conditional cast would be sufficient to determine whether a proxy implements a given interface. - /// However when is a proxy may be returned - /// that implements extra interfaces. - /// In such cases, this method can be used to determine whether the proxy was intentionally created to implement the interface - /// or not, allowing feature testing to still happen since conditional casting might lead to false positives. - /// - T? As() - where T : class; } diff --git a/src/StreamJsonRpc/IRpcMarshaledContext.cs b/src/StreamJsonRpc/IRpcMarshaledContext.cs deleted file mode 100644 index 9e83e1a52..000000000 --- a/src/StreamJsonRpc/IRpcMarshaledContext.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace StreamJsonRpc; - -/// -/// Represents an object to be marshaled and provides check or influence its lifetime. -/// -/// The interface type whose methods are exposed for RPC invocation on the target object. -/// -/// When an object implementing this interface is disposed -/// the marshaling relationship is immediately terminated. -/// This releases resources allocated to facilitating the marshaling of the object -/// and prevents any further invocations of the object by the remote party. -/// If the underlying object implements then its -/// method is also invoked. -/// -/// -/// This type is an interface rather than a class so that users can enjoy covariance on the generic type parameter. -/// -internal interface IRpcMarshaledContext : IDisposableObservable - where T : class -{ - /// - /// Occurs when the marshalling relationship is released, - /// whether by a local call to - /// or at the remote party's request. - /// - event EventHandler? Disposed; - - /// - /// Gets the marshalable proxy that should be used in the RPC message. - /// - T Proxy { get; } - - /// - /// Gets the to associate with this object when it becomes a RPC target. - /// - JsonRpcTargetOptions JsonRpcTargetOptions { get; } -} diff --git a/src/StreamJsonRpc/JsonMessageFormatter.cs b/src/StreamJsonRpc/JsonMessageFormatter.cs index 61cd70155..78a8e5c40 100644 --- a/src/StreamJsonRpc/JsonMessageFormatter.cs +++ b/src/StreamJsonRpc/JsonMessageFormatter.cs @@ -571,7 +571,7 @@ private JTokenWriter CreateJTokenWriter() private bool TryGetMarshaledJsonConverter(Type type, [NotNullWhen(true)] out RpcMarshalableConverter? converter) { - if (MessageFormatterRpcMarshaledContextTracker.TryGetMarshalOptionsForType(type, out JsonRpcProxyOptions? proxyOptions, out JsonRpcTargetOptions? targetOptions, out RpcMarshalableAttribute? rpcMarshalableAttribute)) + if (MessageFormatterRpcMarshaledContextTracker.TryGetMarshalOptionsForType(type, JsonRpcProxyOptions.Default, out JsonRpcProxyOptions? proxyOptions, out JsonRpcTargetOptions? targetOptions, out RpcMarshalableAttribute? rpcMarshalableAttribute)) { converter = new RpcMarshalableConverter(type, this, proxyOptions, targetOptions, rpcMarshalableAttribute); return true; @@ -1279,7 +1279,8 @@ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer } else { - MessageFormatterRpcMarshaledContextTracker.MarshalToken token = jsonMessageFormatter.RpcMarshaledContextTracker.GetToken(value, targetOptions, interfaceType, rpcMarshalableAttribute); + RpcTargetMetadata mapping = RpcTargetMetadata.FromInterface(interfaceType); + MessageFormatterRpcMarshaledContextTracker.MarshalToken token = jsonMessageFormatter.RpcMarshaledContextTracker.GetToken(value, targetOptions, mapping, rpcMarshalableAttribute); serializer.Serialize(writer, token); } } diff --git a/src/StreamJsonRpc/JsonRpc.cs b/src/StreamJsonRpc/JsonRpc.cs index 9e761616a..f43781545 100644 --- a/src/StreamJsonRpc/JsonRpc.cs +++ b/src/StreamJsonRpc/JsonRpc.cs @@ -32,8 +32,6 @@ public class JsonRpc : IDisposableObservable, IJsonRpcFormatterCallbacks, IJsonR /// private const string JoinableTaskTokenHeaderName = "joinableTaskToken"; - private static readonly MethodInfo MarshalWithControlledLifetimeOpenGenericMethodInfo = typeof(JsonRpc).GetMethod(nameof(MarshalWithControlledLifetimeOpen), BindingFlags.Static | BindingFlags.NonPublic)!; - /// /// A singleton error object that can be returned by in error cases /// for requests that are actually notifications and thus the error will be dropped. @@ -1367,39 +1365,23 @@ public void Dispose() /// /// Creates a marshallable proxy for a given object that may be sent over RPC such that the receiving side can invoke methods on the given object. /// - /// - /// The interface type implemented by the that defines the members to expose over RPC. - /// - /// The object to be exposed over RPC. - /// + /// + /// + /// /// A lifetime controlling wrapper around a new proxy value. /// /// /// Use for a simpler lifetime model when the object should only be marshaled within the scope of a single RPC call. /// /// - internal static IRpcMarshaledContext MarshalWithControlledLifetimeOpen(T marshaledObject, JsonRpcTargetOptions options) - where T : class - { - return new RpcMarshaledContext(marshaledObject, options); - } + internal static RpcMarshaledContext MarshalWithControlledLifetime(Type interfaceType, object marshaledObject, JsonRpcTargetOptions options) + => new RpcMarshaledContext(interfaceType, marshaledObject, options); - /// - /// - /// - /// - [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] - [UnconditionalSuppressMessage("Trimming", "IL2060", Justification = "The generic method we construct has no dynamic member access requirements.")] - internal static IRpcMarshaledContext MarshalWithControlledLifetime(Type interfaceType, object marshaledObject, JsonRpcTargetOptions options) - { - return (IRpcMarshaledContext)MarshalWithControlledLifetimeOpenGenericMethodInfo.MakeGenericMethod(interfaceType).Invoke(null, new object?[] { marshaledObject, options })!; - } - - /// + /// /// A proxy value that may be used within an RPC argument so the RPC server may call back into the object on the RPC client. /// /// - /// Use for greater control and flexibility around lifetime of the proxy. + /// Use for greater control and flexibility around lifetime of the proxy. /// This is required when the value is returned from an RPC method or when it is used within an RPC argument and must outlive that RPC invocation. /// /// @@ -1433,10 +1415,10 @@ internal IJsonRpcClientProxyInternal CreateProxy(in ProxyInputs proxyInputs) if (proxyInputs.Options?.ProxySource is JsonRpcProxyOptions.ProxyImplementation.AlwaysSourceGenerated) { - throw new NotImplementedException("No source generated proxy is available for the requested interface(s), and dynamic proxies are forbidden by the options."); + throw proxyInputs.CreateNoSourceGeneratedProxyException(); } - TypeInfo proxyType = ProxyGeneration.Get(proxyInputs.ContractInterface, proxyInputs.AdditionalContractInterfaces.Span, proxyInputs.ImplementedOptionalInterfaces.Span); + TypeInfo proxyType = ProxyGeneration.Get(proxyInputs); return (IJsonRpcClientProxyInternal)Activator.CreateInstance( proxyType.AsType(), this, @@ -1485,6 +1467,9 @@ internal void AddLocalRpcMethod(MethodInfo handler, object? target, JsonRpcMetho return this.rpcTargetInfo.AddLocalRpcTarget(mapping, target, options, requestRevertOption); } + internal RpcTargetInfo.RevertAddLocalRpcTarget? AddLocalRpcTargetInternal(RpcTargetMetadata exposingMembersOn, object target, JsonRpcTargetOptions? options, bool requestRevertOption) + => this.rpcTargetInfo.AddLocalRpcTarget(exposingMembersOn, target, options ?? JsonRpcTargetOptions.Default, requestRevertOption); + /// /// Adds a new RPC interface to an existing target registering additional RPC methods. /// diff --git a/src/StreamJsonRpc/JsonRpcExtensions.cs b/src/StreamJsonRpc/JsonRpcExtensions.cs index b51997d42..cdf3ca2d0 100644 --- a/src/StreamJsonRpc/JsonRpcExtensions.cs +++ b/src/StreamJsonRpc/JsonRpcExtensions.cs @@ -21,6 +21,23 @@ private interface IRpcEnumerable Task PrefetchAsync(int count, CancellationToken cancellationToken); } + /// + /// Casts the proxy to the requested interface type, if it (intentionally) implements it. + /// + /// + /// + /// The receiving object, cast to the requested interface if the proxy implements it and the interface was requested at proxy instantiation time; + /// otherwise . + /// + /// Typically a simple conditional cast would be sufficient to determine whether a proxy implements a given interface. + /// However when is a proxy may be returned + /// that implements extra interfaces. + /// In such cases, this method can be used to determine whether the proxy was intentionally created to implement the interface + /// or not, allowing feature testing to still happen since conditional casting might lead to false positives. + /// + public static T? As(this IClientProxy proxy) + where T : class => Requires.NotNull(proxy).Is(typeof(T)) ? (T)(object)proxy : null; + #pragma warning disable VSTHRD200 // Use "Async" suffix in names of methods that return an awaitable type. /// /// Decorates an with settings that customize how StreamJsonRpc will send its items to the remote party. diff --git a/src/StreamJsonRpc/JsonRpcProxyAttribute.cs b/src/StreamJsonRpc/JsonRpcProxyAttribute.cs new file mode 100644 index 000000000..5bbb7d595 --- /dev/null +++ b/src/StreamJsonRpc/JsonRpcProxyAttribute.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Diagnostics; + +namespace StreamJsonRpc; + +/// +/// Applied to an open generic interface that is also annotated with or +/// to ensure that a proxy of a particular closed generic type is available at runtime. +/// +/// +/// The closed generic instance of the applied interface. For example if this attribute is applied to IMyRpc<T> then this type argument might be IMyRpc<int>. +/// Note it should be the entire closed interface type, not just the type argument. +/// +/// +/// +/// Applying this attribute requires at least C# language version 11.0. +/// Projects target lesser runtimes (e.g., .NET Framework) should set the LangVersion property to 11.0 or higher. +/// +/// +[AttributeUsage(AttributeTargets.Interface, AllowMultiple = true)] +[Conditional("NEVER")] // This attribute is only used at compile time to generate code, and .NET Framework fails on generic attributes, so don't let it survive compilation. +public class JsonRpcProxyAttribute : Attribute +{ +} diff --git a/src/StreamJsonRpc/JsonRpcProxyOptions.cs b/src/StreamJsonRpc/JsonRpcProxyOptions.cs index 8ede04e94..43ff028e9 100644 --- a/src/StreamJsonRpc/JsonRpcProxyOptions.cs +++ b/src/StreamJsonRpc/JsonRpcProxyOptions.cs @@ -154,7 +154,8 @@ public bool ServerRequiresNamedArguments /// Does not apply when is set to . /// /// - /// Code that uses the proxy and wants to do feature testing should use + /// Code that uses the proxy and wants to do feature testing should use + /// or /// instead of conditional casts to avoid false positives when this property is . /// /// @@ -212,5 +213,5 @@ internal Action? OnProxyConstructed } } - private bool IsFrozen { get; init; } + internal bool IsFrozen { get; init; } } diff --git a/src/StreamJsonRpc/MessagePackFormatter.cs b/src/StreamJsonRpc/MessagePackFormatter.cs index bc92b2eb6..dc101e142 100644 --- a/src/StreamJsonRpc/MessagePackFormatter.cs +++ b/src/StreamJsonRpc/MessagePackFormatter.cs @@ -1173,7 +1173,7 @@ internal RpcMarshalableResolver(MessagePackFormatter formatter) } } - if (MessageFormatterRpcMarshaledContextTracker.TryGetMarshalOptionsForType(typeof(T), out JsonRpcProxyOptions? proxyOptions, out JsonRpcTargetOptions? targetOptions, out RpcMarshalableAttribute? attribute)) + if (MessageFormatterRpcMarshaledContextTracker.TryGetMarshalOptionsForType(typeof(T), JsonRpcProxyOptions.Default, out JsonRpcProxyOptions? proxyOptions, out JsonRpcTargetOptions? targetOptions, out RpcMarshalableAttribute? attribute)) { object formatter = Activator.CreateInstance( typeof(RpcMarshalableFormatter<>).MakeGenericType(typeof(T)), @@ -1218,7 +1218,8 @@ public void Serialize(ref MessagePackWriter writer, T? value, MessagePackSeriali } else { - MessageFormatterRpcMarshaledContextTracker.MarshalToken token = messagePackFormatter.RpcMarshaledContextTracker.GetToken(value, targetOptions, typeof(T), rpcMarshalableAttribute); + RpcTargetMetadata mapping = RpcTargetMetadata.FromInterface(typeof(T)); + MessageFormatterRpcMarshaledContextTracker.MarshalToken token = messagePackFormatter.RpcMarshaledContextTracker.GetToken(value, targetOptions, mapping, rpcMarshalableAttribute); MessagePackSerializer.Serialize(ref writer, token, options); } } diff --git a/src/StreamJsonRpc/NamedArgs.cs b/src/StreamJsonRpc/NamedArgs.cs index 0dd5cbadf..6849f298e 100644 --- a/src/StreamJsonRpc/NamedArgs.cs +++ b/src/StreamJsonRpc/NamedArgs.cs @@ -86,8 +86,12 @@ internal NamedArgs(IReadOnlyDictionary declaredArgumentTypes, IRea throw new ArgumentException("The object type must not be System.Object.", nameof(objectType)); } - (IReadOnlyDictionary types, IReadOnlyDictionary> readers) = Arguments.GetOrAdd(objectType, AnalyzeMembers); - return new NamedArgs(types, readers, namedArgsObject); + if (!Arguments.TryGetValue(objectType, out (IReadOnlyDictionary Types, IReadOnlyDictionary> Readers) result)) + { + result = Arguments.GetOrAdd(objectType, AnalyzeMembers(objectType)); + } + + return new NamedArgs(result.Types, result.Readers, namedArgsObject); } bool IReadOnlyDictionary.ContainsKey(string key) => this.readers.ContainsKey(key); diff --git a/src/StreamJsonRpc/NerdbankMessagePackFormatter.RpcMarshalableConverter.cs b/src/StreamJsonRpc/NerdbankMessagePackFormatter.RpcMarshalableConverter.cs index 2f5217b13..3ae56cd57 100644 --- a/src/StreamJsonRpc/NerdbankMessagePackFormatter.RpcMarshalableConverter.cs +++ b/src/StreamJsonRpc/NerdbankMessagePackFormatter.RpcMarshalableConverter.cs @@ -1,6 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Nodes; +using Nerdbank.MessagePack; +using PolyType.Abstractions; +using StreamJsonRpc.Reflection; + namespace StreamJsonRpc; /// @@ -8,9 +14,8 @@ namespace StreamJsonRpc; /// public partial class NerdbankMessagePackFormatter { -#if NBMSGPACK_MARSHALING_SUPPORT - private class RpcMarshalableConverter( + ITypeShape shape, JsonRpcProxyOptions proxyOptions, JsonRpcTargetOptions targetOptions, RpcMarshalableAttribute rpcMarshalableAttribute) : MessagePackConverter @@ -29,7 +34,7 @@ private class RpcMarshalableConverter( #pragma warning restore NBMsgPack030 // Converters should not call top-level `MessagePackSerializer` methods return token.HasValue - ? (T?)formatter.RpcMarshaledContextTracker.GetObject(typeof(T), token, proxyOptions) + ? (T?)formatter.RpcMarshaledContextTracker.GetObject(typeof(T), token, proxyOptions, shape) : default; } @@ -46,12 +51,12 @@ public override void Write(ref MessagePackWriter writer, in T? value, Serializat } else { - MessageFormatterRpcMarshaledContextTracker.MarshalToken token = formatter.RpcMarshaledContextTracker.GetToken(value, targetOptions, typeof(T), rpcMarshalableAttribute); + RpcTargetMetadata targetMetadata = RpcTargetMetadata.FromShape(shape); + MessageFormatterRpcMarshaledContextTracker.MarshalToken token = formatter.RpcMarshaledContextTracker.GetToken(value, targetOptions, targetMetadata, rpcMarshalableAttribute); context.GetConverter(Witness.ShapeProvider).Write(ref writer, token, context); } } public override JsonObject? GetJsonSchema(JsonSchemaContext context, ITypeShape typeShape) => null; } -#endif } diff --git a/src/StreamJsonRpc/NerdbankMessagePackFormatter.cs b/src/StreamJsonRpc/NerdbankMessagePackFormatter.cs index d7f9d0ec8..a442947bf 100644 --- a/src/StreamJsonRpc/NerdbankMessagePackFormatter.cs +++ b/src/StreamJsonRpc/NerdbankMessagePackFormatter.cs @@ -31,10 +31,6 @@ namespace StreamJsonRpc; /// This formatter prioritizes being trim and NativeAOT safe. As such, it uses instead of to load exception types to be deserialized. /// This trim-friendly method should be overridden to return types that are particularly interesting to the application. /// -/// -/// -/// This formatter does not support general marshalable objects yet. -/// /// public partial class NerdbankMessagePackFormatter : FormatterBase, IJsonRpcMessageFormatter, IJsonRpcFormatterTracingCallbacks, IJsonRpcMessageFactory { @@ -60,9 +56,7 @@ public partial class NerdbankMessagePackFormatter : FormatterBase, IJsonRpcMessa ConverterFactories = [ConverterFactory.Instance], Converters = [ -#if NBMSGPACK_MARSHALING_SUPPORT - GetRpcMarshalableConverter(), -#endif + GetRpcMarshalableConverter(Witness.ShapeProvider.Resolve()), PipeConverters.PipeReaderConverter.DefaultInstance, PipeConverters.PipeWriterConverter.DefaultInstance, PipeConverters.DuplexPipeConverter.DefaultInstance, @@ -75,6 +69,8 @@ public partial class NerdbankMessagePackFormatter : FormatterBase, IJsonRpcMessa ], }.WithObjectConverter(); + private static readonly JsonRpcProxyOptions DefaultRpcMarshalableProxyOptions = new JsonRpcProxyOptions(JsonRpcProxyOptions.Default) { AcceptProxyWithExtraInterfaces = true, IsFrozen = true }; + /// /// The serializer context to use for top-level RPC messages. /// @@ -102,6 +98,7 @@ 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,22 +209,21 @@ void IJsonRpcFormatterTracingCallbacks.OnSerializationComplete(JsonRpcMessage me } } -#if NBMSGPACK_MARSHALING_SUPPORT - internal static MessagePackConverter GetRpcMarshalableConverter() + private static MessagePackConverter GetRpcMarshalableConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] T>(ITypeShape shape) where T : class { if (MessageFormatterRpcMarshaledContextTracker.TryGetMarshalOptionsForType( typeof(T), + DefaultRpcMarshalableProxyOptions, out JsonRpcProxyOptions? proxyOptions, out JsonRpcTargetOptions? targetOptions, out RpcMarshalableAttribute? attribute)) { - return new RpcMarshalableConverter(proxyOptions, targetOptions, attribute); + return new RpcMarshalableConverter(shape, proxyOptions, targetOptions, attribute); } throw new NotSupportedException($"Type '{typeof(T).FullName}' is not supported for RPC Marshaling."); } -#endif /// /// Reads a string with an optimized path for the value "2.0". @@ -1287,9 +1283,7 @@ private ConverterFactory() => MessageFormatterProgressTracker.CanDeserialize(typeof(T)) || MessageFormatterProgressTracker.CanSerialize(typeof(T)) ? new ProgressConverter() : TrackerHelpers.IsIAsyncEnumerable(typeof(T)) ? ActivateAssociatedType>(shape, typeof(AsyncEnumerableConverter<>)) : TrackerHelpers.FindIAsyncEnumerableInterfaceImplementedBy(typeof(T)) is Type iface ? ActivateAssociatedType>(shape, typeof(AsyncEnumerableConverter<>)) : -#if NBMSGPACK_MARSHALING_SUPPORT - MessageFormatterRpcMarshaledContextTracker.TryGetMarshalOptionsForType(typeof(T), out JsonRpcProxyOptions? proxyOptions, out JsonRpcTargetOptions? targetOptions, out RpcMarshalableAttribute? attribute) ? new RpcMarshalableConverter(proxyOptions, targetOptions, attribute) : -#endif + MessageFormatterRpcMarshaledContextTracker.TryGetMarshalOptionsForType(shape, DefaultRpcMarshalableProxyOptions, out JsonRpcProxyOptions? proxyOptions, out JsonRpcTargetOptions? targetOptions, out RpcMarshalableAttribute? attribute) ? new RpcMarshalableConverter(shape, proxyOptions, targetOptions, attribute) : typeof(Exception).IsAssignableFrom(typeof(T)) ? new ExceptionConverter() : null; } @@ -1341,6 +1335,7 @@ private NonDefaultConstructorVisitor() [GenerateShapeFor] [GenerateShapeFor] [GenerateShapeFor] + [GenerateShapeFor] [GenerateShapeFor] [GenerateShapeFor] private partial class Witness; diff --git a/src/StreamJsonRpc/ProxyGeneration.cs b/src/StreamJsonRpc/ProxyGeneration.cs index e76014184..1a0cdb255 100644 --- a/src/StreamJsonRpc/ProxyGeneration.cs +++ b/src/StreamJsonRpc/ProxyGeneration.cs @@ -10,6 +10,7 @@ using System.Runtime.Loader; #endif using Microsoft.VisualStudio.Threading; +using StreamJsonRpc.Reflection; using CodeGenHelpers = StreamJsonRpc.Reflection.CodeGenHelpers; // Uncomment the SaveAssembly symbol and run one test to save the generated DLL for inspection in ILSpy as part of debugging. @@ -29,6 +30,8 @@ internal static class ProxyGeneration #endif private static readonly object BuilderLock = new object(); private static readonly AssemblyName ProxyAssemblyName = new AssemblyName(string.Format(CultureInfo.InvariantCulture, "StreamJsonRpc_Proxies_{0}", Guid.NewGuid())); + private static readonly MethodInfo GetTypeMethod = typeof(object).GetRuntimeMethod(nameof(object.GetType), Type.EmptyTypes)!; + private static readonly MethodInfo IsAssignableFromMethod = typeof(Type).GetRuntimeMethod(nameof(Type.IsAssignableFrom), [typeof(Type)])!; private static readonly MethodInfo DelegateCombineMethod = typeof(Delegate).GetRuntimeMethod(nameof(Delegate.Combine), new Type[] { typeof(Delegate), typeof(Delegate) })!; private static readonly MethodInfo DelegateRemoveMethod = typeof(Delegate).GetRuntimeMethod(nameof(Delegate.Remove), new Type[] { typeof(Delegate), typeof(Delegate) })!; private static readonly MethodInfo ActionInvokeMethod = typeof(Action).GetRuntimeMethod(nameof(Action.Invoke), Type.EmptyTypes)!; @@ -59,23 +62,13 @@ internal static class ProxyGeneration /// /// Gets a dynamically generated type that implements a given interface in terms of a instance. /// - /// - /// The interface that describes the RPC contract, and that the client proxy should implement. - /// - /// - /// An optional list of additional interfaces that the client proxy should implement without the name transformation or event limitations - /// involved with . - /// This set should have an empty intersection with . - /// - /// - /// Additional marshalable interfaces that the client proxy should implement. - /// Methods on these interfaces are invoked using a special name transformation that includes an integer code, - /// ensuring that methods do not suffer from name collisions across interfaces. - /// + /// Inputs into the proxy to create. /// The generated type. - internal static TypeInfo Get(Type contractInterface, ReadOnlySpan additionalContractInterfaces, ReadOnlySpan<(Type Type, int Code)> implementedOptionalInterfaces) + internal static TypeInfo Get(ProxyInputs inputs) { - Requires.NotNull(contractInterface, nameof(contractInterface)); + Type contractInterface = inputs.ContractInterface; + 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. @@ -102,20 +95,7 @@ internal static TypeInfo Get(Type contractInterface, ReadOnlySpan addition } Type[] contractInterfaces = [contractInterface, .. additionalContractInterfaces]; - List<(TypeInfo Type, int? Code)> rpcInterfaces = new(1 + additionalContractInterfaces.Length + implementedOptionalInterfaces.Length); - rpcInterfaces.Add((contractInterface.GetTypeInfo(), null)); - foreach (Type addl in additionalContractInterfaces) - { - rpcInterfaces.Add((addl.GetTypeInfo(), null)); - } - - foreach ((Type type, int code) in implementedOptionalInterfaces) - { - rpcInterfaces.Add((type.GetTypeInfo(), code)); - } - - // Rpc interfaces must be sorted so that we implement methods from base interfaces before those from their derivations. - SortRpcInterfaces(rpcInterfaces); + IList<(Type Type, int? Code)> rpcInterfaces = GetSortedInterfaceAndCodes(inputs); // For ALC selection reasons, it's vital that the *user's* selected interfaces come *before* our own supporting interfaces. // If the order is incorrect, type resolution may fail or the wrong AssemblyLoadContext (ALC) may be selected, @@ -272,25 +252,23 @@ internal static TypeInfo Get(Type contractInterface, ReadOnlySpan addition jsonRpcProperty.SetGetMethod(jsonRpcPropertyGetter); } - // IJsonRpcClientProxy.As method + // IJsonRpcClientProxy.Is method { MethodBuilder asMethod = proxyTypeBuilder.DefineMethod( - nameof(IJsonRpcClientProxy.As), + nameof(IJsonRpcClientProxy.Is), MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Final | MethodAttributes.NewSlot | MethodAttributes.Virtual, - null, - Type.EmptyTypes); - GenericTypeParameterBuilder typeArgBuilder = asMethod.DefineGenericParameters("T")[0]; - typeArgBuilder.SetGenericParameterAttributes(GenericParameterAttributes.ReferenceTypeConstraint); - asMethod.SetReturnType(typeArgBuilder); + typeof(bool), + [typeof(Type)]); ILGenerator il = asMethod.GetILGenerator(); - // return this as T; + // return arg1.IsAssignableFrom(this.GetType()); + il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, typeArgBuilder); - il.Emit(OpCodes.Unbox_Any, typeArgBuilder); + il.Emit(OpCodes.Call, GetTypeMethod); + il.Emit(OpCodes.Callvirt, IsAssignableFromMethod); il.Emit(OpCodes.Ret); - proxyTypeBuilder.DefineMethodOverride(asMethod, typeof(IJsonRpcClientProxy).GetTypeInfo().GetDeclaredMethod(nameof(IJsonRpcClientProxy.As))!); + proxyTypeBuilder.DefineMethodOverride(asMethod, typeof(IClientProxy).GetTypeInfo().GetDeclaredMethod(nameof(IClientProxy.Is))!); } IEnumerable invokeWithCancellationAsyncMethodInfos = typeof(JsonRpc).GetTypeInfo().DeclaredMethods.Where(m => m.Name == nameof(JsonRpc.InvokeWithCancellationAsync)); @@ -334,8 +312,8 @@ internal static TypeInfo Get(Type contractInterface, ReadOnlySpan addition string rpcMethodName = name; if (rpcInterfaceCode.HasValue) { - methodName = $"{rpcInterfaceCode.GetValueOrDefault()}.{method.Name}"; - rpcMethodName = $"{rpcInterfaceCode.GetValueOrDefault()}.{rpcMethodName}"; + methodName = $"{rpcInterfaceCode}.{methodName}"; + rpcMethodName = $"{rpcInterfaceCode}.{rpcMethodName}"; } ParameterInfo[] methodParameters = method.GetParameters(); @@ -493,40 +471,6 @@ void CompleteCall(MethodInfo invokingMethod) return generatedType; } - /// - /// Sorts so that: - /// - /// interfaces that are extending a lesser number of other interfaces in come first; - /// interfaces extending the same number of other interfaces in , are ordered by optional interface code; - /// where a code comes first. - /// - /// - /// The list of RPC interfaces to be sorted. - private static void SortRpcInterfaces(List<(TypeInfo Type, int? Code)> list) - { - (TypeInfo Type, int? Code, int InheritanceWeight)[] weightedList - = list.Select(i => (i.Type, i.Code, list.Count(i2 => i2.Type.IsAssignableFrom(i.Type)))).ToArray(); - Array.Sort(weightedList, CompareRpcInterfaces); - - for (int i = 0; i < weightedList.Length; i++) - { - list[i] = (weightedList[i].Type, weightedList[i].Code); - } - - int CompareRpcInterfaces((TypeInfo Type, int? Code, int InheritanceWeight) a, (TypeInfo Type, int? Code, int InheritanceWeight) b) - { - int weightComparison = a.InheritanceWeight.CompareTo(b.InheritanceWeight); - return (weightComparison, a.Code, b.Code) switch - { - (_, _, _) when weightComparison != 0 => weightComparison, - (_, null, null) => 0, - (_, null, _) => -1, - (_, _, null) => 1, - (_, _, _) => a.Code.Value.CompareTo(b.Code.Value), - }; - } - } - private static void EmitRaiseCallEvent(ILGenerator il, FieldBuilder eventHandlerField, string methodName) { Label endOfSubroutine = il.DefineLabel(); @@ -1032,6 +976,26 @@ private static IEnumerable FindAllOnThisAndOtherInterfaces(IEnumerable GetSortedInterfaceAndCodes(ProxyInputs inputs) + { + List<(Type Type, int? Code)> rpcInterfaces = new(1 + inputs.AdditionalContractInterfaces.Length + inputs.ImplementedOptionalInterfaces.Length); + rpcInterfaces.Add((inputs.ContractInterface, null)); + foreach (Type addl in inputs.AdditionalContractInterfaces.Span) + { + rpcInterfaces.Add((addl, null)); + } + + foreach ((Type type, int code) in inputs.ImplementedOptionalInterfaces.Span) + { + rpcInterfaces.Add((type, code)); + } + + // Rpc interfaces must be sorted so that we implement methods from base interfaces before those from their derivations. + ProxyInputs.SortRpcInterfaces(rpcInterfaces); + + return rpcInterfaces; + } + /// /// Dictionary key for . /// diff --git a/src/StreamJsonRpc/Reflection/MessageFormatterRpcMarshaledContextTracker.cs b/src/StreamJsonRpc/Reflection/MessageFormatterRpcMarshaledContextTracker.cs index 574493631..84969d1bb 100644 --- a/src/StreamJsonRpc/Reflection/MessageFormatterRpcMarshaledContextTracker.cs +++ b/src/StreamJsonRpc/Reflection/MessageFormatterRpcMarshaledContextTracker.cs @@ -10,6 +10,7 @@ using System.Runtime.Serialization; using Microsoft.VisualStudio.Threading; using PolyType; +using PolyType.Abstractions; using static System.FormattableString; using STJ = System.Text.Json.Serialization; @@ -51,14 +52,15 @@ internal partial class MessageFormatterRpcMarshaledContextTracker new RpcMarshalableAttribute()), ]; - private static readonly ConcurrentDictionary MarshaledTypes = new(); - private static readonly (JsonRpcProxyOptions ProxyOptions, JsonRpcTargetOptions TargetOptions) RpcMarshalableInterfaceDefaultOptions = (new JsonRpcProxyOptions(), new JsonRpcTargetOptions { NotifyClientOfEvents = false, DisposeOnDisconnect = true }); + private static readonly ConcurrentDictionary MarshaledTypes = new(); + private static readonly JsonRpcTargetOptions RpcMarshalableInterfaceDefaultTargetOptions = new() { NotifyClientOfEvents = false, DisposeOnDisconnect = true }; private static readonly MethodInfo ReleaseMarshaledObjectMethodInfo = typeof(MessageFormatterRpcMarshaledContextTracker).GetMethod(nameof(ReleaseMarshaledObject), BindingFlags.NonPublic | BindingFlags.Instance)!; private static readonly ConcurrentDictionary MarshalableOptionalInterfaces = new ConcurrentDictionary(); - private readonly Dictionary Context, IDisposable Revert)> marshaledObjects = new Dictionary Context, IDisposable Revert)>(); + private readonly Dictionary marshaledObjects = new Dictionary(); private readonly JsonRpc jsonRpc; private readonly IJsonRpcFormatterState formatterState; + private readonly ProxyFactory proxyFactory; private long nextUniqueHandle; /// @@ -71,9 +73,10 @@ internal partial class MessageFormatterRpcMarshaledContextTracker /// private ImmutableDictionary> outboundRequestIdMarshalMap = ImmutableDictionary>.Empty; - internal MessageFormatterRpcMarshaledContextTracker(JsonRpc jsonRpc, IJsonRpcFormatterState formatterState) + internal MessageFormatterRpcMarshaledContextTracker(JsonRpc jsonRpc, ProxyFactory proxyFactory, IJsonRpcFormatterState formatterState) { this.jsonRpc = jsonRpc; + this.proxyFactory = proxyFactory; this.formatterState = formatterState; this.jsonRpc.AddLocalRpcMethod("$/releaseMarshaledObject", ReleaseMarshaledObjectMethodInfo, this); @@ -93,51 +96,67 @@ private enum MarshalMode MarshallingRealObject = 1, } - internal static bool TryGetMarshalOptionsForType( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] Type type, + internal static bool TryGetMarshalOptionsForType( + ITypeShape typeShape, + JsonRpcProxyOptions defaultProxyOptions, [NotNullWhen(true)] out JsonRpcProxyOptions? proxyOptions, [NotNullWhen(true)] out JsonRpcTargetOptions? targetOptions, [NotNullWhen(true)] out RpcMarshalableAttribute? rpcMarshalableAttribute) { - proxyOptions = null; - targetOptions = null; - rpcMarshalableAttribute = null; - if (type.IsInterface is false) + if (TryGetMarshalOptionsForTypeHelper(typeShape.Type, defaultProxyOptions, out proxyOptions, out targetOptions, out rpcMarshalableAttribute)) { - return false; + return true; } - if (MarshaledTypes.TryGetValue(type, out (JsonRpcProxyOptions ProxyOptions, JsonRpcTargetOptions TargetOptions, RpcMarshalableAttribute Attribute) options)) + if (typeShape.Type.GetCustomAttribute() is RpcMarshalableAttribute marshalableAttribute) { - proxyOptions = options.ProxyOptions; - targetOptions = options.TargetOptions; - rpcMarshalableAttribute = options.Attribute; + // Validation requires more trim annotations than our NativeAOT callers can provide. + // And besides, analyzers should have called out any issues at compile-time. + ValidateMarshalableInterface(typeShape, marshalableAttribute); + + proxyOptions = defaultProxyOptions; + targetOptions = RpcMarshalableInterfaceDefaultTargetOptions; + rpcMarshalableAttribute = marshalableAttribute; + + // Custom marshalable objects get proxy options based on the formatter, so don't store this formatter's proxy options in the cache. + MarshaledTypes.TryAdd(typeShape.Type, (ProxyOptions: null, targetOptions, rpcMarshalableAttribute)); return true; } - foreach ((Type implicitlyMarshaledType, JsonRpcProxyOptions typeProxyOptions, JsonRpcTargetOptions typeTargetOptions, RpcMarshalableAttribute attribute) in ImplicitlyMarshaledTypes) + return false; + } + + internal static bool TryGetMarshalOptionsForType( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] Type type, + JsonRpcProxyOptions defaultProxyOptions, + [NotNullWhen(true)] out JsonRpcProxyOptions? proxyOptions, + [NotNullWhen(true)] out JsonRpcTargetOptions? targetOptions, + [NotNullWhen(true)] out RpcMarshalableAttribute? rpcMarshalableAttribute) + { + if (TryGetMarshalOptionsForTypeHelper(type, defaultProxyOptions, out proxyOptions, out targetOptions, out rpcMarshalableAttribute)) { - if (implicitlyMarshaledType == type || - (implicitlyMarshaledType.IsGenericTypeDefinition && - type.IsConstructedGenericType && - implicitlyMarshaledType == type.GetGenericTypeDefinition())) + // Because events are not checked by the Nerdbank.MessagePack formatter, + // Remove this check when issues related to https://github.com/eiriktsarpalis/PolyType/issues/226 are resolved in this file. + if (type.GetEvents().Length > 0) { - proxyOptions = typeProxyOptions; - targetOptions = typeTargetOptions; - rpcMarshalableAttribute = attribute; - MarshaledTypes.TryAdd(type, (proxyOptions, targetOptions, rpcMarshalableAttribute)); - return true; + throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Resources.MarshalableInterfaceHasEvents, type.FullName)); } + + return true; } if (type.GetCustomAttribute() is RpcMarshalableAttribute marshalableAttribute) { + // Validation requires more trim annotations than our NativeAOT callers can provide. + // And besides, analyzers should have called out any issues at compile-time. ValidateMarshalableInterface(type, marshalableAttribute); - proxyOptions = RpcMarshalableInterfaceDefaultOptions.ProxyOptions; - targetOptions = RpcMarshalableInterfaceDefaultOptions.TargetOptions; + proxyOptions = defaultProxyOptions; + targetOptions = RpcMarshalableInterfaceDefaultTargetOptions; rpcMarshalableAttribute = marshalableAttribute; - MarshaledTypes.TryAdd(type, (proxyOptions, targetOptions, rpcMarshalableAttribute)); + + // Custom marshalable objects get proxy options based on the formatter, so don't store this formatter's proxy options in the cache. + MarshaledTypes.TryAdd(type, (ProxyOptions: null, targetOptions, rpcMarshalableAttribute)); return true; } @@ -194,15 +213,14 @@ internal static RpcMarshalableOptionalInterfaceAttribute[] GetMarshalableOptiona /// Prepares a local object to be marshaled over the wire. /// /// The object to be exposed over RPC. - /// + /// /// The marshalable interface type of as declared in the RPC contract. /// The attribute that defines certain options that control which marshaling rules will be followed. /// A token to be serialized so the remote party can invoke methods on the marshaled object. - [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] internal MarshalToken GetToken( object marshaledObject, JsonRpcTargetOptions options, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type declaredType, + RpcTargetMetadata declaredType, RpcMarshalableAttribute rpcMarshalableAttribute) { if (this.formatterState.SerializingMessageWithId.IsEmpty) @@ -234,7 +252,7 @@ internal MarshalToken GetToken( long handle = this.nextUniqueHandle++; - IRpcMarshaledContext context = JsonRpc.MarshalWithControlledLifetime(declaredType, marshaledObject, options); + RpcMarshaledContext context = JsonRpc.MarshalWithControlledLifetime(declaredType.TargetType, marshaledObject, options); RpcTargetInfo.RevertAddLocalRpcTarget? revert = this.jsonRpc.AddLocalRpcTargetInternal( declaredType, @@ -250,7 +268,7 @@ internal MarshalToken GetToken( Type objectType = marshaledObject.GetType(); List? optionalInterfacesCodes = null; - foreach (RpcMarshalableOptionalInterfaceAttribute attribute in GetMarshalableOptionalInterfaces(declaredType, rpcMarshalableAttribute)) + foreach (RpcMarshalableOptionalInterfaceAttribute attribute in GetMarshalableOptionalInterfaces(declaredType.TargetType, rpcMarshalableAttribute)) { if (attribute.OptionalInterface.IsAssignableFrom(objectType)) { @@ -293,10 +311,10 @@ internal MarshalToken GetToken( /// The interface the proxy must implement. /// The token received from the remote party that includes the handle to the remote object. /// The options to feed into proxy generation. + /// The shape of the interface for which a proxy must be produced, if available. /// The generated proxy, or if is null. - [RequiresUnreferencedCode(RuntimeReasons.RefEmit), RequiresDynamicCode(RuntimeReasons.RefEmit)] [return: NotNullIfNotNull("token")] - internal object? GetObject(Type interfaceType, MarshalToken? token, JsonRpcProxyOptions options) + internal object? GetObject(Type interfaceType, MarshalToken? token, JsonRpcProxyOptions options, ITypeShape? typeShape = null) { if (token is null) { @@ -307,7 +325,7 @@ internal MarshalToken GetToken( { lock (this.marshaledObjects) { - if (this.marshaledObjects.TryGetValue(token.Value.Handle, out (IRpcMarshaledContext Context, IDisposable Revert) marshaled)) + if (this.marshaledObjects.TryGetValue(token.Value.Handle, out (RpcMarshaledContext Context, IDisposable Revert) marshaled)) { return marshaled.Context.Proxy; } @@ -339,7 +357,8 @@ internal MarshalToken GetToken( } // CONSIDER: If we ever support arbitrary RPC interfaces, we'd need to consider how events on those interfaces would work. - object result = this.jsonRpc.CreateProxy( + object result = this.proxyFactory.CreateProxy( + this.jsonRpc, new ProxyInputs { ContractInterface = interfaceType, @@ -357,11 +376,12 @@ internal MarshalToken GetToken( this.jsonRpc.NotifyAsync(Invariant($"$/invokeProxy/{token.Value.Handle}/{options.MethodNameTransform(nameof(IDisposable.Dispose))}")).Forget(); } - this.jsonRpc.NotifyWithParameterObjectAsync("$/releaseMarshaledObject", new { handle = token.Value.Handle, ownedBySender = false }).Forget(); + this.jsonRpc.NotifyWithParameterObjectAsync("$/releaseMarshaledObject", NamedArgs.Create(new { handle = token.Value.Handle, ownedBySender = false })).Forget(); } }, }, MarshaledObjectHandle = token.Value.Handle, + ContractInterfaceShape = typeShape, }); if (options.OnProxyConstructed is object) { @@ -401,6 +421,47 @@ internal MarshalToken GetToken( } } + private static bool TryGetMarshalOptionsForTypeHelper( + Type type, + JsonRpcProxyOptions defaultProxyOptions, + [NotNullWhen(true)] out JsonRpcProxyOptions? proxyOptions, + [NotNullWhen(true)] out JsonRpcTargetOptions? targetOptions, + [NotNullWhen(true)] out RpcMarshalableAttribute? rpcMarshalableAttribute) + { + proxyOptions = null; + targetOptions = null; + rpcMarshalableAttribute = null; + if (type.IsInterface is false) + { + return false; + } + + if (MarshaledTypes.TryGetValue(type, out (JsonRpcProxyOptions? ProxyOptions, JsonRpcTargetOptions TargetOptions, RpcMarshalableAttribute Attribute) options)) + { + proxyOptions = options.ProxyOptions ?? defaultProxyOptions; + targetOptions = options.TargetOptions; + rpcMarshalableAttribute = options.Attribute; + return true; + } + + foreach ((Type implicitlyMarshaledType, JsonRpcProxyOptions typeProxyOptions, JsonRpcTargetOptions typeTargetOptions, RpcMarshalableAttribute attribute) in ImplicitlyMarshaledTypes) + { + if (implicitlyMarshaledType == type || + (implicitlyMarshaledType.IsGenericTypeDefinition && + type.IsConstructedGenericType && + implicitlyMarshaledType == type.GetGenericTypeDefinition())) + { + proxyOptions = typeProxyOptions; + targetOptions = typeTargetOptions; + rpcMarshalableAttribute = attribute; + MarshaledTypes.TryAdd(type, (proxyOptions, targetOptions, rpcMarshalableAttribute)); + return true; + } + } + + return false; + } + /// /// Throws if is not a valid marshalable interface. /// This method doesn't validate that has the @@ -431,16 +492,47 @@ private static void ValidateMarshalableInterface( } } + /// + /// Throws if is not a valid marshalable interface. + /// This method doesn't validate that has the + /// attribute. + /// + /// The shape of the interface to validate. + /// The attribute that appears on the interface. + /// When is not a valid marshalable interface: this + /// can happen if has properties, events or it is not disposable. + private static void ValidateMarshalableInterface( + ITypeShape typeShape, + RpcMarshalableAttribute attribute) + { + // We only require marshalable interfaces to derive from IDisposable when they are not call-scoped. + if (!attribute.CallScopedLifetime && !typeof(IDisposable).IsAssignableFrom(typeShape.Type)) + { + throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Resources.MarshalableInterfaceNotDisposable, typeShape.Type.FullName)); + } + + // Enable when https://github.com/eiriktsarpalis/PolyType/issues/226 makes events available. + ////if (typeShape.GetEvents().Length > 0) + ////{ + //// throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Resources.MarshalableInterfaceHasEvents, type.FullName)); + ////} + + if (typeShape is IObjectTypeShape { Properties.Count: > 0 }) + { + throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Resources.MarshalableInterfaceHasProperties, typeShape.Type.FullName)); + } + } + /// /// Releases memory associated with marshaled objects. /// - /// The handle to the object as created by the method. + /// The handle to the object as created by the method. /// if the was created by (and thus the original object owned by) the remote party; if the token and object was created locally. private void ReleaseMarshaledObject(long handle, bool ownedBySender) { lock (this.marshaledObjects) { - if (this.marshaledObjects.TryGetValue(handle, out (IRpcMarshaledContext Context, IDisposable Revert) info)) + if (this.marshaledObjects.TryGetValue(handle, out (RpcMarshaledContext Context, IDisposable Revert) info)) { this.marshaledObjects.Remove(handle); info.Revert.Dispose(); diff --git a/src/StreamJsonRpc/Reflection/ProxyBase.cs b/src/StreamJsonRpc/Reflection/ProxyBase.cs index 614c9840a..ca29db367 100644 --- a/src/StreamJsonRpc/Reflection/ProxyBase.cs +++ b/src/StreamJsonRpc/Reflection/ProxyBase.cs @@ -1,9 +1,21 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Collections.Concurrent; +using System.Collections.Frozen; +using System.Collections.Immutable; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using PolyType; +using PolyType.Abstractions; +using PolyType.Utilities; +using StreamJsonRpc.Reflection; + +// Instruct PolyType to generate shapes with methods included for .NET interfaces that we make special allowances to treat as if they were declared with [RpcMarshalable]. +// Generic interfaces require very special handling to work in NativeAOT environments. +[assembly: TypeShapeExtension(typeof(IDisposable), IncludeMethods = MethodShapeFlags.PublicInstance)] +[assembly: TypeShapeExtension(typeof(IObserver<>), IncludeMethods = MethodShapeFlags.PublicInstance, AssociatedTypes = [typeof(ProxyBase.ObserverProxyActivator<>)], Requirements = TypeShapeRequirements.Constructor)] namespace StreamJsonRpc.Reflection; @@ -13,11 +25,22 @@ namespace StreamJsonRpc.Reflection; [EditorBrowsable(EditorBrowsableState.Never)] public abstract class ProxyBase : IJsonRpcClientProxyInternal { + /// + /// A map of .NET BCL types that we have special handling for so that users can use them in their RPC interfaces + /// as if they had applied to them, + /// to their activation helpers. + /// + private static readonly FrozenDictionary BclTypesTreatedAsMarshalable = new Dictionary + { + [typeof(IObserver<>)] = typeof(ObserverProxyActivator<>), + }.ToFrozenDictionary(); + + private static readonly ConcurrentDictionary> OptionalInterfaceCodeCache = []; + private readonly JsonRpc client; - private readonly JsonRpcProxyOptions? options; - private readonly long? marshaledObjectHandle; - private readonly Action? onDispose; + private readonly ProxyInputs inputs; private readonly ReadOnlyMemory? requestedInterfaces; + private readonly IReadOnlyDictionary optionalInterfaceCodes; private bool disposed; /// @@ -33,9 +56,7 @@ public ProxyBase(JsonRpc client, in ProxyInputs inputs) } this.client = client; - this.options = inputs.Options; - this.marshaledObjectHandle = inputs.MarshaledObjectHandle; - this.onDispose = inputs.Options?.OnDispose; + this.inputs = inputs; Type[] requestedInterfaces = new Type[1 + inputs.AdditionalContractInterfaces.Length + inputs.ImplementedOptionalInterfaces.Length]; int i = 0; @@ -51,6 +72,58 @@ public ProxyBase(JsonRpc client, in ProxyInputs inputs) } this.requestedInterfaces = requestedInterfaces; + + this.optionalInterfaceCodes = OptionalInterfaceCodeCache.GetOrAdd( + inputs.ContractInterface, + static contract => + { + RpcMarshalableAttribute? mainAttribute = (RpcMarshalableAttribute?)contract.GetCustomAttribute(typeof(RpcMarshalableAttribute), inherit: false); + if (mainAttribute is null) + { + return ImmutableDictionary.Empty; + } + + RpcMarshalableOptionalInterfaceAttribute[] optionalInterfaceAttributes = MessageFormatterRpcMarshaledContextTracker.GetMarshalableOptionalInterfaces(contract, mainAttribute); + if (optionalInterfaceAttributes is []) + { + return ImmutableDictionary.Empty; + } + + (Type, int?)[] sortedOptionalInterfaces = optionalInterfaceAttributes.Select(optionalInterfaceAttributes => (optionalInterfaceAttributes.OptionalInterface, (int?)optionalInterfaceAttributes.OptionalInterfaceCode)).ToArray(); + ProxyInputs.SortRpcInterfaces(sortedOptionalInterfaces); + + // COMPAT warning: dynamic proxies only ever consider the selected subset of optional interfaces when assigning prefixes. + // But this (newer) code considers all possible optional interfaces whether they are selected or not, so that we can share the results + // across all such proxies. + // It also makes sense that these should be stable assignments so the client knows how to call a particular method, even though it may not + // know all the interfaces the server implements. + // So if we ever get into a compat issue here, we should probably fix the algorithm in ProxyGeneration.cs to match the one we have here. + Dictionary codes = []; + foreach ((Type optionalIface, _) in sortedOptionalInterfaces) + { + // Access the interface through the attribute to avoid trim safety warnings. + RpcMarshalableOptionalInterfaceAttribute attribute = optionalInterfaceAttributes.First(att => att.OptionalInterface == optionalIface); + AddInterfaceCode(optionalIface, attribute.OptionalInterfaceCode); + + Type[] baseInterfaces = attribute.OptionalInterface.GetInterfaces(); + foreach (Type iface in baseInterfaces) + { + AddInterfaceCode(iface, attribute.OptionalInterfaceCode); + } + } + + void AddInterfaceCode(Type iface, int code) + { + // We never assign a code to an interface that the contract derives from. + // We also follow a first one wins policy (in the sorted collection our caller is enumerating). + if (!iface.IsAssignableFrom(contract) && !codes.ContainsKey(iface)) + { + codes.Add(iface, code); + } + } + + return codes; + }); } /// @@ -59,6 +132,25 @@ public ProxyBase(JsonRpc client, in ProxyInputs inputs) /// public event EventHandler? CalledMethod; + /// + /// A stub interface used to trigger generation of a source generated proxy for . + /// + /// The type of observed value. + /// + /// The proxy is activated by . + /// + [RpcMarshalable] + [TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + internal interface IObserverProxyGenerator : IObserver, IDisposable; + + /// + /// A non-generic interface that can activate a generic proxy type. + /// + private interface IProxyActivator + { + IJsonRpcClientProxy Activate(JsonRpc client, in ProxyInputs inputs); + } + /// public JsonRpc JsonRpc => this.client; @@ -66,12 +158,12 @@ public ProxyBase(JsonRpc client, in ProxyInputs inputs) public bool IsDisposed => this.disposed || this.client.IsDisposed; /// - long? IJsonRpcClientProxyInternal.MarshaledObjectHandle => this.marshaledObjectHandle; + long? IJsonRpcClientProxyInternal.MarshaledObjectHandle => this.inputs.MarshaledObjectHandle; /// /// Gets options related to this proxy. /// - protected JsonRpcProxyOptions Options => this.options ?? JsonRpcProxyOptions.Default; + protected JsonRpcProxyOptions Options => this.inputs.Options ?? JsonRpcProxyOptions.Default; /// /// Creates a source generated proxy for the specified and . @@ -131,10 +223,30 @@ public static IJsonRpcClientProxy CreateProxy(JsonRpc jsonRpc, in ProxyInputs pr /// public static bool TryCreateProxy(JsonRpc jsonRpc, in ProxyInputs proxyInputs, [NotNullWhen(true)] out IJsonRpcClientProxy? proxy) { - if (proxyInputs.ImplementedOptionalInterfaces.Span is not []) + // Special case for certain interfaces which we document that we + // can create proxies for without any effort on the user's part. + if (proxyInputs.AdditionalContractInterfaces.IsEmpty && proxyInputs.ImplementedOptionalInterfaces.IsEmpty) { - proxy = null; - return false; + if (proxyInputs.ContractInterface == typeof(IDisposable)) + { + proxy = new ProxyForIDisposable(jsonRpc, proxyInputs); + return true; + } + else if (proxyInputs.ContractInterface is { GenericTypeArguments.Length: 1 } && proxyInputs.ContractInterfaceShape is not null) + { + // To avoid having to dynamically close a generic type, we utilize PolyType associated type shapes to get our activation class, + // which is generic and therefore the NativeAOT compiler will have precompiled it and the proxy it depends on. + if (BclTypesTreatedAsMarshalable.TryGetValue(proxyInputs.ContractInterface.GetGenericTypeDefinition(), out Type? associatedActivatorType)) + { + IObjectTypeShape? proxyGenerationShape = (IObjectTypeShape?)proxyInputs.ContractInterfaceShape.GetAssociatedTypeShape(associatedActivatorType); + if (proxyGenerationShape?.GetDefaultConstructor() is { } ctor) + { + IProxyActivator activator = (IProxyActivator)ctor(); + proxy = activator.Activate(jsonRpc, proxyInputs); + return true; + } + } + } } // Look for a source generated proxy type first. @@ -143,7 +255,12 @@ public static bool TryCreateProxy(JsonRpc jsonRpc, in ProxyInputs proxyInputs, [ { // Of the various proxies that implement the interfaces the user requires, // look for a match. - if (ProxyImplementsCompatibleSetOfInterfaces(attribute.ProxyClass, proxyInputs.ContractInterface, proxyInputs.AdditionalContractInterfaces.Span, proxyInputs.Options)) + if (ProxyImplementsCompatibleSetOfInterfaces( + attribute.ProxyClass, + proxyInputs.ContractInterface, + proxyInputs.AdditionalContractInterfaces.Span, + proxyInputs.ImplementedOptionalInterfaces.Span, + proxyInputs.Options)) { // If the source generated proxy type exists, use it. proxy = (IJsonRpcClientProxyInternal)Activator.CreateInstance(attribute.ProxyClass, jsonRpc, proxyInputs)!; @@ -156,30 +273,34 @@ public static bool TryCreateProxy(JsonRpc jsonRpc, in ProxyInputs proxyInputs, [ } /// - public T? As() - where T : class + public bool Is(Type type) { + Requires.NotNull(type); + + bool assignable = type.IsAssignableFrom(this.GetType()); + // If the type check fails, then the contract is definitely not implemented. - if (this is not T thisAsThat) + if (!assignable) { - return null; + return false; } - if (!this.requestedInterfaces.HasValue || this.options?.AcceptProxyWithExtraInterfaces is not true) + if (!this.requestedInterfaces.HasValue || !this.Options.AcceptProxyWithExtraInterfaces) { - // There's no chance this proxy implements too many interfaces. - return thisAsThat; + // There's no chance this proxy implements too many interfaces, + // so fallback to assignability check. + return assignable; } foreach (Type iface in this.requestedInterfaces.Value.Span) { - if (iface == typeof(T)) + if (type.IsAssignableFrom(iface)) { - return thisAsThat; + return true; } } - return null; + return false; } /// @@ -192,9 +313,9 @@ public void Dispose() this.disposed = true; - if (this.onDispose is not null) + if (this.inputs.Options?.OnDispose is Action dispose) { - this.onDispose(); + dispose(); } else { @@ -202,6 +323,24 @@ public void Dispose() } } + /// + /// Applies the prescribed transform to obtain the RPC method name from an existing method. + /// + /// The name of the method in CLR terms. + /// The declaring type of the method. + /// The RPC name of the method. + protected string TransformMethodName(string name, Type declaringType) + => this.Options.MethodNameTransform(this.TryGetOptionalInterfaceCode(Requires.NotNull(declaringType), out int code) ? $"{code}.{name}" : name); + + /// + /// Transforms the specified event name using the configured event name transformation logic. + /// + /// The name of the event to transform in CLR terms. + /// The type that declares the event. + /// The name of the RPC method invoked when the event is raised. + protected string TransformEventName(string name, Type declaringType) + => this.Options.EventNameTransform(this.TryGetOptionalInterfaceCode(Requires.NotNull(declaringType), out int code) ? $"{code}.{name}" : name); + /// /// Invokes the event. /// @@ -220,6 +359,7 @@ public void Dispose() /// The type of the proxy class to be evaluated. This type must implement the specified interfaces. /// The primary contract interface that the proxy class must implement. /// A span of additional contract interfaces that the proxy class must also implement. + /// Another span of contract interfaces that the proxy class must implement. /// Options that influence the compatibility check, such as whether extra interfaces are acceptable. /// if the proxy class implements the specified contract interface and additional interfaces, /// and optionally extra interfaces if allowed by the options; otherwise, . @@ -231,6 +371,7 @@ private static bool ProxyImplementsCompatibleSetOfInterfaces( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type proxyClass, Type contractInterface, ReadOnlySpan additionalContractInterfaces, + ReadOnlySpan<(Type Type, int Code)> implementedOptionalInterfaces, JsonRpcProxyOptions? options) { HashSet proxyInterfaces = [.. proxyClass.GetInterfaces()]; @@ -247,6 +388,14 @@ private static bool ProxyImplementsCompatibleSetOfInterfaces( } } + foreach ((Type addl, _) in implementedOptionalInterfaces) + { + if (!proxyInterfaces.Remove(addl)) + { + return false; + } + } + // At this point, we've ensured that the proxy implements the contract interface and any additional interfaces. // But does it implement *more* than the caller wants? if (options?.AcceptProxyWithExtraInterfaces is true) @@ -274,10 +423,77 @@ private static bool ProxyImplementsCompatibleSetOfInterfaces( } } + foreach ((Type addl, _) in implementedOptionalInterfaces) + { + if (remaining.IsAssignableFrom(addl)) + { + continue; + } + } + // This is an extra, unwanted interface. return false; } return true; } + + private bool TryGetOptionalInterfaceCode(Type iface, [MaybeNullWhen(false)] out int code) + { + // Never produce a code when the interface being tested for is assignable from the primary contract interface + // or any of the additional contract interfaces. + if (iface.IsAssignableFrom(this.inputs.ContractInterface)) + { + code = default; + return false; + } + + foreach (Type addl in this.inputs.AdditionalContractInterfaces.Span) + { + if (iface.IsAssignableFrom(addl)) + { + code = default; + return false; + } + } + + return this.optionalInterfaceCodes.TryGetValue(iface, out code); + } + + /// + /// A helper class that can activate a closed generic proxy for in a NativeAOT-compatible way. + /// + /// The type argument for the proxy. + /// + /// + /// Because this class has a hard-coded type parameter, it can activate the proxy in a NativeAOT-safe way. + /// Instances of this class are obtained via PolyType associated type shapes in order to avoid any code ever having to call + /// or which requires a runtime + /// that supports dynamic code generation. + /// + /// + /// The proxy this class activates is generated by the source generator in response to . + /// + /// + /// This class may be hidden from users, but it is here to trigger source generators to emit code that references it, + /// so treat this class just like any other legitimate public API by honoring binary API compatibility requirements. + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public class ObserverProxyActivator : IProxyActivator + { + IJsonRpcClientProxy IProxyActivator.Activate(JsonRpc client, in ProxyInputs inputs) => new Generated.StreamJsonRpc_Reflection_ProxyBase_IObserverProxyGenerator_Proxy(client, inputs); + } + + /// + /// A minimal derived class that serves as an proxy. + /// + /// + /// + /// + /// The base class already implements the interface. + /// The only reason we have to declare this class is because is + /// and we need a concrete type to instantiate. + /// + private class ProxyForIDisposable(JsonRpc client, in ProxyInputs inputs) : ProxyBase(client, inputs), IDisposable; } diff --git a/src/StreamJsonRpc/Reflection/ProxyFactory.cs b/src/StreamJsonRpc/Reflection/ProxyFactory.cs new file mode 100644 index 000000000..b3ee4064c --- /dev/null +++ b/src/StreamJsonRpc/Reflection/ProxyFactory.cs @@ -0,0 +1,42 @@ +using System.Diagnostics.CodeAnalysis; + +namespace StreamJsonRpc.Reflection; + +internal abstract class ProxyFactory +{ + internal static ProxyFactory Default + { + [RequiresDynamicCode(RuntimeReasons.RefEmit), RequiresUnreferencedCode(RuntimeReasons.RefEmit)] + get => DefaultProxyFactory.Instance; + } + + internal static ProxyFactory NoDynamic => NoDynamicProxyFactory.Instance; + + internal abstract IJsonRpcClientProxyInternal CreateProxy(JsonRpc jsonRpc, ProxyInputs proxyInputs); + + [RequiresDynamicCode(RuntimeReasons.RefEmit), RequiresUnreferencedCode(RuntimeReasons.RefEmit)] + private class DefaultProxyFactory : ProxyFactory + { + internal static readonly DefaultProxyFactory Instance = new(); + + private DefaultProxyFactory() + { + } + + internal override IJsonRpcClientProxyInternal CreateProxy(JsonRpc jsonRpc, ProxyInputs proxyInputs) => jsonRpc.CreateProxy(proxyInputs); + } + + private class NoDynamicProxyFactory : ProxyFactory + { + internal static readonly NoDynamicProxyFactory Instance = new(); + + private NoDynamicProxyFactory() + { + } + + internal override IJsonRpcClientProxyInternal CreateProxy(JsonRpc jsonRpc, ProxyInputs proxyInputs) + => ProxyBase.TryCreateProxy(jsonRpc, proxyInputs, out IJsonRpcClientProxy? proxy) + ? (IJsonRpcClientProxyInternal)proxy + : throw proxyInputs.CreateNoSourceGeneratedProxyException(); + } +} diff --git a/src/StreamJsonRpc/Reflection/ProxyInputs.cs b/src/StreamJsonRpc/Reflection/ProxyInputs.cs index d11f18f5e..0cff8de6e 100644 --- a/src/StreamJsonRpc/Reflection/ProxyInputs.cs +++ b/src/StreamJsonRpc/Reflection/ProxyInputs.cs @@ -4,6 +4,8 @@ #pragma warning disable SA1629 // Documentation should end with a period. using System.Diagnostics; +using System.Text; +using PolyType.Abstractions; namespace StreamJsonRpc.Reflection; @@ -18,13 +20,25 @@ namespace StreamJsonRpc.Reflection; [DebuggerDisplay($"{{{nameof(Requirements)},nq}}")] public readonly struct ProxyInputs { + internal ProxyInputs(ProxyInputs copyFrom) + { + this.ContractInterface = copyFrom.ContractInterface; + this.AdditionalContractInterfaces = copyFrom.AdditionalContractInterfaces; + this.Options = copyFrom.Options; + this.ImplementedOptionalInterfaces = copyFrom.ImplementedOptionalInterfaces; + this.MarshaledObjectHandle = copyFrom.MarshaledObjectHandle; + this.ContractInterfaceShape = copyFrom.ContractInterfaceShape; + } + /// - /// Gets the interface that describes the functions available on the remote end. + /// Gets the primary/main interface that describes the functions available on the remote end. /// public required Type ContractInterface { get; init; } /// - /// Gets + /// Gets a list of additional interfaces that the client proxy should implement without the name transformation or event limitations + /// involved with . + /// This set should have an empty intersection with . /// public ReadOnlyMemory AdditionalContractInterfaces { get; init; } @@ -34,7 +48,9 @@ public readonly struct ProxyInputs public JsonRpcProxyOptions? Options { get; init; } /// - /// Gets + /// Gets a list of additional marshalable interfaces that the client proxy should implement. + /// Methods on these interfaces are invoked using a special name transformation that includes an integer code, + /// ensuring that methods do not suffer from name collisions across interfaces. /// internal ReadOnlyMemory<(Type Type, int Code)> ImplementedOptionalInterfaces { get; init; } @@ -43,8 +59,60 @@ public readonly struct ProxyInputs /// internal long? MarshaledObjectHandle { get; init; } + /// + /// Gets the shape of the contract interface, if available. + /// + internal ITypeShape? ContractInterfaceShape { get; init; } + /// /// Gets a description of the requirements on the proxy to be used. /// internal string Requirements => $"Implementing interface(s): {string.Join(", ", [this.ContractInterface, .. this.AdditionalContractInterfaces.Span])}."; + + /// + /// Sorts so that: + /// + /// interfaces that are extending a lesser number of other interfaces in come first; + /// interfaces extending the same number of other interfaces in , are ordered by optional interface code; + /// where a code comes first. + /// + /// + /// The list of RPC interfaces to be sorted. + internal static void SortRpcInterfaces(IList<(Type Type, int? Code)> list) + { + (Type Type, int? Code, int InheritanceWeight)[] weightedList + = [.. list.Select(i => (i.Type, i.Code, list.Count(i2 => i2.Type.IsAssignableFrom(i.Type))))]; + Array.Sort(weightedList, CompareRpcInterfaces); + + for (int i = 0; i < weightedList.Length; i++) + { + list[i] = (weightedList[i].Type, weightedList[i].Code); + } + + int CompareRpcInterfaces((Type Type, int? Code, int InheritanceWeight) a, (Type Type, int? Code, int InheritanceWeight) b) + { + int weightComparison = a.InheritanceWeight.CompareTo(b.InheritanceWeight); + return (weightComparison, a.Code, b.Code) switch + { + (_, _, _) when weightComparison != 0 => weightComparison, + (_, null, null) => 0, + (_, null, _) => -1, + (_, _, null) => 1, + (_, _, _) => a.Code.Value.CompareTo(b.Code.Value), + }; + } + } + + internal Exception CreateNoSourceGeneratedProxyException() + { + StringBuilder builder = new(); + builder.Append(this.ContractInterface.FullName ?? this.ContractInterface.Name); + foreach (Type additionalInterface in this.AdditionalContractInterfaces.Span) + { + builder.Append(", "); + builder.Append(additionalInterface.FullName ?? additionalInterface.Name); + } + + return new NotImplementedException(Resources.FormatNoSourceGeneratedProxyAvailable(builder)); + } } diff --git a/src/StreamJsonRpc/Reflection/RpcMarshalableAttribute.cs b/src/StreamJsonRpc/Reflection/RpcMarshalableAttribute.cs index e2158249e..f677735cf 100644 --- a/src/StreamJsonRpc/Reflection/RpcMarshalableAttribute.cs +++ b/src/StreamJsonRpc/Reflection/RpcMarshalableAttribute.cs @@ -24,4 +24,9 @@ public class RpcMarshalableAttribute : Attribute /// The original object owner retains ownership of the lifetime of the object after the RPC call. /// public bool CallScopedLifetime { get; init; } + + /// + /// Gets a value indicating whether the interface may appear as an . + /// + public bool IsOptional { get; init; } } diff --git a/src/StreamJsonRpc/Resources.resx b/src/StreamJsonRpc/Resources.resx index fd78b8e83..75bc703db 100644 --- a/src/StreamJsonRpc/Resources.resx +++ b/src/StreamJsonRpc/Resources.resx @@ -384,4 +384,7 @@ The shape for {member} must include a value for AttributeProvider. + + No source generated proxy is available for the requested interface(s): {ifaces}. Dynamic proxies are forbidden by the options. + \ No newline at end of file diff --git a/src/StreamJsonRpc/RpcMarshaledContext.cs b/src/StreamJsonRpc/RpcMarshaledContext.cs index 9cb80acc1..a84306a08 100644 --- a/src/StreamJsonRpc/RpcMarshaledContext.cs +++ b/src/StreamJsonRpc/RpcMarshaledContext.cs @@ -3,34 +3,61 @@ namespace StreamJsonRpc; -/// -internal class RpcMarshaledContext : IRpcMarshaledContext - where T : class +/// +/// Represents an object to be marshaled and provides check or influence its lifetime. +/// +/// +/// When an object implementing this interface is disposed +/// the marshaling relationship is immediately terminated. +/// This releases resources allocated to facilitating the marshaling of the object +/// and prevents any further invocations of the object by the remote party. +/// If the underlying object implements then its +/// method is also invoked. +/// +/// +/// This type is an interface rather than a class so that users can enjoy covariance on the generic type parameter. +/// +internal class RpcMarshaledContext : IDisposableObservable { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The value that should be used in the object graph to be sent over RPC, to trigger marshaling. + /// The declared type of the proxy. + /// The value that should be used in the object graph to be sent over RPC, to trigger marshaling. /// The to use when adding this object as an RPC target. - internal RpcMarshaledContext(T value, JsonRpcTargetOptions options) + internal RpcMarshaledContext(Type interfaceType, object marshaledObject, JsonRpcTargetOptions options) { // We shouldn't reach this point with a proxy. - Requires.Argument(value is not IJsonRpcClientProxyInternal, nameof(value), "Cannot marshal a proxy."); + Requires.Argument(marshaledObject is not IJsonRpcClientProxyInternal, nameof(marshaledObject), "Cannot marshal a proxy."); - this.Proxy = value; + this.DeclaredType = interfaceType; + this.Proxy = marshaledObject; this.JsonRpcTargetOptions = options; } - /// + /// + /// Occurs when the marshalling relationship is released, + /// whether by a local call to + /// or at the remote party's request. + /// public event EventHandler? Disposed; - /// - public T Proxy { get; private set; } + /// + /// Gets the interface type whose methods are exposed for RPC invocation on the target object. + /// + public Type DeclaredType { get; } + + /// + /// Gets the marshalable proxy that should be used in the RPC message. + /// + public object Proxy { get; } /// public bool IsDisposed { get; private set; } - /// + /// + /// Gets the to associate with this object when it becomes a RPC target. + /// public JsonRpcTargetOptions JsonRpcTargetOptions { get; } /// diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index 955f8c6c3..54c71b1ca 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -908,12 +908,12 @@ public RpcMarshalableConverterFactory(SystemTextJsonFormatter formatter) public override bool CanConvert(Type typeToConvert) { - return MessageFormatterRpcMarshaledContextTracker.TryGetMarshalOptionsForType(typeToConvert, out _, out _, out _); + return MessageFormatterRpcMarshaledContextTracker.TryGetMarshalOptionsForType(typeToConvert, JsonRpcProxyOptions.Default, out _, out _, out _); } public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - Assumes.True(MessageFormatterRpcMarshaledContextTracker.TryGetMarshalOptionsForType(typeToConvert, out JsonRpcProxyOptions? proxyOptions, out JsonRpcTargetOptions? targetOptions, out RpcMarshalableAttribute? attribute)); + Assumes.True(MessageFormatterRpcMarshaledContextTracker.TryGetMarshalOptionsForType(typeToConvert, JsonRpcProxyOptions.Default, out JsonRpcProxyOptions? proxyOptions, out JsonRpcTargetOptions? targetOptions, out RpcMarshalableAttribute? attribute)); return (JsonConverter)Activator.CreateInstance( typeof(Converter<>).MakeGenericType(typeToConvert), this.formatter, @@ -934,7 +934,8 @@ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerial public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { - MessageFormatterRpcMarshaledContextTracker.MarshalToken token = formatter.RpcMarshaledContextTracker.GetToken(value, targetOptions, typeof(T), rpcMarshalableAttribute); + RpcTargetMetadata mapping = RpcTargetMetadata.FromInterface(typeof(T)); + MessageFormatterRpcMarshaledContextTracker.MarshalToken token = formatter.RpcMarshaledContextTracker.GetToken(value, targetOptions, mapping, rpcMarshalableAttribute); JsonSerializer.Serialize(writer, token, options); } } diff --git a/src/StreamJsonRpc/net8.0/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/net8.0/PublicAPI.Unshipped.txt index 4e068a1ae..7b741b88d 100644 --- a/src/StreamJsonRpc/net8.0/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/net8.0/PublicAPI.Unshipped.txt @@ -1,4 +1,5 @@ override StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.ToString() -> string! +static StreamJsonRpc.JsonRpcExtensions.As(this StreamJsonRpc.IClientProxy! proxy) -> T? static StreamJsonRpc.JsonRpcProxyOptions.Default.get -> StreamJsonRpc.JsonRpcProxyOptions! static StreamJsonRpc.NamedArgs.Create(System.Type! objectType, object? namedArgsObject) -> StreamJsonRpc.NamedArgs? static StreamJsonRpc.NamedArgs.Create(T? namedArgsObject) -> StreamJsonRpc.NamedArgs? @@ -21,7 +22,8 @@ StreamJsonRpc.ExportRpcContractProxiesAttribute StreamJsonRpc.ExportRpcContractProxiesAttribute.ExportRpcContractProxiesAttribute() -> void StreamJsonRpc.ExportRpcContractProxiesAttribute.ForbidExternalProxyGeneration.get -> bool StreamJsonRpc.ExportRpcContractProxiesAttribute.ForbidExternalProxyGeneration.set -> void -StreamJsonRpc.IJsonRpcClientProxy.As() -> T? +StreamJsonRpc.IClientProxy +StreamJsonRpc.IClientProxy.Is(System.Type! type) -> bool StreamJsonRpc.JsonRpc.AddLocalRpcTarget(StreamJsonRpc.RpcTargetMetadata! exposingMembersOn, object! target, StreamJsonRpc.JsonRpcTargetOptions? options) -> void override StreamJsonRpc.NerdbankMessagePackFormatter.AsyncEnumerableConverter.GetJsonSchema(Nerdbank.MessagePack.JsonSchemaContext! context, PolyType.Abstractions.ITypeShape! typeShape) -> System.Text.Json.Nodes.JsonObject? override StreamJsonRpc.NerdbankMessagePackFormatter.AsyncEnumerableConverter.Read(ref Nerdbank.MessagePack.MessagePackReader reader, Nerdbank.MessagePack.SerializationContext context) -> System.Collections.Generic.IAsyncEnumerable? @@ -37,6 +39,8 @@ StreamJsonRpc.JsonRpc.NotifyWithParameterObjectAsync(string! targetName, StreamJ StreamJsonRpc.JsonRpc.NotifyWithParameterObjectAsync(string! targetName, System.Collections.Generic.IReadOnlyDictionary? namedArguments, System.Collections.Generic.IReadOnlyDictionary? argumentDeclaredTypes) -> System.Threading.Tasks.Task! StreamJsonRpc.JsonRpcContractAttribute StreamJsonRpc.JsonRpcContractAttribute.JsonRpcContractAttribute() -> void +StreamJsonRpc.JsonRpcProxyAttribute +StreamJsonRpc.JsonRpcProxyAttribute.JsonRpcProxyAttribute() -> void StreamJsonRpc.JsonRpcProxyInterfaceGroupAttribute StreamJsonRpc.JsonRpcProxyInterfaceGroupAttribute.AdditionalInterfaces.get -> System.ReadOnlyMemory StreamJsonRpc.JsonRpcProxyInterfaceGroupAttribute.JsonRpcProxyInterfaceGroupAttribute(params System.Type![]! additionalInterfaces) -> void @@ -59,12 +63,14 @@ StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute.ProxyClass.get -> System.Type! StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute.JsonRpcProxyMappingAttribute(System.Type! proxyClass) -> void StreamJsonRpc.Reflection.ProxyBase -StreamJsonRpc.Reflection.ProxyBase.As() -> T? StreamJsonRpc.Reflection.ProxyBase.CalledMethod -> System.EventHandler? StreamJsonRpc.Reflection.ProxyBase.CallingMethod -> System.EventHandler? StreamJsonRpc.Reflection.ProxyBase.Dispose() -> void +StreamJsonRpc.Reflection.ProxyBase.Is(System.Type! type) -> bool StreamJsonRpc.Reflection.ProxyBase.IsDisposed.get -> bool StreamJsonRpc.Reflection.ProxyBase.JsonRpc.get -> StreamJsonRpc.JsonRpc! +StreamJsonRpc.Reflection.ProxyBase.ObserverProxyActivator +StreamJsonRpc.Reflection.ProxyBase.ObserverProxyActivator.ObserverProxyActivator() -> void StreamJsonRpc.Reflection.ProxyBase.OnCalledMethod(string! method) -> void StreamJsonRpc.Reflection.ProxyBase.OnCallingMethod(string! method) -> void StreamJsonRpc.Reflection.ProxyBase.Options.get -> StreamJsonRpc.JsonRpcProxyOptions! @@ -87,6 +93,8 @@ StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.EnumeratorResults. StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.EnumeratorResults.Values.get -> System.Collections.Generic.IReadOnlyList? StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.EnumeratorResults.Values.init -> void StreamJsonRpc.Reflection.ProxyBase.ProxyBase(StreamJsonRpc.JsonRpc! client, in StreamJsonRpc.Reflection.ProxyInputs inputs) -> void +StreamJsonRpc.Reflection.ProxyBase.TransformEventName(string! name, System.Type! declaringType) -> string! +StreamJsonRpc.Reflection.ProxyBase.TransformMethodName(string! name, System.Type! declaringType) -> string! StreamJsonRpc.Reflection.ProxyInputs StreamJsonRpc.Reflection.ProxyInputs.AdditionalContractInterfaces.get -> System.ReadOnlyMemory StreamJsonRpc.Reflection.ProxyInputs.AdditionalContractInterfaces.init -> void @@ -95,6 +103,8 @@ StreamJsonRpc.Reflection.ProxyInputs.ContractInterface.init -> void StreamJsonRpc.Reflection.ProxyInputs.Options.get -> StreamJsonRpc.JsonRpcProxyOptions? StreamJsonRpc.Reflection.ProxyInputs.Options.init -> void StreamJsonRpc.Reflection.ProxyInputs.ProxyInputs() -> void +StreamJsonRpc.RpcMarshalableAttribute.IsOptional.get -> bool +StreamJsonRpc.RpcMarshalableAttribute.IsOptional.init -> void StreamJsonRpc.RpcTargetMetadata StreamJsonRpc.RpcTargetMetadata.AliasedMethods.get -> System.Collections.Generic.IReadOnlyDictionary!>! StreamJsonRpc.RpcTargetMetadata.AliasedMethods.init -> void diff --git a/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt index 99044e07f..288883017 100644 --- a/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,4 +1,5 @@ override StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.ToString() -> string! +static StreamJsonRpc.JsonRpcExtensions.As(this StreamJsonRpc.IClientProxy! proxy) -> T? static StreamJsonRpc.JsonRpcProxyOptions.Default.get -> StreamJsonRpc.JsonRpcProxyOptions! static StreamJsonRpc.NamedArgs.Create(System.Type! objectType, object? namedArgsObject) -> StreamJsonRpc.NamedArgs? static StreamJsonRpc.NamedArgs.Create(T? namedArgsObject) -> StreamJsonRpc.NamedArgs? @@ -19,7 +20,8 @@ StreamJsonRpc.ExportRpcContractProxiesAttribute StreamJsonRpc.ExportRpcContractProxiesAttribute.ExportRpcContractProxiesAttribute() -> void StreamJsonRpc.ExportRpcContractProxiesAttribute.ForbidExternalProxyGeneration.get -> bool StreamJsonRpc.ExportRpcContractProxiesAttribute.ForbidExternalProxyGeneration.set -> void -StreamJsonRpc.IJsonRpcClientProxy.As() -> T? +StreamJsonRpc.IClientProxy +StreamJsonRpc.IClientProxy.Is(System.Type! type) -> bool StreamJsonRpc.JsonRpc.AddLocalRpcTarget(StreamJsonRpc.RpcTargetMetadata! exposingMembersOn, object! target, StreamJsonRpc.JsonRpcTargetOptions? options) -> void override StreamJsonRpc.NerdbankMessagePackFormatter.AsyncEnumerableConverter.GetJsonSchema(Nerdbank.MessagePack.JsonSchemaContext! context, PolyType.Abstractions.ITypeShape! typeShape) -> System.Text.Json.Nodes.JsonObject? override StreamJsonRpc.NerdbankMessagePackFormatter.AsyncEnumerableConverter.Read(ref Nerdbank.MessagePack.MessagePackReader reader, Nerdbank.MessagePack.SerializationContext context) -> System.Collections.Generic.IAsyncEnumerable? @@ -35,6 +37,8 @@ StreamJsonRpc.JsonRpc.NotifyWithParameterObjectAsync(string! targetName, StreamJ StreamJsonRpc.JsonRpc.NotifyWithParameterObjectAsync(string! targetName, System.Collections.Generic.IReadOnlyDictionary? namedArguments, System.Collections.Generic.IReadOnlyDictionary? argumentDeclaredTypes) -> System.Threading.Tasks.Task! StreamJsonRpc.JsonRpcContractAttribute StreamJsonRpc.JsonRpcContractAttribute.JsonRpcContractAttribute() -> void +StreamJsonRpc.JsonRpcProxyAttribute +StreamJsonRpc.JsonRpcProxyAttribute.JsonRpcProxyAttribute() -> void StreamJsonRpc.JsonRpcProxyInterfaceGroupAttribute StreamJsonRpc.JsonRpcProxyInterfaceGroupAttribute.AdditionalInterfaces.get -> System.ReadOnlyMemory StreamJsonRpc.JsonRpcProxyInterfaceGroupAttribute.JsonRpcProxyInterfaceGroupAttribute(params System.Type![]! additionalInterfaces) -> void @@ -57,12 +61,14 @@ StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute.ProxyClass.get -> System.Type! StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute.JsonRpcProxyMappingAttribute(System.Type! proxyClass) -> void StreamJsonRpc.Reflection.ProxyBase -StreamJsonRpc.Reflection.ProxyBase.As() -> T? StreamJsonRpc.Reflection.ProxyBase.CalledMethod -> System.EventHandler? StreamJsonRpc.Reflection.ProxyBase.CallingMethod -> System.EventHandler? StreamJsonRpc.Reflection.ProxyBase.Dispose() -> void +StreamJsonRpc.Reflection.ProxyBase.Is(System.Type! type) -> bool StreamJsonRpc.Reflection.ProxyBase.IsDisposed.get -> bool StreamJsonRpc.Reflection.ProxyBase.JsonRpc.get -> StreamJsonRpc.JsonRpc! +StreamJsonRpc.Reflection.ProxyBase.ObserverProxyActivator +StreamJsonRpc.Reflection.ProxyBase.ObserverProxyActivator.ObserverProxyActivator() -> void StreamJsonRpc.Reflection.ProxyBase.OnCalledMethod(string! method) -> void StreamJsonRpc.Reflection.ProxyBase.OnCallingMethod(string! method) -> void StreamJsonRpc.Reflection.ProxyBase.Options.get -> StreamJsonRpc.JsonRpcProxyOptions! @@ -85,6 +91,8 @@ StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.EnumeratorResults. StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.EnumeratorResults.Values.get -> System.Collections.Generic.IReadOnlyList? StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.EnumeratorResults.Values.init -> void StreamJsonRpc.Reflection.ProxyBase.ProxyBase(StreamJsonRpc.JsonRpc! client, in StreamJsonRpc.Reflection.ProxyInputs inputs) -> void +StreamJsonRpc.Reflection.ProxyBase.TransformEventName(string! name, System.Type! declaringType) -> string! +StreamJsonRpc.Reflection.ProxyBase.TransformMethodName(string! name, System.Type! declaringType) -> string! StreamJsonRpc.Reflection.ProxyInputs StreamJsonRpc.Reflection.ProxyInputs.AdditionalContractInterfaces.get -> System.ReadOnlyMemory StreamJsonRpc.Reflection.ProxyInputs.AdditionalContractInterfaces.init -> void @@ -93,6 +101,8 @@ StreamJsonRpc.Reflection.ProxyInputs.ContractInterface.init -> void StreamJsonRpc.Reflection.ProxyInputs.Options.get -> StreamJsonRpc.JsonRpcProxyOptions? StreamJsonRpc.Reflection.ProxyInputs.Options.init -> void StreamJsonRpc.Reflection.ProxyInputs.ProxyInputs() -> void +StreamJsonRpc.RpcMarshalableAttribute.IsOptional.get -> bool +StreamJsonRpc.RpcMarshalableAttribute.IsOptional.init -> void StreamJsonRpc.RpcTargetMetadata StreamJsonRpc.RpcTargetMetadata.AliasedMethods.get -> System.Collections.Generic.IReadOnlyDictionary!>! StreamJsonRpc.RpcTargetMetadata.AliasedMethods.init -> void diff --git a/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt index 99044e07f..288883017 100644 --- a/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt @@ -1,4 +1,5 @@ override StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.ToString() -> string! +static StreamJsonRpc.JsonRpcExtensions.As(this StreamJsonRpc.IClientProxy! proxy) -> T? static StreamJsonRpc.JsonRpcProxyOptions.Default.get -> StreamJsonRpc.JsonRpcProxyOptions! static StreamJsonRpc.NamedArgs.Create(System.Type! objectType, object? namedArgsObject) -> StreamJsonRpc.NamedArgs? static StreamJsonRpc.NamedArgs.Create(T? namedArgsObject) -> StreamJsonRpc.NamedArgs? @@ -19,7 +20,8 @@ StreamJsonRpc.ExportRpcContractProxiesAttribute StreamJsonRpc.ExportRpcContractProxiesAttribute.ExportRpcContractProxiesAttribute() -> void StreamJsonRpc.ExportRpcContractProxiesAttribute.ForbidExternalProxyGeneration.get -> bool StreamJsonRpc.ExportRpcContractProxiesAttribute.ForbidExternalProxyGeneration.set -> void -StreamJsonRpc.IJsonRpcClientProxy.As() -> T? +StreamJsonRpc.IClientProxy +StreamJsonRpc.IClientProxy.Is(System.Type! type) -> bool StreamJsonRpc.JsonRpc.AddLocalRpcTarget(StreamJsonRpc.RpcTargetMetadata! exposingMembersOn, object! target, StreamJsonRpc.JsonRpcTargetOptions? options) -> void override StreamJsonRpc.NerdbankMessagePackFormatter.AsyncEnumerableConverter.GetJsonSchema(Nerdbank.MessagePack.JsonSchemaContext! context, PolyType.Abstractions.ITypeShape! typeShape) -> System.Text.Json.Nodes.JsonObject? override StreamJsonRpc.NerdbankMessagePackFormatter.AsyncEnumerableConverter.Read(ref Nerdbank.MessagePack.MessagePackReader reader, Nerdbank.MessagePack.SerializationContext context) -> System.Collections.Generic.IAsyncEnumerable? @@ -35,6 +37,8 @@ StreamJsonRpc.JsonRpc.NotifyWithParameterObjectAsync(string! targetName, StreamJ StreamJsonRpc.JsonRpc.NotifyWithParameterObjectAsync(string! targetName, System.Collections.Generic.IReadOnlyDictionary? namedArguments, System.Collections.Generic.IReadOnlyDictionary? argumentDeclaredTypes) -> System.Threading.Tasks.Task! StreamJsonRpc.JsonRpcContractAttribute StreamJsonRpc.JsonRpcContractAttribute.JsonRpcContractAttribute() -> void +StreamJsonRpc.JsonRpcProxyAttribute +StreamJsonRpc.JsonRpcProxyAttribute.JsonRpcProxyAttribute() -> void StreamJsonRpc.JsonRpcProxyInterfaceGroupAttribute StreamJsonRpc.JsonRpcProxyInterfaceGroupAttribute.AdditionalInterfaces.get -> System.ReadOnlyMemory StreamJsonRpc.JsonRpcProxyInterfaceGroupAttribute.JsonRpcProxyInterfaceGroupAttribute(params System.Type![]! additionalInterfaces) -> void @@ -57,12 +61,14 @@ StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute.ProxyClass.get -> System.Type! StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute.JsonRpcProxyMappingAttribute(System.Type! proxyClass) -> void StreamJsonRpc.Reflection.ProxyBase -StreamJsonRpc.Reflection.ProxyBase.As() -> T? StreamJsonRpc.Reflection.ProxyBase.CalledMethod -> System.EventHandler? StreamJsonRpc.Reflection.ProxyBase.CallingMethod -> System.EventHandler? StreamJsonRpc.Reflection.ProxyBase.Dispose() -> void +StreamJsonRpc.Reflection.ProxyBase.Is(System.Type! type) -> bool StreamJsonRpc.Reflection.ProxyBase.IsDisposed.get -> bool StreamJsonRpc.Reflection.ProxyBase.JsonRpc.get -> StreamJsonRpc.JsonRpc! +StreamJsonRpc.Reflection.ProxyBase.ObserverProxyActivator +StreamJsonRpc.Reflection.ProxyBase.ObserverProxyActivator.ObserverProxyActivator() -> void StreamJsonRpc.Reflection.ProxyBase.OnCalledMethod(string! method) -> void StreamJsonRpc.Reflection.ProxyBase.OnCallingMethod(string! method) -> void StreamJsonRpc.Reflection.ProxyBase.Options.get -> StreamJsonRpc.JsonRpcProxyOptions! @@ -85,6 +91,8 @@ StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.EnumeratorResults. StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.EnumeratorResults.Values.get -> System.Collections.Generic.IReadOnlyList? StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.EnumeratorResults.Values.init -> void StreamJsonRpc.Reflection.ProxyBase.ProxyBase(StreamJsonRpc.JsonRpc! client, in StreamJsonRpc.Reflection.ProxyInputs inputs) -> void +StreamJsonRpc.Reflection.ProxyBase.TransformEventName(string! name, System.Type! declaringType) -> string! +StreamJsonRpc.Reflection.ProxyBase.TransformMethodName(string! name, System.Type! declaringType) -> string! StreamJsonRpc.Reflection.ProxyInputs StreamJsonRpc.Reflection.ProxyInputs.AdditionalContractInterfaces.get -> System.ReadOnlyMemory StreamJsonRpc.Reflection.ProxyInputs.AdditionalContractInterfaces.init -> void @@ -93,6 +101,8 @@ StreamJsonRpc.Reflection.ProxyInputs.ContractInterface.init -> void StreamJsonRpc.Reflection.ProxyInputs.Options.get -> StreamJsonRpc.JsonRpcProxyOptions? StreamJsonRpc.Reflection.ProxyInputs.Options.init -> void StreamJsonRpc.Reflection.ProxyInputs.ProxyInputs() -> void +StreamJsonRpc.RpcMarshalableAttribute.IsOptional.get -> bool +StreamJsonRpc.RpcMarshalableAttribute.IsOptional.init -> void StreamJsonRpc.RpcTargetMetadata StreamJsonRpc.RpcTargetMetadata.AliasedMethods.get -> System.Collections.Generic.IReadOnlyDictionary!>! StreamJsonRpc.RpcTargetMetadata.AliasedMethods.init -> void diff --git a/test/NativeAOTCompatibility.Test/Program.cs b/test/NativeAOTCompatibility.Test/Program.cs index e63ccba6b..7c2248c6a 100644 --- a/test/NativeAOTCompatibility.Test/Program.cs +++ b/test/NativeAOTCompatibility.Test/Program.cs @@ -5,6 +5,7 @@ #pragma warning disable SA1649 // File name should match first type name using NativeAOTCompatibility.Test; +using PolyType; using StreamJsonRpc; Console.WriteLine("This test is run by \"dotnet publish -r [RID]-x64\" rather than by executing the program."); @@ -14,7 +15,7 @@ await NerdbankMessagePack.RunAsync(); await SystemTextJson.RunAsync(); -[JsonRpcContract] +[JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] internal partial interface IServer { event EventHandler Added; diff --git a/test/StreamJsonRpc.Analyzer.Tests/JsonRpcContractAnalyzerTests.cs b/test/StreamJsonRpc.Analyzer.Tests/JsonRpcContractAnalyzerTests.cs index adcf8aade..63a3e621e 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/JsonRpcContractAnalyzerTests.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/JsonRpcContractAnalyzerTests.cs @@ -11,11 +11,77 @@ public JsonRpcContractAnalyzerTests() JsonRpcContractCodeFixProvider.NormalizeLineEndings = true; } + [Fact] + public async Task OpenGenericRpcMarshalableNeedNotBePartial() + { + await VerifyCS.VerifyAnalyzerAsync(""" + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + public interface IMyRpcMarshalable : IDisposable + { + } + """); + } + + [Fact] + public async Task OpenGenericRpcMarshalableShouldHaveTypeShapeMethods() + { + string source = """ + using System; + using StreamJsonRpc; + + [RpcMarshalable] + public interface {|StreamJsonRpc0008:IMyRpcMarshalable|} : IDisposable + { + } + """; + string fixedSource = """ + using System; + using PolyType; + using StreamJsonRpc; + + [RpcMarshalable] + [TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + public interface IMyRpcMarshalable : IDisposable + { + } + """; + await VerifyCS.VerifyCodeFixAsync(source, fixedSource); + } + + [Fact] + public async Task OpenGenericRpcMarshalableShouldHaveTypeShapeMethods_HasGenerateShape() + { + string source = """ + using System; + using PolyType; + using StreamJsonRpc; + + [RpcMarshalable] + [GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] // Ineffective on an open generic + public interface {|StreamJsonRpc0008:IMyRpcMarshalable|} : IDisposable + { + } + """; + string fixedSource = """ + using System; + using PolyType; + using StreamJsonRpc; + + [RpcMarshalable] + [GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] // Ineffective on an open generic + [TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + public interface IMyRpcMarshalable : IDisposable + { + } + """; + await VerifyCS.VerifyCodeFixAsync(source, fixedSource); + } + [Fact] public async Task MethodReturnTypes() { await VerifyCS.VerifyAnalyzerAsync(""" - [JsonRpcContract] + [JsonRpcContract, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IMyRpc { Task TaskOfTAsync(); @@ -34,7 +100,7 @@ public async Task InaccessibleInterface_Private() await VerifyCS.VerifyAnalyzerAsync(""" internal partial class Wrapper { - [JsonRpcContract] + [JsonRpcContract, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] private partial interface {|StreamJsonRpc0001:IMyRpc|} { } @@ -48,7 +114,7 @@ public async Task InaccessibleInterface_Protected() await VerifyCS.VerifyAnalyzerAsync(""" public partial class Wrapper { - [JsonRpcContract] + [JsonRpcContract, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] protected partial interface {|StreamJsonRpc0001:IMyRpc|} { } @@ -62,7 +128,7 @@ public async Task InternalInterface() await VerifyCS.VerifyAnalyzerAsync(""" internal partial class Wrapper { - [JsonRpcContract] + [JsonRpcContract, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] internal partial interface IMyRpc { } @@ -76,7 +142,7 @@ public async Task NonPartialInterface() await VerifyCS.VerifyAnalyzerAsync(""" internal class Wrapper { - [JsonRpcContract] + [JsonRpcContract, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] internal interface {|StreamJsonRpc0002:IMyRpc|} { } @@ -88,7 +154,7 @@ internal interface {|StreamJsonRpc0002:IMyRpc|} public async Task DisallowedMembers() { await VerifyCS.VerifyAnalyzerAsync(""" - [JsonRpcContract] + [JsonRpcContract, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMyRpc { event EventHandler Changed; @@ -102,11 +168,25 @@ partial interface IMyRpc """); } +#if NET + [Fact] + public async Task StaticMembersIgnored() + { + await VerifyCS.VerifyAnalyzerAsync(""" + [JsonRpcContract, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + partial interface IMyRpc + { + static int StaticMethodsAreIgnored() => 3; + } + """); + } +#endif + [Fact] public async Task DisallowedMembers_InBaseInterface() { await VerifyCS.VerifyAnalyzerAsync(""" - [JsonRpcContract] + [JsonRpcContract, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMyRpc : {|StreamJsonRpc0013:{|StreamJsonRpc0012:{|StreamJsonRpc0016:IBase|}|}|} { } @@ -128,7 +208,7 @@ interface IBase public async Task DisallowedMembers_InBaseInterfaceTwoStepsAway() { await VerifyCS.VerifyAnalyzerAsync(""" - [JsonRpcContract] + [JsonRpcContract, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMyRpc : {|StreamJsonRpc0013:{|StreamJsonRpc0012:{|StreamJsonRpc0016:IBase2|}|}|} { } @@ -152,7 +232,7 @@ interface IBase public async Task CancellationTokenPositions() { await VerifyCS.VerifyAnalyzerAsync(""" - [JsonRpcContract] + [JsonRpcContract, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMyRpc { Task AddAsync(int a, int b, CancellationToken token); @@ -166,7 +246,7 @@ partial interface IMyRpc public async Task GenericInterface() { await VerifyCS.VerifyAnalyzerAsync(""" - [JsonRpcContract] + [JsonRpcContract, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface {|StreamJsonRpc0015:IMyRpc|} { } @@ -180,7 +260,7 @@ partial interface {|StreamJsonRpc0015:IMyRpc|} public async Task RpcMarshalable_GenericInterface() { await VerifyCS.VerifyAnalyzerAsync(""" - [RpcMarshalable] + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMyRpc : IDisposable { } @@ -191,7 +271,7 @@ partial interface IMyRpc : IDisposable public async Task RpcMarshalable() { await VerifyCS.VerifyAnalyzerAsync(""" - [RpcMarshalable] + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMyRpc : IDisposable { Task SayHiAsync(); @@ -204,7 +284,7 @@ partial interface IMyRpc : IDisposable public async Task RpcMarshalable_DisallowedMembers() { await VerifyCS.VerifyAnalyzerAsync(""" - [RpcMarshalable] + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMyRpc : IDisposable { event EventHandler {|StreamJsonRpc0012:Changed|}; @@ -222,7 +302,7 @@ partial interface IMyRpc : IDisposable public async Task RpcMarshalable_WithOptionalInterfaceAndNoAttribute() { await VerifyCS.VerifyAnalyzerAsync(""" - [RpcMarshalable] + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] [{|StreamJsonRpc0007:RpcMarshalableOptionalInterface(1, typeof(IMarshalableSubType1))|}] partial interface IMyRpc : IDisposable { @@ -234,17 +314,40 @@ interface IMarshalableSubType1 """); } + [Fact] + public async Task RpcMarshalable_WithOptionalInterfaceWithoutIsOptionalTrue() + { + await VerifyCS.VerifyAnalyzerAsync(""" + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [{|StreamJsonRpc0007:RpcMarshalableOptionalInterface(1, typeof(IMarshalableSubType1))|}] + [{|StreamJsonRpc0007:RpcMarshalableOptionalInterface(2, typeof(IMarshalableSubType2))|}] + partial interface IMyRpc : IDisposable + { + } + + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + partial interface IMarshalableSubType1 : IDisposable + { + } + + [RpcMarshalable(IsOptional = false), TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + partial interface IMarshalableSubType2 : IDisposable + { + } + """); + } + [Fact] public async Task RpcMarshalable_WithOptionalInterface() { await VerifyCS.VerifyAnalyzerAsync(""" - [RpcMarshalable] + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] [RpcMarshalableOptionalInterface(1, typeof(IMarshalableSubType1))] partial interface IMyRpc : IDisposable { } - [RpcMarshalable] + [RpcMarshalable(IsOptional = true), TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMarshalableSubType1 : IDisposable { } @@ -255,7 +358,7 @@ partial interface IMarshalableSubType1 : IDisposable public async Task RpcMarshalable_CallScopedNeedNotBeIDisposable() { await VerifyCS.VerifyAnalyzerAsync(""" - [RpcMarshalable(CallScopedLifetime = true)] + [RpcMarshalable(CallScopedLifetime = true), TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMyRpc { } @@ -266,19 +369,87 @@ partial interface IMyRpc public async Task RpcMarshalable_MustDeriveFromIDisposable() { string source = """ - [StreamJsonRpc.RpcMarshalable] + using PolyType; + + [StreamJsonRpc.RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface {|StreamJsonRpc0005:IMyRpc|} { } """; string fixedSource = """ using System; + using PolyType; - [StreamJsonRpc.RpcMarshalable] + [StreamJsonRpc.RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] partial interface IMyRpc: IDisposable { } """; await VerifyCS.VerifyCodeFixAsync(source, fixedSource); } + + [Fact] + public async Task RpcInterfacesNeedMethodsIncludedInShape_Fixable() + { + string source = """ + using StreamJsonRpc; + + [JsonRpcContract] + partial interface {|StreamJsonRpc0008:IRegularContract|} + { + } + + [RpcMarshalable] + partial interface {|StreamJsonRpc0008:IMarshalable|} : System.IDisposable + { + } + """; + string fixedSource = """ + using PolyType; + using StreamJsonRpc; + + [JsonRpcContract] + [GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + partial interface IRegularContract + { + } + + [RpcMarshalable] + [TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + partial interface IMarshalable : System.IDisposable + { + } + """; + await VerifyCS.VerifyCodeFixAsync(source, fixedSource); + } + + /// + /// The code fix provider isn't sophisticated enough to correct existing GenerateShape attributes. + /// + [Fact] + public async Task RpcInterfacesNeedMethodsIncludedInShape_NotAutomaticallyFixable() + { + string source = """ + [JsonRpcContract, GenerateShape] + partial interface {|StreamJsonRpc0008:IContractWithoutMethods|} + { + } + + [JsonRpcContract, TypeShape(IncludeMethods = MethodShapeFlags.None)] + partial interface {|StreamJsonRpc0008:IContractWithoutMethodsExplicitly|} + { + } + + [JsonRpcContract, TypeShape(IncludeMethods = MethodShapeFlags.PublicStatic)] + partial interface {|StreamJsonRpc0008:IContractWithOnlyStaticMethods|} + { + } + + [JsonRpcContract, TypeShape(IncludeMethods = MethodShapeFlags.AllPublic)] + partial interface IContractWithAllPublicMethods + { + } + """; + await VerifyCS.VerifyAnalyzerAsync(source); + } } diff --git a/test/StreamJsonRpc.Analyzer.Tests/JsonRpcProxyAnalyzerTests.cs b/test/StreamJsonRpc.Analyzer.Tests/JsonRpcProxyAnalyzerTests.cs new file mode 100644 index 000000000..6af4a9e3c --- /dev/null +++ b/test/StreamJsonRpc.Analyzer.Tests/JsonRpcProxyAnalyzerTests.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using VerifyCS = CodeFixVerifier; + +public class JsonRpcProxyAnalyzerTests +{ + [Fact] + public async Task ProperUse() + { + await VerifyCS.VerifyAnalyzerAsync(""" + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [JsonRpcProxy>] + public partial interface IMyRpcMarshalable : IDisposable + { + } + + [JsonRpcContract, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [JsonRpcProxy>] + public partial interface IMyRpcContract + { + } + """); + } + + [Fact] + public async Task NotOnGenericInterface() + { + await VerifyCS.VerifyAnalyzerAsync(""" + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [{|StreamJsonRpc0030:JsonRpcProxy|}] + public partial interface IMyRpcMarshalable : IDisposable + { + } + """); + } + + [Fact] + public async Task TypeArgIsNotClosedAppliedInterface() + { + await VerifyCS.VerifyAnalyzerAsync(""" + [JsonRpcContract, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [{|StreamJsonRpc0031:JsonRpcProxy|}] + public partial interface IMyRpcMarshalable + { + } + """); + } + + [Fact] + public async Task MissingContractAttribute() + { + await VerifyCS.VerifyAnalyzerAsync(""" + [{|StreamJsonRpc0032:JsonRpcProxy>|}] + public partial interface IMyRpcMarshalable : IDisposable + { + } + + [{|StreamJsonRpc0032:JsonRpcProxy>|}] + public partial interface IMyRpcContract + { + } + """); + } +} diff --git a/test/StreamJsonRpc.Analyzer.Tests/OptionalInterfaceTypeCheckAnalyzerTests.cs b/test/StreamJsonRpc.Analyzer.Tests/OptionalInterfaceTypeCheckAnalyzerTests.cs new file mode 100644 index 000000000..456632021 --- /dev/null +++ b/test/StreamJsonRpc.Analyzer.Tests/OptionalInterfaceTypeCheckAnalyzerTests.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using VerifyCS = CodeFixVerifier; + +public class OptionalInterfaceTypeCheckAnalyzerTests +{ + [Fact] + public async Task TraditionalTypeCheckOperatorsShouldBeReplaced() + { + await VerifyCS.VerifyAnalyzerAsync(""" + [RpcMarshalable] + [RpcMarshalableOptionalInterface(1, typeof(IMyObject2))] + [RpcMarshalableOptionalInterface(2, typeof(IMyObject3))] + partial interface IMyObject : IDisposable + { + } + + [RpcMarshalable(IsOptional = true)] + partial interface IMyObject2 : IDisposable + { + } + + [RpcMarshalable(IsOptional = true)] + partial interface IMyObject3 : IDisposable + { + } + + class FromBaseToOptional + { + bool IsOperator(IMyObject o) => {|StreamJsonRpc0050:o is IMyObject2|}; + IMyObject2 AsOperator(IMyObject o) => {|StreamJsonRpc0050:o as IMyObject2|}; + IMyObject2 CastOperator(IMyObject o) => {|StreamJsonRpc0050:(IMyObject2)o|}; + } + + class FromOptionalToBase + { + bool IsOperator(IMyObject2 o) => o is IMyObject; + IMyObject AsOperator(IMyObject2 o) => o as IMyObject; + IMyObject CastOperator(IMyObject2 o) => (IMyObject)o; + } + + class BetweenOptionals + { + bool IsOperator(IMyObject3 o) => {|StreamJsonRpc0050:o is IMyObject2|}; + IMyObject2 AsOperator(IMyObject3 o) => {|StreamJsonRpc0050:o as IMyObject2|}; + IMyObject2 CastOperator(IMyObject3 o) => {|StreamJsonRpc0050:(IMyObject2)o|}; + } + """); + } + + [Fact] + public async Task CastsToOtherInterfacesNotChecked() + { + await VerifyCS.VerifyAnalyzerAsync(""" + [RpcMarshalable] + [RpcMarshalableOptionalInterface(1, typeof(IMyObject2))] + partial interface IMyObject : IDisposable + { + } + + [RpcMarshalable(IsOptional = true)] + partial interface IMyObject2 : IDisposable + { + } + + class OneWay + { + bool IsOperator(IMyObject o) => o is IDisposable; + IDisposable AsOperator(IMyObject o) => o as IDisposable; + IDisposable CastOperator(IMyObject o) => (IDisposable)o; + } + + class OtherWay + { + bool IsOperator(IDisposable o) => o is IMyObject; + IMyObject AsOperator(IDisposable o) => o as IMyObject; + IMyObject CastOperator(IDisposable o) => (IMyObject)o; + } + """); + } +} diff --git a/test/StreamJsonRpc.Analyzer.Tests/ProxyGeneratorTests.cs b/test/StreamJsonRpc.Analyzer.Tests/ProxyGeneratorTests.cs index 9578e981a..de1f62046 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/ProxyGeneratorTests.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/ProxyGeneratorTests.cs @@ -102,6 +102,27 @@ protected partial interface IMyRpc """); } + [Fact] + public async Task MethodNamesCustomizedByAttribute() + { + await VerifyCS.RunDefaultAsync(""" + using PolyType; + + [JsonRpcContract] + public partial interface IMyRpc + { + [JsonRpcMethod("AddRenamed")] + Task AddAsync(int a, int b, CancellationToken cancellationToken); + + [MethodShape(Name = "IntegrateRenamed")] + Task IntegrateAsync(double from, double to, CancellationToken cancellationToken); + + [MethodShape(Name = "DontWannaSeeThis"), JsonRpcMethod("DivideRenamed")] + Task DivideAsync(double from, double to, CancellationToken cancellationToken); + } + """); + } + [Fact] public async Task NamesRequiredNamespaceQualifier() { @@ -163,7 +184,7 @@ public partial interface IFoo : IDisposable } [Fact] - public async Task Interface_HasDisposeWithoutIDisposable() + public async Task Interface_HasAsyncDisposeWithoutIDisposable() { await VerifyCS.RunDefaultAsync(""" [JsonRpcContract] @@ -174,6 +195,33 @@ public partial interface IFoo """); } + [Fact] + public async Task Interface_DerivesFromIDisposal() + { + await VerifyCS.RunDefaultAsync(""" + [RpcMarshalable] + public partial interface IAmDisposable : IDisposable + { + } + """); + } + + [Fact] + public async Task Interface_HasNestedTypes() + { + await VerifyCS.RunDefaultAsync(""" + [RpcMarshalable] + public partial interface IHaveNestedTypes : IDisposable + { + Task DoSomethingAsync(); + + private class A { } + private struct B { } + private record C { } + } + """); + } + [Fact] public async Task Interface_DerivesFromOthers() { @@ -327,6 +375,32 @@ public partial interface IGenericMarshalable """); } + [Fact] + public async Task RpcMarshalable_GenericWithClosedPrescriptions() + { + await VerifyCS.RunDefaultAsync(""" + [RpcMarshalable] + [JsonRpcProxy>] + public partial interface IGenericMarshalable + { + Task DoSomethingWithParameterAsync(T parameter); + } + """); + } + + [Fact] + public async Task RpcMarshalable_GenericWithClosedPrescriptions_Arity2() + { + await VerifyCS.RunDefaultAsync(""" + [RpcMarshalable] + [JsonRpcProxy>] + public partial interface IGenericMarshalable + { + Task DoSomethingWithParameterAsync(T2 parameter); + } + """); + } + /// /// Verifies that an RpcMarshalable attribute on an interface with both valid and invalid members does not break the build (but it will report a diagnostic, as tested elsewhere). /// @@ -343,6 +417,102 @@ public partial interface INotSoMarshalable """); } + [Fact] + public async Task RpcMarshalable_OptionalInterfaces_WithExtensionMethods() + { + await VerifyCS.RunDefaultAsync(""" + [RpcMarshalable] + [RpcMarshalableOptionalInterfaceAttribute(1, typeof(IOptional))] + public partial interface IMarshalable + { + } + + internal partial interface IOptional { } + + class Foo + { + public static void Bar(IMarshalable m) + { + IOptional opt = m.As(); + IMarshalable back = opt.As(); + bool can = m.Is(typeof(IOptional)); + can = opt.Is(typeof(IMarshalable)); + } + } + """); + } + + [Fact] + public async Task RpcMarshalable_OptionalInterfaces_WithExtensionMethods_NotPublic() + { + await VerifyCS.RunDefaultAsync( + """ + [RpcMarshalable] + [RpcMarshalableOptionalInterfaceAttribute(1, typeof(IOptional))] + public partial interface IMarshalable + { + } + + internal partial interface IOptional { } + + class Foo + { + public static void Bar(IMarshalable m) + { + IOptional opt = m.As(); + IMarshalable back = opt.As(); + bool can = m.Is(typeof(IOptional)); + can = opt.Is(typeof(IMarshalable)); + } + } + """, + configuration: GeneratorConfiguration.Default with { PublicRpcMarshalableInterfaceExtensions = false }); + } + + [Fact] + public async Task RpcMarshalable_OptionalInterfaces_WithExtensionMethods_NestedInClass() + { + await VerifyCS.RunDefaultAsync(""" + namespace NS; + + partial class Wrapper + { + [RpcMarshalable] + [RpcMarshalableOptionalInterfaceAttribute(1, typeof(IOptional))] + public partial interface IMarshalable + { + } + + internal partial interface IOptional { } + + public static void Bar(IMarshalable m) + { + IOptional opt = m.As(); + IMarshalable back = opt.As(); + bool can = m.Is(typeof(IOptional)); + can = opt.Is(typeof(IMarshalable)); + } + } + """); + } + +#if NET + /// + /// Verifies that static members are ignored during proxy generation. + /// + [Fact] + public async Task RpcMarshalable_HasStaticMethod() + { + await VerifyCS.RunDefaultAsync(""" + [RpcMarshalable] + public partial interface IMarshalableWithProperties + { + static int GetInt() => 3; + } + """); + } +#endif + /// /// Verifies that an RpcMarshalable attribute on an invalid interface does not break the build (but it will report a diagnostic, as tested elsewhere). /// diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/Events/IFoo.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/Events/IFoo.g.cs index d97dde9d4..392e28f4d 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/Events/IFoo.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/Events/IFoo.g.cs @@ -19,8 +19,8 @@ internal class IFoo_Proxy : global::StreamJsonRpc.Reflection.ProxyBase public IFoo_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.Reflection.ProxyInputs inputs) : base(client, inputs) { - this.JsonRpc.AddLocalRpcMethod(this.Options.EventNameTransform("MyEvent"), this.OnMyEvent); - this.JsonRpc.AddLocalRpcMethod(this.Options.EventNameTransform("MyGenericEvent"), this.OnMyGenericEvent); + this.JsonRpc.AddLocalRpcMethod(this.TransformEventName("MyEvent", typeof(global::IFoo)), this.OnMyEvent); + this.JsonRpc.AddLocalRpcMethod(this.TransformEventName("MyGenericEvent", typeof(global::IFoo)), this.OnMyGenericEvent); } public event global::System.EventHandler? MyEvent; diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_ArrayInitializer/IMyService.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_ArrayInitializer/IMyService.g.cs index a7e107514..21a23a163 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_ArrayInitializer/IMyService.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_ArrayInitializer/IMyService.g.cs @@ -36,7 +36,7 @@ public IMyService_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJson if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("Task1"); - string rpcMethodName = this.transformedTask11 ??= this.Options.MethodNameTransform("Task1"); + string rpcMethodName = this.transformedTask11 ??= this.TransformMethodName("Task1", typeof(global::IMyService)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), Task1NamedArgumentDeclaredTypes1, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [], Task1PositionalArgumentDeclaredTypes1, default); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_ArrayInitializer/IMyService2.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_ArrayInitializer/IMyService2.g.cs index e218989f0..e8015c26f 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_ArrayInitializer/IMyService2.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_ArrayInitializer/IMyService2.g.cs @@ -36,7 +36,7 @@ public IMyService2_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJso if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("Task2"); - string rpcMethodName = this.transformedTask21 ??= this.Options.MethodNameTransform("Task2"); + string rpcMethodName = this.transformedTask21 ??= this.TransformMethodName("Task2", typeof(global::IMyService2)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), Task2NamedArgumentDeclaredTypes1, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [], Task2PositionalArgumentDeclaredTypes1, default); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_ArrayInitializer/IMyServiceZiHkAQOD.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_ArrayInitializer/IMyServiceZiHkAQOD.g.cs index b709f4b3f..77f3ba892 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_ArrayInitializer/IMyServiceZiHkAQOD.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_ArrayInitializer/IMyServiceZiHkAQOD.g.cs @@ -52,7 +52,7 @@ public IMyServiceZiHkAQOD_Proxy(global::StreamJsonRpc.JsonRpc client, global::St if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("Task1"); - string rpcMethodName = this.transformedTask11 ??= this.Options.MethodNameTransform("Task1"); + string rpcMethodName = this.transformedTask11 ??= this.TransformMethodName("Task1", typeof(global::IMyService)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), Task1NamedArgumentDeclaredTypes1, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [], Task1PositionalArgumentDeclaredTypes1, default); @@ -71,7 +71,7 @@ public IMyServiceZiHkAQOD_Proxy(global::StreamJsonRpc.JsonRpc client, global::St if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("Task2"); - string rpcMethodName = this.transformedTask22 ??= this.Options.MethodNameTransform("Task2"); + string rpcMethodName = this.transformedTask22 ??= this.TransformMethodName("Task2", typeof(global::IMyService2)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), Task2NamedArgumentDeclaredTypes2, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [], Task2PositionalArgumentDeclaredTypes2, default); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_CollectionInitializer/IMyService.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_CollectionInitializer/IMyService.g.cs index a7e107514..21a23a163 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_CollectionInitializer/IMyService.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_CollectionInitializer/IMyService.g.cs @@ -36,7 +36,7 @@ public IMyService_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJson if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("Task1"); - string rpcMethodName = this.transformedTask11 ??= this.Options.MethodNameTransform("Task1"); + string rpcMethodName = this.transformedTask11 ??= this.TransformMethodName("Task1", typeof(global::IMyService)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), Task1NamedArgumentDeclaredTypes1, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [], Task1PositionalArgumentDeclaredTypes1, default); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_CollectionInitializer/IMyService2.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_CollectionInitializer/IMyService2.g.cs index e218989f0..e8015c26f 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_CollectionInitializer/IMyService2.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_CollectionInitializer/IMyService2.g.cs @@ -36,7 +36,7 @@ public IMyService2_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJso if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("Task2"); - string rpcMethodName = this.transformedTask21 ??= this.Options.MethodNameTransform("Task2"); + string rpcMethodName = this.transformedTask21 ??= this.TransformMethodName("Task2", typeof(global::IMyService2)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), Task2NamedArgumentDeclaredTypes1, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [], Task2PositionalArgumentDeclaredTypes1, default); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_CollectionInitializer/IMyServiceZiHkAQOD.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_CollectionInitializer/IMyServiceZiHkAQOD.g.cs index b709f4b3f..77f3ba892 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_CollectionInitializer/IMyServiceZiHkAQOD.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_CollectionInitializer/IMyServiceZiHkAQOD.g.cs @@ -52,7 +52,7 @@ public IMyServiceZiHkAQOD_Proxy(global::StreamJsonRpc.JsonRpc client, global::St if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("Task1"); - string rpcMethodName = this.transformedTask11 ??= this.Options.MethodNameTransform("Task1"); + string rpcMethodName = this.transformedTask11 ??= this.TransformMethodName("Task1", typeof(global::IMyService)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), Task1NamedArgumentDeclaredTypes1, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [], Task1PositionalArgumentDeclaredTypes1, default); @@ -71,7 +71,7 @@ public IMyServiceZiHkAQOD_Proxy(global::StreamJsonRpc.JsonRpc client, global::St if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("Task2"); - string rpcMethodName = this.transformedTask22 ??= this.Options.MethodNameTransform("Task2"); + string rpcMethodName = this.transformedTask22 ??= this.TransformMethodName("Task2", typeof(global::IMyService2)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), Task2NamedArgumentDeclaredTypes2, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [], Task2PositionalArgumentDeclaredTypes2, default); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_DistinctYetRedundantMethods/IMyService.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_DistinctYetRedundantMethods/IMyService.g.cs index aa2d23ad4..a8107c3de 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_DistinctYetRedundantMethods/IMyService.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_DistinctYetRedundantMethods/IMyService.g.cs @@ -38,7 +38,7 @@ public IMyService_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJson if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("Task1"); - string rpcMethodName = this.transformedTask11 ??= this.Options.MethodNameTransform("Task1"); + string rpcMethodName = this.transformedTask11 ??= this.TransformMethodName("Task1", typeof(global::IMyService)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), Task1NamedArgumentDeclaredTypes1, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [name], Task1PositionalArgumentDeclaredTypes1, default); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_DistinctYetRedundantMethods/IMyService2.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_DistinctYetRedundantMethods/IMyService2.g.cs index 10dfae4a3..f0773b84d 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_DistinctYetRedundantMethods/IMyService2.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_DistinctYetRedundantMethods/IMyService2.g.cs @@ -38,7 +38,7 @@ public IMyService2_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJso if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("Task1"); - string rpcMethodName = this.transformedTask11 ??= this.Options.MethodNameTransform("Task1"); + string rpcMethodName = this.transformedTask11 ??= this.TransformMethodName("Task1", typeof(global::IMyService2)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), Task1NamedArgumentDeclaredTypes1, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [name], Task1PositionalArgumentDeclaredTypes1, default); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_DistinctYetRedundantMethods/IMyServiceZiHkAQOD.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_DistinctYetRedundantMethods/IMyServiceZiHkAQOD.g.cs index 923f113eb..0ba580674 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_DistinctYetRedundantMethods/IMyServiceZiHkAQOD.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_DistinctYetRedundantMethods/IMyServiceZiHkAQOD.g.cs @@ -56,7 +56,7 @@ public IMyServiceZiHkAQOD_Proxy(global::StreamJsonRpc.JsonRpc client, global::St if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("Task1"); - string rpcMethodName = this.transformedTask11 ??= this.Options.MethodNameTransform("Task1"); + string rpcMethodName = this.transformedTask11 ??= this.TransformMethodName("Task1", typeof(global::IMyService)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), Task1NamedArgumentDeclaredTypes1, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [name], Task1PositionalArgumentDeclaredTypes1, default); @@ -76,7 +76,7 @@ public IMyServiceZiHkAQOD_Proxy(global::StreamJsonRpc.JsonRpc client, global::St if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("Task1"); - string rpcMethodName = this.transformedTask12 ??= this.Options.MethodNameTransform("Task1"); + string rpcMethodName = this.transformedTask12 ??= this.TransformMethodName("Task1", typeof(global::IMyService2)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), Task1NamedArgumentDeclaredTypes2, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [name], Task1PositionalArgumentDeclaredTypes2, default); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_OneDerivesFromTheOther/IMyService.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_OneDerivesFromTheOther/IMyService.g.cs index aa2d23ad4..a8107c3de 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_OneDerivesFromTheOther/IMyService.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_OneDerivesFromTheOther/IMyService.g.cs @@ -38,7 +38,7 @@ public IMyService_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJson if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("Task1"); - string rpcMethodName = this.transformedTask11 ??= this.Options.MethodNameTransform("Task1"); + string rpcMethodName = this.transformedTask11 ??= this.TransformMethodName("Task1", typeof(global::IMyService)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), Task1NamedArgumentDeclaredTypes1, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [name], Task1PositionalArgumentDeclaredTypes1, default); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_OneDerivesFromTheOther/IMyService2.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_OneDerivesFromTheOther/IMyService2.g.cs index ccba76374..5a9ed1e96 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_OneDerivesFromTheOther/IMyService2.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_OneDerivesFromTheOther/IMyService2.g.cs @@ -50,7 +50,7 @@ public IMyService2_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJso if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("Task1"); - string rpcMethodName = this.transformedTask11 ??= this.Options.MethodNameTransform("Task1"); + string rpcMethodName = this.transformedTask11 ??= this.TransformMethodName("Task1", typeof(global::IMyService)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), Task1NamedArgumentDeclaredTypes1, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [name], Task1PositionalArgumentDeclaredTypes1, default); @@ -70,7 +70,7 @@ public IMyService2_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJso if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("Task2"); - string rpcMethodName = this.transformedTask22 ??= this.Options.MethodNameTransform("Task2"); + string rpcMethodName = this.transformedTask22 ??= this.TransformMethodName("Task2", typeof(global::IMyService2)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), Task2NamedArgumentDeclaredTypes2, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [color], Task2PositionalArgumentDeclaredTypes2, default); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_OneDerivesFromTheOther/IMyServiceZiHkAQOD.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_OneDerivesFromTheOther/IMyServiceZiHkAQOD.g.cs index 567ab65de..a30b30915 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_OneDerivesFromTheOther/IMyServiceZiHkAQOD.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interceptor_AttachMultipleInterfaces_OneDerivesFromTheOther/IMyServiceZiHkAQOD.g.cs @@ -56,7 +56,7 @@ public IMyServiceZiHkAQOD_Proxy(global::StreamJsonRpc.JsonRpc client, global::St if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("Task1"); - string rpcMethodName = this.transformedTask11 ??= this.Options.MethodNameTransform("Task1"); + string rpcMethodName = this.transformedTask11 ??= this.TransformMethodName("Task1", typeof(global::IMyService)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), Task1NamedArgumentDeclaredTypes1, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [name], Task1PositionalArgumentDeclaredTypes1, default); @@ -76,7 +76,7 @@ public IMyServiceZiHkAQOD_Proxy(global::StreamJsonRpc.JsonRpc client, global::St if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("Task2"); - string rpcMethodName = this.transformedTask22 ??= this.Options.MethodNameTransform("Task2"); + string rpcMethodName = this.transformedTask22 ??= this.TransformMethodName("Task2", typeof(global::IMyService2)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), Task2NamedArgumentDeclaredTypes2, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [color], Task2PositionalArgumentDeclaredTypes2, default); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interface_DerivesFromIDisposable/IFoo.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interface_DerivesFromIDisposable/IFoo.g.cs index e0a8c03cd..a0ec3ebb9 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interface_DerivesFromIDisposable/IFoo.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interface_DerivesFromIDisposable/IFoo.g.cs @@ -36,7 +36,7 @@ public IFoo_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.Re if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("JustCancellationAsync"); - string rpcMethodName = this.transformedJustCancellationAsync1 ??= this.Options.MethodNameTransform("JustCancellationAsync"); + string rpcMethodName = this.transformedJustCancellationAsync1 ??= this.TransformMethodName("JustCancellationAsync", typeof(global::IFoo)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), JustCancellationAsyncNamedArgumentDeclaredTypes1, cancellationToken) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [], JustCancellationAsyncPositionalArgumentDeclaredTypes1, cancellationToken); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interface_DerivesFromIDisposal/IAmDisposable.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interface_DerivesFromIDisposal/IAmDisposable.g.cs new file mode 100644 index 000000000..ad1923f15 --- /dev/null +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interface_DerivesFromIDisposal/IAmDisposable.g.cs @@ -0,0 +1,24 @@ +// + +#nullable enable +#pragma warning disable CS0436 // prefer local types to imported ones + +[global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.IAmDisposable_Proxy))] +partial interface IAmDisposable +{ +} + +namespace StreamJsonRpc.Generated +{ + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StreamJsonRpc.Analyzers", "x.x.x.x")] + internal class IAmDisposable_Proxy : global::StreamJsonRpc.Reflection.ProxyBase + , global::IAmDisposable + { + + public IAmDisposable_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.Reflection.ProxyInputs inputs) + : base(client, inputs) + { + } + } +} diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interface_DerivesFromOthers/IFoo2.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interface_DerivesFromOthers/IFoo2.g.cs index 104b5e1eb..7ed018ff0 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interface_DerivesFromOthers/IFoo2.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interface_DerivesFromOthers/IFoo2.g.cs @@ -46,7 +46,7 @@ public IFoo2_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.R if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("JustCancellationAsync"); - string rpcMethodName = this.transformedJustCancellationAsync1 ??= this.Options.MethodNameTransform("JustCancellationAsync"); + string rpcMethodName = this.transformedJustCancellationAsync1 ??= this.TransformMethodName("JustCancellationAsync", typeof(global::IFoo)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), JustCancellationAsyncNamedArgumentDeclaredTypes1, cancellationToken) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [], JustCancellationAsyncPositionalArgumentDeclaredTypes1, cancellationToken); @@ -65,7 +65,7 @@ public IFoo2_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.R if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("JustAnotherCancellationAsync"); - string rpcMethodName = this.transformedJustAnotherCancellationAsync2 ??= this.Options.MethodNameTransform("JustAnotherCancellationAsync"); + string rpcMethodName = this.transformedJustAnotherCancellationAsync2 ??= this.TransformMethodName("JustAnotherCancellationAsync", typeof(global::IFoo2)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), JustAnotherCancellationAsyncNamedArgumentDeclaredTypes2, cancellationToken) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [], JustAnotherCancellationAsyncPositionalArgumentDeclaredTypes2, cancellationToken); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interface_DerivesFromOthersWithRedundantMethods/ICalc.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interface_DerivesFromOthersWithRedundantMethods/ICalc.g.cs index cc174827a..6b827dc42 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interface_DerivesFromOthersWithRedundantMethods/ICalc.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interface_DerivesFromOthersWithRedundantMethods/ICalc.g.cs @@ -54,7 +54,7 @@ public ICalc_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.R if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("AddAsync"); - string rpcMethodName = this.transformedAddAsync1 ??= this.Options.MethodNameTransform("AddAsync"); + string rpcMethodName = this.transformedAddAsync1 ??= this.TransformMethodName("AddAsync", typeof(global::ICalc1)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), AddAsyncNamedArgumentDeclaredTypes1, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [a, b], AddAsyncPositionalArgumentDeclaredTypes1, default); @@ -75,7 +75,7 @@ public ICalc_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.R if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("AddAsync"); - string rpcMethodName = this.transformedAddAsync2 ??= this.Options.MethodNameTransform("AddAsync"); + string rpcMethodName = this.transformedAddAsync2 ??= this.TransformMethodName("AddAsync", typeof(global::ICalc2)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), AddAsyncNamedArgumentDeclaredTypes2, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [a, b], AddAsyncPositionalArgumentDeclaredTypes2, default); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interface_HasDisposeWithoutIDisposable/IFoo.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interface_HasAsyncDisposeWithoutIDisposable/IFoo.g.cs similarity index 94% rename from test/StreamJsonRpc.Analyzer.Tests/Resources/Interface_HasDisposeWithoutIDisposable/IFoo.g.cs rename to test/StreamJsonRpc.Analyzer.Tests/Resources/Interface_HasAsyncDisposeWithoutIDisposable/IFoo.g.cs index 55ac06e76..e722bc3e3 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interface_HasDisposeWithoutIDisposable/IFoo.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interface_HasAsyncDisposeWithoutIDisposable/IFoo.g.cs @@ -36,7 +36,7 @@ public IFoo_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.Re if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("Dispose"); - string rpcMethodName = this.transformedDispose1 ??= this.Options.MethodNameTransform("Dispose"); + string rpcMethodName = this.transformedDispose1 ??= this.TransformMethodName("Dispose", typeof(global::IFoo)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), DisposeNamedArgumentDeclaredTypes1, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [], DisposePositionalArgumentDeclaredTypes1, default); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/Interface_HasNestedTypes/IHaveNestedTypes.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interface_HasNestedTypes/IHaveNestedTypes.g.cs new file mode 100644 index 000000000..1df559114 --- /dev/null +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/Interface_HasNestedTypes/IHaveNestedTypes.g.cs @@ -0,0 +1,53 @@ +// + +#nullable enable +#pragma warning disable CS0436 // prefer local types to imported ones + +[global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.IHaveNestedTypes_Proxy))] +partial interface IHaveNestedTypes +{ +} + +namespace StreamJsonRpc.Generated +{ + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StreamJsonRpc.Analyzers", "x.x.x.x")] + internal class IHaveNestedTypes_Proxy : global::StreamJsonRpc.Reflection.ProxyBase + , global::IHaveNestedTypes + { + + private static readonly global::System.Collections.Generic.IReadOnlyDictionary DoSomethingAsyncNamedArgumentDeclaredTypes1 = new global::System.Collections.Generic.Dictionary + { + }; + + private static readonly global::System.Collections.Generic.IReadOnlyList DoSomethingAsyncPositionalArgumentDeclaredTypes1 = new global::System.Collections.Generic.List + { + }; + + private string? transformedDoSomethingAsync1; + + public IHaveNestedTypes_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.Reflection.ProxyInputs inputs) + : base(client, inputs) + { + } + + global::System.Threading.Tasks.Task global::IHaveNestedTypes.DoSomethingAsync() + { + if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); + + this.OnCallingMethod("DoSomethingAsync"); + string rpcMethodName = this.transformedDoSomethingAsync1 ??= this.TransformMethodName("DoSomethingAsync", typeof(global::IHaveNestedTypes)); + global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? + this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), DoSomethingAsyncNamedArgumentDeclaredTypes1, default) : + this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [], DoSomethingAsyncPositionalArgumentDeclaredTypes1, default); + this.OnCalledMethod("DoSomethingAsync"); + + return result; + + global::System.Collections.Generic.Dictionary ConstructNamedArgs() + => new() + { + }; + } + } +} diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/MethodNamesCustomizedByAttribute/IMyRpc.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/MethodNamesCustomizedByAttribute/IMyRpc.g.cs new file mode 100644 index 000000000..91fbc2a3d --- /dev/null +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/MethodNamesCustomizedByAttribute/IMyRpc.g.cs @@ -0,0 +1,129 @@ +// + +#nullable enable +#pragma warning disable CS0436 // prefer local types to imported ones + +[global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.IMyRpc_Proxy))] +partial interface IMyRpc +{ +} + +namespace StreamJsonRpc.Generated +{ + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StreamJsonRpc.Analyzers", "x.x.x.x")] + internal class IMyRpc_Proxy : global::StreamJsonRpc.Reflection.ProxyBase + , global::IMyRpc + { + + private static readonly global::System.Collections.Generic.IReadOnlyDictionary AddAsyncNamedArgumentDeclaredTypes1 = new global::System.Collections.Generic.Dictionary + { + ["a"] = typeof(int), + ["b"] = typeof(int), + }; + + private static readonly global::System.Collections.Generic.IReadOnlyList AddAsyncPositionalArgumentDeclaredTypes1 = new global::System.Collections.Generic.List + { + typeof(int), + typeof(int), + }; + + private string? transformedAddAsync1; + + private static readonly global::System.Collections.Generic.IReadOnlyDictionary IntegrateAsyncNamedArgumentDeclaredTypes2 = new global::System.Collections.Generic.Dictionary + { + ["from"] = typeof(double), + ["to"] = typeof(double), + }; + + private static readonly global::System.Collections.Generic.IReadOnlyList IntegrateAsyncPositionalArgumentDeclaredTypes2 = new global::System.Collections.Generic.List + { + typeof(double), + typeof(double), + }; + + private string? transformedIntegrateAsync2; + + private static readonly global::System.Collections.Generic.IReadOnlyDictionary DivideAsyncNamedArgumentDeclaredTypes3 = new global::System.Collections.Generic.Dictionary + { + ["from"] = typeof(double), + ["to"] = typeof(double), + }; + + private static readonly global::System.Collections.Generic.IReadOnlyList DivideAsyncPositionalArgumentDeclaredTypes3 = new global::System.Collections.Generic.List + { + typeof(double), + typeof(double), + }; + + private string? transformedDivideAsync3; + + public IMyRpc_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.Reflection.ProxyInputs inputs) + : base(client, inputs) + { + } + + global::System.Threading.Tasks.Task global::IMyRpc.AddAsync(int a, int b, global::System.Threading.CancellationToken cancellationToken) + { + if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); + + this.OnCallingMethod("AddAsync"); + string rpcMethodName = this.transformedAddAsync1 ??= this.TransformMethodName("AddRenamed", typeof(global::IMyRpc)); + global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? + this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), AddAsyncNamedArgumentDeclaredTypes1, cancellationToken) : + this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [a, b], AddAsyncPositionalArgumentDeclaredTypes1, cancellationToken); + this.OnCalledMethod("AddAsync"); + + return result; + + global::System.Collections.Generic.Dictionary ConstructNamedArgs() + => new() + { + ["a"] = a, + ["b"] = b, + }; + } + + global::System.Threading.Tasks.Task global::IMyRpc.IntegrateAsync(double from, double to, global::System.Threading.CancellationToken cancellationToken) + { + if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); + + this.OnCallingMethod("IntegrateAsync"); + string rpcMethodName = this.transformedIntegrateAsync2 ??= this.TransformMethodName("IntegrateRenamed", typeof(global::IMyRpc)); + global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? + this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), IntegrateAsyncNamedArgumentDeclaredTypes2, cancellationToken) : + this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [from, to], IntegrateAsyncPositionalArgumentDeclaredTypes2, cancellationToken); + this.OnCalledMethod("IntegrateAsync"); + + return result; + + global::System.Collections.Generic.Dictionary ConstructNamedArgs() + => new() + { + ["from"] = from, + ["to"] = to, + }; + } + + global::System.Threading.Tasks.Task global::IMyRpc.DivideAsync(double from, double to, global::System.Threading.CancellationToken cancellationToken) + { + if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); + + this.OnCallingMethod("DivideAsync"); + string rpcMethodName = this.transformedDivideAsync3 ??= this.TransformMethodName("DivideRenamed", typeof(global::IMyRpc)); + global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? + this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), DivideAsyncNamedArgumentDeclaredTypes3, cancellationToken) : + this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [from, to], DivideAsyncPositionalArgumentDeclaredTypes3, cancellationToken); + this.OnCalledMethod("DivideAsync"); + + return result; + + global::System.Collections.Generic.Dictionary ConstructNamedArgs() + => new() + { + ["from"] = from, + ["to"] = to, + }; + } + } +} diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/NameRequiredContainingTypeQualifier/A.IMyRpc.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/NameRequiredContainingTypeQualifier/A.IMyRpc.g.cs index 7082127f5..e59e7dbcc 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/NameRequiredContainingTypeQualifier/A.IMyRpc.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/NameRequiredContainingTypeQualifier/A.IMyRpc.g.cs @@ -31,7 +31,7 @@ public A_IMyRpc_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRp if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("JustCancellationAsync"); - string rpcMethodName = this.transformedJustCancellationAsync1 ??= this.Options.MethodNameTransform("JustCancellationAsync"); + string rpcMethodName = this.transformedJustCancellationAsync1 ??= this.TransformMethodName("JustCancellationAsync", typeof(global::A.IMyRpc)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), JustCancellationAsyncNamedArgumentDeclaredTypes1, cancellationToken) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [], JustCancellationAsyncPositionalArgumentDeclaredTypes1, cancellationToken); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/NameRequiredContainingTypeQualifier/B.IMyRpc.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/NameRequiredContainingTypeQualifier/B.IMyRpc.g.cs index b68c3d20a..48c9e6eb1 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/NameRequiredContainingTypeQualifier/B.IMyRpc.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/NameRequiredContainingTypeQualifier/B.IMyRpc.g.cs @@ -31,7 +31,7 @@ public B_IMyRpc_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRp if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("JustAnotherCancellationAsync"); - string rpcMethodName = this.transformedJustAnotherCancellationAsync1 ??= this.Options.MethodNameTransform("JustAnotherCancellationAsync"); + string rpcMethodName = this.transformedJustAnotherCancellationAsync1 ??= this.TransformMethodName("JustAnotherCancellationAsync", typeof(global::B.IMyRpc)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), JustAnotherCancellationAsyncNamedArgumentDeclaredTypes1, cancellationToken) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [], JustAnotherCancellationAsyncPositionalArgumentDeclaredTypes1, cancellationToken); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/NamesRequiredNamespaceQualifier/A.IMyRpc.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/NamesRequiredNamespaceQualifier/A.IMyRpc.g.cs index 5eca92e62..50320941a 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/NamesRequiredNamespaceQualifier/A.IMyRpc.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/NamesRequiredNamespaceQualifier/A.IMyRpc.g.cs @@ -39,7 +39,7 @@ public A_IMyRpc_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRp if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("JustCancellationAsync"); - string rpcMethodName = this.transformedJustCancellationAsync1 ??= this.Options.MethodNameTransform("JustCancellationAsync"); + string rpcMethodName = this.transformedJustCancellationAsync1 ??= this.TransformMethodName("JustCancellationAsync", typeof(global::A.IMyRpc)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), JustCancellationAsyncNamedArgumentDeclaredTypes1, cancellationToken) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [], JustCancellationAsyncPositionalArgumentDeclaredTypes1, cancellationToken); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/NamesRequiredNamespaceQualifier/B.IMyRpc.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/NamesRequiredNamespaceQualifier/B.IMyRpc.g.cs index 62c760a80..20a172953 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/NamesRequiredNamespaceQualifier/B.IMyRpc.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/NamesRequiredNamespaceQualifier/B.IMyRpc.g.cs @@ -39,7 +39,7 @@ public B_IMyRpc_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRp if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("JustAnotherCancellationAsync"); - string rpcMethodName = this.transformedJustAnotherCancellationAsync1 ??= this.Options.MethodNameTransform("JustAnotherCancellationAsync"); + string rpcMethodName = this.transformedJustAnotherCancellationAsync1 ??= this.TransformMethodName("JustAnotherCancellationAsync", typeof(global::B.IMyRpc)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), JustAnotherCancellationAsyncNamedArgumentDeclaredTypes1, cancellationToken) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [], JustAnotherCancellationAsyncPositionalArgumentDeclaredTypes1, cancellationToken); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/NestedInType/Wrapper.IMyRpc.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/NestedInType/Wrapper.IMyRpc.g.cs index 76d54542a..7ca463786 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/NestedInType/Wrapper.IMyRpc.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/NestedInType/Wrapper.IMyRpc.g.cs @@ -39,7 +39,7 @@ public Wrapper_IMyRpc_Proxy(global::StreamJsonRpc.JsonRpc client, global::Stream if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("JustCancellationAsync"); - string rpcMethodName = this.transformedJustCancellationAsync1 ??= this.Options.MethodNameTransform("JustCancellationAsync"); + string rpcMethodName = this.transformedJustCancellationAsync1 ??= this.TransformMethodName("JustCancellationAsync", typeof(global::Wrapper.IMyRpc)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), JustCancellationAsyncNamedArgumentDeclaredTypes1, cancellationToken) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [], JustCancellationAsyncPositionalArgumentDeclaredTypes1, cancellationToken); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/NestedInTypeAndNamespace/A.Wrapper.IMyRpc.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/NestedInTypeAndNamespace/A.Wrapper.IMyRpc.g.cs index 0eb1dbfc3..972b0d138 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/NestedInTypeAndNamespace/A.Wrapper.IMyRpc.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/NestedInTypeAndNamespace/A.Wrapper.IMyRpc.g.cs @@ -42,7 +42,7 @@ public A_Wrapper_IMyRpc_Proxy(global::StreamJsonRpc.JsonRpc client, global::Stre if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("JustCancellationAsync"); - string rpcMethodName = this.transformedJustCancellationAsync1 ??= this.Options.MethodNameTransform("JustCancellationAsync"); + string rpcMethodName = this.transformedJustCancellationAsync1 ??= this.TransformMethodName("JustCancellationAsync", typeof(global::A.Wrapper.IMyRpc)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), JustCancellationAsyncNamedArgumentDeclaredTypes1, cancellationToken) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [], JustCancellationAsyncPositionalArgumentDeclaredTypes1, cancellationToken); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/NonPartialNestedInPartialType/Wrapper.IMyRpc.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/NonPartialNestedInPartialType/Wrapper.IMyRpc.g.cs index 92adab2fa..e9706fa05 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/NonPartialNestedInPartialType/Wrapper.IMyRpc.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/NonPartialNestedInPartialType/Wrapper.IMyRpc.g.cs @@ -31,7 +31,7 @@ public Wrapper_IMyRpc_Proxy(global::StreamJsonRpc.JsonRpc client, global::Stream if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("JustCancellationAsync"); - string rpcMethodName = this.transformedJustCancellationAsync1 ??= this.Options.MethodNameTransform("JustCancellationAsync"); + string rpcMethodName = this.transformedJustCancellationAsync1 ??= this.TransformMethodName("JustCancellationAsync", typeof(global::Wrapper.IMyRpc)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), JustCancellationAsyncNamedArgumentDeclaredTypes1, cancellationToken) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [], JustCancellationAsyncPositionalArgumentDeclaredTypes1, cancellationToken); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/NullableTypeArgument/IMyService.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/NullableTypeArgument/IMyService.g.cs index 5ac44b41d..6f4a067a7 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/NullableTypeArgument/IMyService.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/NullableTypeArgument/IMyService.g.cs @@ -38,7 +38,7 @@ public IMyService_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJson if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("GetNullableIntAsync"); - string rpcMethodName = this.transformedGetNullableIntAsync1 ??= this.Options.MethodNameTransform("GetNullableIntAsync"); + string rpcMethodName = this.transformedGetNullableIntAsync1 ??= this.TransformMethodName("GetNullableIntAsync", typeof(global::IMyService)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), GetNullableIntAsyncNamedArgumentDeclaredTypes1, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [value], GetNullableIntAsyncPositionalArgumentDeclaredTypes1, default); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/Overloads/IFoo.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/Overloads/IFoo.g.cs index 1ea4dbfad..7d097d5ad 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/Overloads/IFoo.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/Overloads/IFoo.g.cs @@ -62,7 +62,7 @@ public IFoo_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.Re if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("SayHi"); - string rpcMethodName = this.transformedSayHi1 ??= this.Options.MethodNameTransform("SayHi"); + string rpcMethodName = this.transformedSayHi1 ??= this.TransformMethodName("SayHi", typeof(global::IFoo)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), SayHiNamedArgumentDeclaredTypes1, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [], SayHiPositionalArgumentDeclaredTypes1, default); @@ -81,7 +81,7 @@ public IFoo_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.Re if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("SayHi"); - string rpcMethodName = this.transformedSayHi2 ??= this.Options.MethodNameTransform("SayHi"); + string rpcMethodName = this.transformedSayHi2 ??= this.TransformMethodName("SayHi", typeof(global::IFoo)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), SayHiNamedArgumentDeclaredTypes2, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [name], SayHiPositionalArgumentDeclaredTypes2, default); @@ -101,7 +101,7 @@ public IFoo_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.Re if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("SayHi"); - string rpcMethodName = this.transformedSayHi3 ??= this.Options.MethodNameTransform("SayHi"); + string rpcMethodName = this.transformedSayHi3 ??= this.TransformMethodName("SayHi", typeof(global::IFoo)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), SayHiNamedArgumentDeclaredTypes3, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [name, age], SayHiPositionalArgumentDeclaredTypes3, default); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/PartialNestedInNonPartialType/Wrapper.IMyRpc.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/PartialNestedInNonPartialType/Wrapper.IMyRpc.g.cs index 92adab2fa..e9706fa05 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/PartialNestedInNonPartialType/Wrapper.IMyRpc.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/PartialNestedInNonPartialType/Wrapper.IMyRpc.g.cs @@ -31,7 +31,7 @@ public Wrapper_IMyRpc_Proxy(global::StreamJsonRpc.JsonRpc client, global::Stream if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("JustCancellationAsync"); - string rpcMethodName = this.transformedJustCancellationAsync1 ??= this.Options.MethodNameTransform("JustCancellationAsync"); + string rpcMethodName = this.transformedJustCancellationAsync1 ??= this.TransformMethodName("JustCancellationAsync", typeof(global::Wrapper.IMyRpc)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), JustCancellationAsyncNamedArgumentDeclaredTypes1, cancellationToken) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [], JustCancellationAsyncPositionalArgumentDeclaredTypes1, cancellationToken); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/Public_NotNested/IMyRpc.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/Public_NotNested/IMyRpc.g.cs index f0f72409e..dc40e414b 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/Public_NotNested/IMyRpc.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/Public_NotNested/IMyRpc.g.cs @@ -114,7 +114,7 @@ public IMyRpc_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc. if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("JustCancellationAsync"); - string rpcMethodName = this.transformedJustCancellationAsync1 ??= this.Options.MethodNameTransform("JustCancellationAsync"); + string rpcMethodName = this.transformedJustCancellationAsync1 ??= this.TransformMethodName("JustCancellationAsync", typeof(global::IMyRpc)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), JustCancellationAsyncNamedArgumentDeclaredTypes1, cancellationToken) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [], JustCancellationAsyncPositionalArgumentDeclaredTypes1, cancellationToken); @@ -133,7 +133,7 @@ public IMyRpc_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc. if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("AnArgAndCancellationAsync"); - string rpcMethodName = this.transformedAnArgAndCancellationAsync2 ??= this.Options.MethodNameTransform("AnArgAndCancellationAsync"); + string rpcMethodName = this.transformedAnArgAndCancellationAsync2 ??= this.TransformMethodName("AnArgAndCancellationAsync", typeof(global::IMyRpc)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), AnArgAndCancellationAsyncNamedArgumentDeclaredTypes2, cancellationToken) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [arg], AnArgAndCancellationAsyncPositionalArgumentDeclaredTypes2, cancellationToken); @@ -153,7 +153,7 @@ public IMyRpc_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc. if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("AddAsync"); - string rpcMethodName = this.transformedAddAsync3 ??= this.Options.MethodNameTransform("AddAsync"); + string rpcMethodName = this.transformedAddAsync3 ??= this.TransformMethodName("AddAsync", typeof(global::IMyRpc)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), AddAsyncNamedArgumentDeclaredTypes3, cancellationToken) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [a, b], AddAsyncPositionalArgumentDeclaredTypes3, cancellationToken); @@ -174,7 +174,7 @@ public IMyRpc_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc. if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("MultiplyAsync"); - string rpcMethodName = this.transformedMultiplyAsync4 ??= this.Options.MethodNameTransform("MultiplyAsync"); + string rpcMethodName = this.transformedMultiplyAsync4 ??= this.TransformMethodName("MultiplyAsync", typeof(global::IMyRpc)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), MultiplyAsyncNamedArgumentDeclaredTypes4, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [a, b], MultiplyAsyncPositionalArgumentDeclaredTypes4, default); @@ -195,7 +195,7 @@ public IMyRpc_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc. if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("Start"); - string rpcMethodName = this.transformedStart5 ??= this.Options.MethodNameTransform("Start"); + string rpcMethodName = this.transformedStart5 ??= this.TransformMethodName("Start", typeof(global::IMyRpc)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.NotifyWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), StartNamedArgumentDeclaredTypes5) : this.JsonRpc.NotifyAsync(rpcMethodName, [bah], StartPositionalArgumentDeclaredTypes5); @@ -215,7 +215,7 @@ public IMyRpc_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc. if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("StartCancelable"); - string rpcMethodName = this.transformedStartCancelable6 ??= this.Options.MethodNameTransform("StartCancelable"); + string rpcMethodName = this.transformedStartCancelable6 ??= this.TransformMethodName("StartCancelable", typeof(global::IMyRpc)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.NotifyWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), StartCancelableNamedArgumentDeclaredTypes6) : this.JsonRpc.NotifyAsync(rpcMethodName, [bah], StartCancelablePositionalArgumentDeclaredTypes6); @@ -235,7 +235,7 @@ public IMyRpc_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc. if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("CountAsync"); - string rpcMethodName = this.transformedCountAsync7 ??= this.Options.MethodNameTransform("CountAsync"); + string rpcMethodName = this.transformedCountAsync7 ??= this.TransformMethodName("CountAsync", typeof(global::IMyRpc)); global::System.Threading.Tasks.Task> result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync>(rpcMethodName, ConstructNamedArgs(), CountAsyncNamedArgumentDeclaredTypes7, cancellationToken) : this.JsonRpc.InvokeWithCancellationAsync>(rpcMethodName, [start, count], CountAsyncPositionalArgumentDeclaredTypes7, cancellationToken); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable/IMyRpc.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable/IMyRpc.g.cs index f0f72409e..dc40e414b 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable/IMyRpc.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable/IMyRpc.g.cs @@ -114,7 +114,7 @@ public IMyRpc_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc. if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("JustCancellationAsync"); - string rpcMethodName = this.transformedJustCancellationAsync1 ??= this.Options.MethodNameTransform("JustCancellationAsync"); + string rpcMethodName = this.transformedJustCancellationAsync1 ??= this.TransformMethodName("JustCancellationAsync", typeof(global::IMyRpc)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), JustCancellationAsyncNamedArgumentDeclaredTypes1, cancellationToken) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [], JustCancellationAsyncPositionalArgumentDeclaredTypes1, cancellationToken); @@ -133,7 +133,7 @@ public IMyRpc_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc. if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("AnArgAndCancellationAsync"); - string rpcMethodName = this.transformedAnArgAndCancellationAsync2 ??= this.Options.MethodNameTransform("AnArgAndCancellationAsync"); + string rpcMethodName = this.transformedAnArgAndCancellationAsync2 ??= this.TransformMethodName("AnArgAndCancellationAsync", typeof(global::IMyRpc)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), AnArgAndCancellationAsyncNamedArgumentDeclaredTypes2, cancellationToken) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [arg], AnArgAndCancellationAsyncPositionalArgumentDeclaredTypes2, cancellationToken); @@ -153,7 +153,7 @@ public IMyRpc_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc. if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("AddAsync"); - string rpcMethodName = this.transformedAddAsync3 ??= this.Options.MethodNameTransform("AddAsync"); + string rpcMethodName = this.transformedAddAsync3 ??= this.TransformMethodName("AddAsync", typeof(global::IMyRpc)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), AddAsyncNamedArgumentDeclaredTypes3, cancellationToken) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [a, b], AddAsyncPositionalArgumentDeclaredTypes3, cancellationToken); @@ -174,7 +174,7 @@ public IMyRpc_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc. if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("MultiplyAsync"); - string rpcMethodName = this.transformedMultiplyAsync4 ??= this.Options.MethodNameTransform("MultiplyAsync"); + string rpcMethodName = this.transformedMultiplyAsync4 ??= this.TransformMethodName("MultiplyAsync", typeof(global::IMyRpc)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), MultiplyAsyncNamedArgumentDeclaredTypes4, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [a, b], MultiplyAsyncPositionalArgumentDeclaredTypes4, default); @@ -195,7 +195,7 @@ public IMyRpc_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc. if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("Start"); - string rpcMethodName = this.transformedStart5 ??= this.Options.MethodNameTransform("Start"); + string rpcMethodName = this.transformedStart5 ??= this.TransformMethodName("Start", typeof(global::IMyRpc)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.NotifyWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), StartNamedArgumentDeclaredTypes5) : this.JsonRpc.NotifyAsync(rpcMethodName, [bah], StartPositionalArgumentDeclaredTypes5); @@ -215,7 +215,7 @@ public IMyRpc_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc. if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("StartCancelable"); - string rpcMethodName = this.transformedStartCancelable6 ??= this.Options.MethodNameTransform("StartCancelable"); + string rpcMethodName = this.transformedStartCancelable6 ??= this.TransformMethodName("StartCancelable", typeof(global::IMyRpc)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.NotifyWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), StartCancelableNamedArgumentDeclaredTypes6) : this.JsonRpc.NotifyAsync(rpcMethodName, [bah], StartCancelablePositionalArgumentDeclaredTypes6); @@ -235,7 +235,7 @@ public IMyRpc_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc. if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("CountAsync"); - string rpcMethodName = this.transformedCountAsync7 ??= this.Options.MethodNameTransform("CountAsync"); + string rpcMethodName = this.transformedCountAsync7 ??= this.TransformMethodName("CountAsync", typeof(global::IMyRpc)); global::System.Threading.Tasks.Task> result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync>(rpcMethodName, ConstructNamedArgs(), CountAsyncNamedArgumentDeclaredTypes7, cancellationToken) : this.JsonRpc.InvokeWithCancellationAsync>(rpcMethodName, [start, count], CountAsyncPositionalArgumentDeclaredTypes7, cancellationToken); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_Generic/IGenericMarshalable_T_.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_Generic/IGenericMarshalable_T_.g.cs index 5b280ef57..78719bf09 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_Generic/IGenericMarshalable_T_.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_Generic/IGenericMarshalable_T_.g.cs @@ -38,7 +38,7 @@ public IGenericMarshalable_Proxy(global::StreamJsonRpc.JsonRpc client, global::S if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); this.OnCallingMethod("DoSomethingWithParameterAsync"); - string rpcMethodName = this.transformedDoSomethingWithParameterAsync1 ??= this.Options.MethodNameTransform("DoSomethingWithParameterAsync"); + string rpcMethodName = this.transformedDoSomethingWithParameterAsync1 ??= this.TransformMethodName("DoSomethingWithParameterAsync", typeof(global::IGenericMarshalable)); global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), DoSomethingWithParameterAsyncNamedArgumentDeclaredTypes1, default) : this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [parameter], DoSomethingWithParameterAsyncPositionalArgumentDeclaredTypes1, default); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_GenericWithClosedPrescriptions/IGenericMarshalable_T_.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_GenericWithClosedPrescriptions/IGenericMarshalable_T_.g.cs new file mode 100644 index 000000000..a6fa3a196 --- /dev/null +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_GenericWithClosedPrescriptions/IGenericMarshalable_T_.g.cs @@ -0,0 +1,57 @@ +// + +#nullable enable +#pragma warning disable CS0436 // prefer local types to imported ones + +[global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.IGenericMarshalable_Proxy<>))] +[global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.IGenericMarshalable_Proxy))] +partial interface IGenericMarshalable +{ +} + +namespace StreamJsonRpc.Generated +{ + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StreamJsonRpc.Analyzers", "x.x.x.x")] + internal class IGenericMarshalable_Proxy : global::StreamJsonRpc.Reflection.ProxyBase + , global::IGenericMarshalable + { + + private static readonly global::System.Collections.Generic.IReadOnlyDictionary DoSomethingWithParameterAsyncNamedArgumentDeclaredTypes1 = new global::System.Collections.Generic.Dictionary + { + ["parameter"] = typeof(T), + }; + + private static readonly global::System.Collections.Generic.IReadOnlyList DoSomethingWithParameterAsyncPositionalArgumentDeclaredTypes1 = new global::System.Collections.Generic.List + { + typeof(T), + }; + + private string? transformedDoSomethingWithParameterAsync1; + + public IGenericMarshalable_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.Reflection.ProxyInputs inputs) + : base(client, inputs) + { + } + + global::System.Threading.Tasks.Task global::IGenericMarshalable.DoSomethingWithParameterAsync(T parameter) + { + if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); + + this.OnCallingMethod("DoSomethingWithParameterAsync"); + string rpcMethodName = this.transformedDoSomethingWithParameterAsync1 ??= this.TransformMethodName("DoSomethingWithParameterAsync", typeof(global::IGenericMarshalable)); + global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? + this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), DoSomethingWithParameterAsyncNamedArgumentDeclaredTypes1, default) : + this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [parameter], DoSomethingWithParameterAsyncPositionalArgumentDeclaredTypes1, default); + this.OnCalledMethod("DoSomethingWithParameterAsync"); + + return result; + + global::System.Collections.Generic.Dictionary ConstructNamedArgs() + => new() + { + ["parameter"] = parameter, + }; + } + } +} diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_GenericWithClosedPrescriptions_Arity2/IGenericMarshalable_T1, T2_.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_GenericWithClosedPrescriptions_Arity2/IGenericMarshalable_T1, T2_.g.cs new file mode 100644 index 000000000..00a4c8402 --- /dev/null +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_GenericWithClosedPrescriptions_Arity2/IGenericMarshalable_T1, T2_.g.cs @@ -0,0 +1,57 @@ +// + +#nullable enable +#pragma warning disable CS0436 // prefer local types to imported ones + +[global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.IGenericMarshalable_Proxy<,>))] +[global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.IGenericMarshalable_Proxy))] +partial interface IGenericMarshalable +{ +} + +namespace StreamJsonRpc.Generated +{ + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StreamJsonRpc.Analyzers", "x.x.x.x")] + internal class IGenericMarshalable_Proxy : global::StreamJsonRpc.Reflection.ProxyBase + , global::IGenericMarshalable + { + + private static readonly global::System.Collections.Generic.IReadOnlyDictionary DoSomethingWithParameterAsyncNamedArgumentDeclaredTypes1 = new global::System.Collections.Generic.Dictionary + { + ["parameter"] = typeof(T2), + }; + + private static readonly global::System.Collections.Generic.IReadOnlyList DoSomethingWithParameterAsyncPositionalArgumentDeclaredTypes1 = new global::System.Collections.Generic.List + { + typeof(T2), + }; + + private string? transformedDoSomethingWithParameterAsync1; + + public IGenericMarshalable_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.Reflection.ProxyInputs inputs) + : base(client, inputs) + { + } + + global::System.Threading.Tasks.Task global::IGenericMarshalable.DoSomethingWithParameterAsync(T2 parameter) + { + if (this.IsDisposed) throw new global::System.ObjectDisposedException(this.GetType().FullName); + + this.OnCallingMethod("DoSomethingWithParameterAsync"); + string rpcMethodName = this.transformedDoSomethingWithParameterAsync1 ??= this.TransformMethodName("DoSomethingWithParameterAsync", typeof(global::IGenericMarshalable)); + global::System.Threading.Tasks.Task result = this.Options.ServerRequiresNamedArguments ? + this.JsonRpc.InvokeWithParameterObjectAsync(rpcMethodName, ConstructNamedArgs(), DoSomethingWithParameterAsyncNamedArgumentDeclaredTypes1, default) : + this.JsonRpc.InvokeWithCancellationAsync(rpcMethodName, [parameter], DoSomethingWithParameterAsyncPositionalArgumentDeclaredTypes1, default); + this.OnCalledMethod("DoSomethingWithParameterAsync"); + + return result; + + global::System.Collections.Generic.Dictionary ConstructNamedArgs() + => new() + { + ["parameter"] = parameter, + }; + } + } +} diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_HasEvent/IMarshalableWithEvents.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_HasEvent/IMarshalableWithEvents.g.cs index fac795f99..4f3711770 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_HasEvent/IMarshalableWithEvents.g.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_HasEvent/IMarshalableWithEvents.g.cs @@ -19,7 +19,7 @@ internal class IMarshalableWithEvents_Proxy : global::StreamJsonRpc.Reflection.P public IMarshalableWithEvents_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.Reflection.ProxyInputs inputs) : base(client, inputs) { - this.JsonRpc.AddLocalRpcMethod(this.Options.EventNameTransform("Changed"), this.OnChanged); + this.JsonRpc.AddLocalRpcMethod(this.TransformEventName("Changed", typeof(global::IMarshalableWithEvents)), this.OnChanged); } public event global::System.EventHandler? Changed; diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_HasStaticMethod/IMarshalableWithProperties.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_HasStaticMethod/IMarshalableWithProperties.g.cs new file mode 100644 index 000000000..4e54b128d --- /dev/null +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_HasStaticMethod/IMarshalableWithProperties.g.cs @@ -0,0 +1,24 @@ +// + +#nullable enable +#pragma warning disable CS0436 // prefer local types to imported ones + +[global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.IMarshalableWithProperties_Proxy))] +partial interface IMarshalableWithProperties +{ +} + +namespace StreamJsonRpc.Generated +{ + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StreamJsonRpc.Analyzers", "x.x.x.x")] + internal class IMarshalableWithProperties_Proxy : global::StreamJsonRpc.Reflection.ProxyBase + , global::IMarshalableWithProperties + { + + public IMarshalableWithProperties_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.Reflection.ProxyInputs inputs) + : base(client, inputs) + { + } + } +} diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods/IMarshalable.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods/IMarshalable.g.cs new file mode 100644 index 000000000..6467f1f6a --- /dev/null +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods/IMarshalable.g.cs @@ -0,0 +1,24 @@ +// + +#nullable enable +#pragma warning disable CS0436 // prefer local types to imported ones + +[global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.IMarshalable_Proxy))] +partial interface IMarshalable +{ +} + +namespace StreamJsonRpc.Generated +{ + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StreamJsonRpc.Analyzers", "x.x.x.x")] + internal class IMarshalable_Proxy : global::StreamJsonRpc.Reflection.ProxyBase + , global::IMarshalable + { + + public IMarshalable_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.Reflection.ProxyInputs inputs) + : base(client, inputs) + { + } + } +} diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods/IMarshalableSobURHW8.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods/IMarshalableSobURHW8.g.cs new file mode 100644 index 000000000..722c62e0e --- /dev/null +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods/IMarshalableSobURHW8.g.cs @@ -0,0 +1,30 @@ +// + +#nullable enable +#pragma warning disable CS0436 // prefer local types to imported ones + +[global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.IMarshalableSobURHW8_Proxy))] +partial interface IMarshalable +{ +} + +[global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.IMarshalableSobURHW8_Proxy))] +partial interface IOptional +{ +} + +namespace StreamJsonRpc.Generated +{ + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StreamJsonRpc.Analyzers", "x.x.x.x")] + internal class IMarshalableSobURHW8_Proxy : global::StreamJsonRpc.Reflection.ProxyBase + , global::IMarshalable + , global::IOptional + { + + public IMarshalableSobURHW8_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.Reflection.ProxyInputs inputs) + : base(client, inputs) + { + } + } +} diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods/OptionalInterfaceExtensions.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods/OptionalInterfaceExtensions.g.cs new file mode 100644 index 000000000..a23bf04bf --- /dev/null +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods/OptionalInterfaceExtensions.g.cs @@ -0,0 +1,22 @@ +// + +#nullable enable +#pragma warning disable CS0436 // prefer local types to imported ones + +using StreamJsonRpc; + +/// Extension methods for interfaces acting as optional interfaces on proxies. +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("StreamJsonRpc.Analyzers", "x.x.x.x")] +[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] +public static partial class StreamJsonRpcOptionalInterfaceAccessors +{ + /// + public static bool Is(this IMarshalable self, global::System.Type type) => self is global::StreamJsonRpc.IClientProxy proxy ? proxy.Is(type) : type.IsAssignableFrom(self.GetType()); + /// + public static T? As(this IMarshalable self) where T : class => self is global::StreamJsonRpc.IClientProxy proxy ? proxy.As() : self as T; + + /// + internal static bool Is(this IOptional self, global::System.Type type) => self is global::StreamJsonRpc.IClientProxy proxy ? proxy.Is(type) : type.IsAssignableFrom(self.GetType()); + /// + internal static T? As(this IOptional self) where T : class => self is global::StreamJsonRpc.IClientProxy proxy ? proxy.As() : self as T; +} diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods_NestedInClass/NS.Wrapper.IMarshalable.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods_NestedInClass/NS.Wrapper.IMarshalable.g.cs new file mode 100644 index 000000000..22bfb80e7 --- /dev/null +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods_NestedInClass/NS.Wrapper.IMarshalable.g.cs @@ -0,0 +1,30 @@ +// + +#nullable enable +#pragma warning disable CS0436 // prefer local types to imported ones + +namespace NS +{ + partial class Wrapper + { + [global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.NS_Wrapper_IMarshalable_Proxy))] + partial interface IMarshalable + { + } + } +} + +namespace StreamJsonRpc.Generated +{ + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StreamJsonRpc.Analyzers", "x.x.x.x")] + internal class NS_Wrapper_IMarshalable_Proxy : global::StreamJsonRpc.Reflection.ProxyBase + , global::NS.Wrapper.IMarshalable + { + + public NS_Wrapper_IMarshalable_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.Reflection.ProxyInputs inputs) + : base(client, inputs) + { + } + } +} diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods_NestedInClass/NS.Wrapper.IMarshalablegBnetfdo.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods_NestedInClass/NS.Wrapper.IMarshalablegBnetfdo.g.cs new file mode 100644 index 000000000..4c0566cba --- /dev/null +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods_NestedInClass/NS.Wrapper.IMarshalablegBnetfdo.g.cs @@ -0,0 +1,42 @@ +// + +#nullable enable +#pragma warning disable CS0436 // prefer local types to imported ones + +namespace NS +{ + partial class Wrapper + { + [global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.NS_Wrapper_IMarshalablegBnetfdo_Proxy))] + partial interface IMarshalable + { + } + } +} + +namespace NS +{ + partial class Wrapper + { + [global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.NS_Wrapper_IMarshalablegBnetfdo_Proxy))] + partial interface IOptional + { + } + } +} + +namespace StreamJsonRpc.Generated +{ + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StreamJsonRpc.Analyzers", "x.x.x.x")] + internal class NS_Wrapper_IMarshalablegBnetfdo_Proxy : global::StreamJsonRpc.Reflection.ProxyBase + , global::NS.Wrapper.IMarshalable + , global::NS.Wrapper.IOptional + { + + public NS_Wrapper_IMarshalablegBnetfdo_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.Reflection.ProxyInputs inputs) + : base(client, inputs) + { + } + } +} diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods_NestedInClass/OptionalInterfaceExtensions.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods_NestedInClass/OptionalInterfaceExtensions.g.cs new file mode 100644 index 000000000..9d29a4aaa --- /dev/null +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods_NestedInClass/OptionalInterfaceExtensions.g.cs @@ -0,0 +1,24 @@ +// + +#nullable enable +#pragma warning disable CS0436 // prefer local types to imported ones + +using StreamJsonRpc; + +namespace NS +{ + /// Extension methods for interfaces acting as optional interfaces on proxies. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StreamJsonRpc.Analyzers", "x.x.x.x")] + internal static partial class StreamJsonRpcOptionalInterfaceAccessors + { + /// + internal static bool Is(this NS.Wrapper.IMarshalable self, global::System.Type type) => self is global::StreamJsonRpc.IClientProxy proxy ? proxy.Is(type) : type.IsAssignableFrom(self.GetType()); + /// + internal static T? As(this NS.Wrapper.IMarshalable self) where T : class => self is global::StreamJsonRpc.IClientProxy proxy ? proxy.As() : self as T; + + /// + internal static bool Is(this NS.Wrapper.IOptional self, global::System.Type type) => self is global::StreamJsonRpc.IClientProxy proxy ? proxy.Is(type) : type.IsAssignableFrom(self.GetType()); + /// + internal static T? As(this NS.Wrapper.IOptional self) where T : class => self is global::StreamJsonRpc.IClientProxy proxy ? proxy.As() : self as T; + } +} diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods_NotPublic/IMarshalable.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods_NotPublic/IMarshalable.g.cs new file mode 100644 index 000000000..6467f1f6a --- /dev/null +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods_NotPublic/IMarshalable.g.cs @@ -0,0 +1,24 @@ +// + +#nullable enable +#pragma warning disable CS0436 // prefer local types to imported ones + +[global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.IMarshalable_Proxy))] +partial interface IMarshalable +{ +} + +namespace StreamJsonRpc.Generated +{ + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StreamJsonRpc.Analyzers", "x.x.x.x")] + internal class IMarshalable_Proxy : global::StreamJsonRpc.Reflection.ProxyBase + , global::IMarshalable + { + + public IMarshalable_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.Reflection.ProxyInputs inputs) + : base(client, inputs) + { + } + } +} diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods_NotPublic/IMarshalableSobURHW8.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods_NotPublic/IMarshalableSobURHW8.g.cs new file mode 100644 index 000000000..722c62e0e --- /dev/null +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods_NotPublic/IMarshalableSobURHW8.g.cs @@ -0,0 +1,30 @@ +// + +#nullable enable +#pragma warning disable CS0436 // prefer local types to imported ones + +[global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.IMarshalableSobURHW8_Proxy))] +partial interface IMarshalable +{ +} + +[global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof(StreamJsonRpc.Generated.IMarshalableSobURHW8_Proxy))] +partial interface IOptional +{ +} + +namespace StreamJsonRpc.Generated +{ + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("StreamJsonRpc.Analyzers", "x.x.x.x")] + internal class IMarshalableSobURHW8_Proxy : global::StreamJsonRpc.Reflection.ProxyBase + , global::IMarshalable + , global::IOptional + { + + public IMarshalableSobURHW8_Proxy(global::StreamJsonRpc.JsonRpc client, global::StreamJsonRpc.Reflection.ProxyInputs inputs) + : base(client, inputs) + { + } + } +} diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods_NotPublic/OptionalInterfaceExtensions.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods_NotPublic/OptionalInterfaceExtensions.g.cs new file mode 100644 index 000000000..ef399fd27 --- /dev/null +++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_OptionalInterfaces_WithExtensionMethods_NotPublic/OptionalInterfaceExtensions.g.cs @@ -0,0 +1,21 @@ +// + +#nullable enable +#pragma warning disable CS0436 // prefer local types to imported ones + +using StreamJsonRpc; + +/// Extension methods for interfaces acting as optional interfaces on proxies. +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("StreamJsonRpc.Analyzers", "x.x.x.x")] +internal static partial class StreamJsonRpcOptionalInterfaceAccessors +{ + /// + public static bool Is(this IMarshalable self, global::System.Type type) => self is global::StreamJsonRpc.IClientProxy proxy ? proxy.Is(type) : type.IsAssignableFrom(self.GetType()); + /// + public static T? As(this IMarshalable self) where T : class => self is global::StreamJsonRpc.IClientProxy proxy ? proxy.As() : self as T; + + /// + internal static bool Is(this IOptional self, global::System.Type type) => self is global::StreamJsonRpc.IClientProxy proxy ? proxy.Is(type) : type.IsAssignableFrom(self.GetType()); + /// + internal static T? As(this IOptional self) where T : class => self is global::StreamJsonRpc.IClientProxy proxy ? proxy.As() : self as T; +} diff --git a/test/StreamJsonRpc.Analyzer.Tests/Verifiers/CSharpSourceGeneratorVerifier`1.cs b/test/StreamJsonRpc.Analyzer.Tests/Verifiers/CSharpSourceGeneratorVerifier`1.cs index 511edcf3f..d062f73f0 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Verifiers/CSharpSourceGeneratorVerifier`1.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Verifiers/CSharpSourceGeneratorVerifier`1.cs @@ -32,7 +32,7 @@ internal static partial class CSharpSourceGeneratorVerifier private const LanguageVersion DefaultLanguageVersion = LanguageVersion.CSharp7_3; - public static Task RunDefaultAsync([StringSyntax("c#-test")] string testSource, LanguageVersion languageVersion = DefaultLanguageVersion, [CallerFilePath] string testFile = null!, [CallerMemberName] string testMethod = null!) + public static Task RunDefaultAsync([StringSyntax("c#-test")] string testSource, LanguageVersion languageVersion = DefaultLanguageVersion, GeneratorConfiguration? configuration = null, [CallerFilePath] string testFile = null!, [CallerMemberName] string testMethod = null!) { Test test = new(testFile: testFile, testMethod: testMethod) { @@ -47,6 +47,7 @@ public static Task RunDefaultAsync([StringSyntax("c#-test")] string testSource, }, }, LanguageVersion = languageVersion, + GeneratorConfiguration = configuration ?? GeneratorConfiguration.Default, }; return test.RunDefaultAsync(testSource); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Verifiers/CodeFixVerifier{TAnalyzer,TCodeFix}.cs b/test/StreamJsonRpc.Analyzer.Tests/Verifiers/CodeFixVerifier{TAnalyzer,TCodeFix}.cs index 1222418d7..04230ed50 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Verifiers/CodeFixVerifier{TAnalyzer,TCodeFix}.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Verifiers/CodeFixVerifier{TAnalyzer,TCodeFix}.cs @@ -30,6 +30,7 @@ public static async Task VerifyAnalyzerAsync([StringSyntax("c#-test")] string so using System; using System.Threading; using System.Threading.Tasks; + using PolyType; using StreamJsonRpc; {source} diff --git a/test/StreamJsonRpc.Analyzer.Tests/Verifiers/GeneratorConfiguration.cs b/test/StreamJsonRpc.Analyzer.Tests/Verifiers/GeneratorConfiguration.cs index f0a30be20..81c746a99 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Verifiers/GeneratorConfiguration.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Verifiers/GeneratorConfiguration.cs @@ -9,12 +9,15 @@ internal record GeneratorConfiguration internal bool InterceptorsEnabled { get; init; } = true; + internal bool PublicRpcMarshalableInterfaceExtensions { get; init; } = true; + internal string ToGlobalConfigString() { StringBuilder globalConfigBuilder = new(); globalConfigBuilder.AppendLine("is_global = true"); globalConfigBuilder.AppendLine(); AddProperty("EnableStreamJsonRpcInterceptors", this.InterceptorsEnabled ? "true" : "false"); + AddProperty("PublicRpcMarshalableInterfaceExtensions", this.PublicRpcMarshalableInterfaceExtensions ? "true" : "false"); return globalConfigBuilder.ToString(); diff --git a/test/StreamJsonRpc.Analyzer.Tests/Verifiers/ReferencesHelper.cs b/test/StreamJsonRpc.Analyzer.Tests/Verifiers/ReferencesHelper.cs index 2806df74c..8fd36a8a3 100644 --- a/test/StreamJsonRpc.Analyzer.Tests/Verifiers/ReferencesHelper.cs +++ b/test/StreamJsonRpc.Analyzer.Tests/Verifiers/ReferencesHelper.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft; +using PolyType; internal static class ReferencesHelper { @@ -19,6 +20,7 @@ internal static class ReferencesHelper internal static IEnumerable GetReferences() { yield return MetadataReference.CreateFromFile(typeof(JsonRpc).Assembly.Location); + yield return MetadataReference.CreateFromFile(typeof(GenerateShapeAttribute).Assembly.Location); yield return MetadataReference.CreateFromFile(typeof(IDisposableObservable).Assembly.Location); } } diff --git a/test/StreamJsonRpc.Tests.ExternalAssembly/IPublicNestedInInternalInterface.cs b/test/StreamJsonRpc.Tests.ExternalAssembly/IPublicNestedInInternalInterface.cs index 6cf7c946d..e38962fbd 100644 --- a/test/StreamJsonRpc.Tests.ExternalAssembly/IPublicNestedInInternalInterface.cs +++ b/test/StreamJsonRpc.Tests.ExternalAssembly/IPublicNestedInInternalInterface.cs @@ -7,7 +7,7 @@ namespace StreamJsonRpc.Tests.ExternalAssembly; internal partial interface IInternal { - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IPublicNestedInInternalInterface { Task SubtractAsync(int a, int b); diff --git a/test/StreamJsonRpc.Tests.ExternalAssembly/ISomeInternalProxyInterface.cs b/test/StreamJsonRpc.Tests.ExternalAssembly/ISomeInternalProxyInterface.cs index bf8c946ff..92b10f06a 100644 --- a/test/StreamJsonRpc.Tests.ExternalAssembly/ISomeInternalProxyInterface.cs +++ b/test/StreamJsonRpc.Tests.ExternalAssembly/ISomeInternalProxyInterface.cs @@ -3,7 +3,7 @@ namespace StreamJsonRpc.Tests.ExternalAssembly; -[JsonRpcContract] +[JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] internal partial interface ISomeInternalProxyInterface { Task SubtractAsync(int a, int b); diff --git a/test/StreamJsonRpc.Tests.ExternalAssembly/Usings.cs b/test/StreamJsonRpc.Tests.ExternalAssembly/Usings.cs new file mode 100644 index 000000000..c59ed9bea --- /dev/null +++ b/test/StreamJsonRpc.Tests.ExternalAssembly/Usings.cs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +global using PolyType; diff --git a/test/StreamJsonRpc.Tests.NoInterceptors/StreamJsonRpc.Tests.NoInterceptors.csproj b/test/StreamJsonRpc.Tests.NoInterceptors/StreamJsonRpc.Tests.NoInterceptors.csproj index ae705836b..ef757ccc1 100644 --- a/test/StreamJsonRpc.Tests.NoInterceptors/StreamJsonRpc.Tests.NoInterceptors.csproj +++ b/test/StreamJsonRpc.Tests.NoInterceptors/StreamJsonRpc.Tests.NoInterceptors.csproj @@ -9,6 +9,8 @@ to avoid proliferation of #if sections in our test code. So suppress it unless we're targeting the oldest framework among our targets. --> $(NoWarn);xUnit1051 + + $(NoWarn);CS0436 true $(DefineConstants);NO_INTERCEPTORS diff --git a/test/StreamJsonRpc.Tests/ArchitectureTests.cs b/test/StreamJsonRpc.Tests/ArchitectureTests.cs index 1e88cf3f6..5ea1e31a4 100644 --- a/test/StreamJsonRpc.Tests/ArchitectureTests.cs +++ b/test/StreamJsonRpc.Tests/ArchitectureTests.cs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Reflection; +using System.Runtime.CompilerServices; + public class ArchitectureTests { /// @@ -14,6 +17,6 @@ public class ArchitectureTests [Fact] public void NoInterceptors() { - Assert.DoesNotContain(typeof(JsonRpc).Assembly.GetTypes(), t => t.Namespace == "StreamJsonRpc.Generated"); + Assert.DoesNotContain(typeof(JsonRpc).Assembly.GetTypes(), t => t.Namespace == "StreamJsonRpc.Generated" && t.Name != "StreamJsonRpc_Reflection_ProxyBase_IObserverProxyGenerator_Proxy`1" && t.GetCustomAttribute() is null); } } diff --git a/test/StreamJsonRpc.Tests/AsyncEnumerableTests.cs b/test/StreamJsonRpc.Tests/AsyncEnumerableTests.cs index 88dc68437..05a8f404b 100644 --- a/test/StreamJsonRpc.Tests/AsyncEnumerableTests.cs +++ b/test/StreamJsonRpc.Tests/AsyncEnumerableTests.cs @@ -38,7 +38,7 @@ protected AsyncEnumerableTests(ITestOutputHelper logger) /// since the server implements the methods on this interface with a return type of Task{T} /// but we want the client proxy to NOT be that. /// - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IServer2 { IAsyncEnumerable WaitTillCanceledBeforeReturningAsync(CancellationToken cancellationToken); @@ -46,7 +46,7 @@ public partial interface IServer2 IAsyncEnumerable GetNumbersParameterizedAsync(int batchSize, int readAhead, int prefetch, int totalCount, bool endWithException, CancellationToken cancellationToken); } - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IServer { IAsyncEnumerable GetValuesFromEnumeratedSourceAsync(CancellationToken cancellationToken); @@ -80,7 +80,7 @@ public partial interface IServer IAsyncEnumerable CallbackClientAndYieldOneValueAsync(CancellationToken cancellationToken); } - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IClient { Task DoSomethingAsync(CancellationToken cancellationToken); diff --git a/test/StreamJsonRpc.Tests/DisposableProxyTests.cs b/test/StreamJsonRpc.Tests/DisposableProxyTests.cs index 621888f60..deda22b6a 100644 --- a/test/StreamJsonRpc.Tests/DisposableProxyTests.cs +++ b/test/StreamJsonRpc.Tests/DisposableProxyTests.cs @@ -39,7 +39,7 @@ protected DisposableProxyTests(ITestOutputHelper logger) this.serverRpc.StartListening(); } - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IServer { Task GetDisposableAsync(bool returnNull = false); @@ -74,9 +74,6 @@ public async Task NoLeakWhenServerThrows() [Fact] public async Task IDisposableInNotificationArgumentIsRejected() { -#if !NBMSGPACK_MARSHALING_SUPPORT - Assert.SkipWhen(this is DisposableProxyNerdbankMessagePackTests, "NerdbankMessagePackFormatter does not yet support marshaled objects."); -#endif var ex = await Assert.ThrowsAnyAsync(() => this.clientRpc.NotifyAsync("someMethod", new object?[] { new DisposableAction(() => { }) }, new Type[] { typeof(IDisposable) })); Assert.True(IsExceptionOrInnerOfType(ex)); } @@ -84,9 +81,6 @@ public async Task IDisposableInNotificationArgumentIsRejected() [Fact] public async Task DisposableReturnValue_DisposeSwallowsSecondCall() { -#if !NBMSGPACK_MARSHALING_SUPPORT - Assert.SkipWhen(this is DisposableProxyNerdbankMessagePackTests, "NerdbankMessagePackFormatter does not yet support marshaled objects."); -#endif IDisposable? proxyDisposable = await this.client.GetDisposableAsync(); Assumes.NotNull(proxyDisposable); proxyDisposable.Dispose(); @@ -96,9 +90,6 @@ public async Task DisposableReturnValue_DisposeSwallowsSecondCall() [Fact] public async Task DisposableReturnValue_IsMarshaledAndLaterCollected() { -#if !NBMSGPACK_MARSHALING_SUPPORT - Assert.SkipWhen(this is DisposableProxyNerdbankMessagePackTests, "NerdbankMessagePackFormatter does not yet support marshaled objects."); -#endif var weakRefs = await this.DisposableReturnValue_Helper(); await this.AssertWeakReferenceGetsCollectedAsync(weakRefs.Proxy); await this.AssertWeakReferenceGetsCollectedAsync(weakRefs.Target); @@ -107,9 +98,6 @@ public async Task DisposableReturnValue_IsMarshaledAndLaterCollected() [Fact] public async Task DisposableArg_IsMarshaledAndLaterCollected() { -#if !NBMSGPACK_MARSHALING_SUPPORT - Assert.SkipWhen(this is DisposableProxyNerdbankMessagePackTests, "NerdbankMessagePackFormatter does not yet support marshaled objects."); -#endif var weakRefs = await this.DisposableArg_Helper(); await this.AssertWeakReferenceGetsCollectedAsync(weakRefs.Proxy); await this.AssertWeakReferenceGetsCollectedAsync(weakRefs.Target); @@ -118,9 +106,6 @@ public async Task DisposableArg_IsMarshaledAndLaterCollected() [Fact] public async Task DisposableWithinArg_IsMarshaledAndLaterCollected() { -#if !NBMSGPACK_MARSHALING_SUPPORT - Assert.SkipWhen(this is DisposableProxyNerdbankMessagePackTests, "NerdbankMessagePackFormatter does not yet support marshaled objects."); -#endif var weakRefs = await this.DisposableWithinArg_Helper(); await this.AssertWeakReferenceGetsCollectedAsync(weakRefs.Proxy); await this.AssertWeakReferenceGetsCollectedAsync(weakRefs.Target); @@ -155,9 +140,6 @@ public async Task IDisposableDataAsReturnType_ShouldSerialize() [Fact] public async Task IDisposable_MarshaledBackAndForth() { -#if !NBMSGPACK_MARSHALING_SUPPORT - Assert.SkipWhen(this is DisposableProxyNerdbankMessagePackTests, "NerdbankMessagePackFormatter does not yet support marshaled objects."); -#endif IDisposable? disposable = await this.client.GetDisposableAsync().WithCancellation(this.TimeoutToken); Assert.NotNull(disposable); await this.client.AcceptProxyAsync(disposable).WithCancellation(this.TimeoutToken); diff --git a/test/StreamJsonRpc.Tests/JsonContractResolverTests.cs b/test/StreamJsonRpc.Tests/JsonContractResolverTests.cs index dcfac5fc6..ea08911bf 100644 --- a/test/StreamJsonRpc.Tests/JsonContractResolverTests.cs +++ b/test/StreamJsonRpc.Tests/JsonContractResolverTests.cs @@ -34,7 +34,7 @@ public JsonContractResolverTest(ITestOutputHelper logger) this.serverRpc.StartListening(); } - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IServer : IDisposable { Task GiveObserver(IObserver observer); @@ -54,7 +54,7 @@ public partial interface IServer : IDisposable Task> GetMarshalableContainer(); } - [RpcMarshalable] + [RpcMarshalable, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IMarshalable : IDisposable { void DoSomething(); diff --git a/test/StreamJsonRpc.Tests/JsonRpcMessagePackLengthTests.cs b/test/StreamJsonRpc.Tests/JsonRpcMessagePackLengthTests.cs index ffa5ec6d5..795817c78 100644 --- a/test/StreamJsonRpc.Tests/JsonRpcMessagePackLengthTests.cs +++ b/test/StreamJsonRpc.Tests/JsonRpcMessagePackLengthTests.cs @@ -9,7 +9,7 @@ public abstract partial class JsonRpcMessagePackLengthTests(ITestOutputHelper logger) : JsonRpcTests(logger) { - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] internal partial interface IMessagePackServer { Task ReturnUnionTypeAsync(CancellationToken cancellationToken); diff --git a/test/StreamJsonRpc.Tests/JsonRpcProxyGenerationTests.cs b/test/StreamJsonRpc.Tests/JsonRpcProxyGenerationTests.cs index d1e6006e5..e12e5ec34 100644 --- a/test/StreamJsonRpc.Tests/JsonRpcProxyGenerationTests.cs +++ b/test/StreamJsonRpc.Tests/JsonRpcProxyGenerationTests.cs @@ -4,13 +4,13 @@ #pragma warning disable CS0436 // Type conflicts with a type in the external assembly, but we want to test that we can handle this. using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Reflection; #if NET using System.Runtime.Loader; #endif using Microsoft.VisualStudio.Threading; using Nerdbank; -using PolyType; using StreamJsonRpc.Reflection; using StreamJsonRpc.Tests; using ExAssembly = StreamJsonRpc.Tests.ExternalAssembly; @@ -47,7 +47,7 @@ protected JsonRpcProxyGenerationTests(ITestOutputHelper logger, JsonRpcProxyOpti this.clientJsonRpc.TraceSource.Listeners.Add(new XunitTraceListener(this.Logger)); } - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IServer { event EventHandler ItHappened; @@ -71,13 +71,13 @@ public partial interface IServer Task Dispose(); } - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IServerWithMoreEvents { event EventHandler AnotherEvent; } - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IServerDerived : IServer { Task HeavyWorkAsync(CancellationToken cancellationToken); @@ -88,7 +88,7 @@ public partial interface IServerDerived : IServer Task ARoseByAsync(string name); } - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IRpcWithAsyncSuffixedMethod { Task DoSomethingAsync(); @@ -103,7 +103,7 @@ public partial interface IServerWithBadCancellationParam Task HeavyWorkAsync(CancellationToken cancellationToken, int param1); } - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IServer3 { Task SayHiAsync(); @@ -112,18 +112,18 @@ public partial interface IServer3 Task ARoseByAsync(string name); } - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IServer2 { Task MultiplyAsync(int a, int b); } - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IDisposableServer2 : IDisposable, IServer2 { } - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IServerWithParamsObject { Task SumOfParameterObject(int a, int b); @@ -131,7 +131,7 @@ public partial interface IServerWithParamsObject Task SumOfParameterObject(int a, int b, CancellationToken cancellationToken); } - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IServerWithParamsObjectNoResult { Task SumOfParameterObject(int a, int b); @@ -139,7 +139,7 @@ public partial interface IServerWithParamsObjectNoResult Task SumOfParameterObject(int a, int b, CancellationToken cancellationToken); } - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IServerWithValueTasks { ValueTask DoSomethingValueAsync(); @@ -152,7 +152,7 @@ public interface IServerWithNonTaskReturnTypes int Add(int a, int b); } - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IServerWithVoidReturnType { void Notify(int a, int b); @@ -178,12 +178,14 @@ public interface IServerWithGenericMethod } [JsonRpcContract] + [SuppressMessage("Usage", "StreamJsonRpc0008", Justification = "Source generated shapes cause this to fail unrelated tests.")] public partial interface IReferenceAnUnreachableAssembly { Task TakeAsync(UnreachableAssembly.SomeUnreachableClass obj); } [JsonRpcContract] + [SuppressMessage("Usage", "StreamJsonRpc0008", Justification = "Blocked by https://github.com/eiriktsarpalis/PolyType/issues/233")] internal partial interface IServerInternal : ExAssembly.ISomeInternalProxyInterface, IServerInternalWithInternalTypesFromOtherAssemblies, @@ -192,7 +194,7 @@ internal partial interface IServerInternal : Task AddAsync(int a, int b); } - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] internal partial interface IServerInternalWithInternalTypesFromOtherAssemblies { Task SomeMethodAsync(); @@ -200,20 +202,20 @@ internal partial interface IServerInternalWithInternalTypesFromOtherAssemblies internal partial interface IRemoteService { - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] internal partial interface ICallback : ExAssembly.IInternalGenericInterface { } } - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] [JsonRpcProxyInterfaceGroup(typeof(IInterfaceGroup2))] internal partial interface IInterfaceGroup1; - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] internal partial interface IInterfaceGroup2; - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] internal partial interface ITimeTestedProxy { event EventHandler TestEvent; diff --git a/test/StreamJsonRpc.Tests/MarshalableProxyNerdbankMessagePackTests.cs b/test/StreamJsonRpc.Tests/MarshalableProxyNerdbankMessagePackTests.cs index 1a9776f23..97e0d02fc 100644 --- a/test/StreamJsonRpc.Tests/MarshalableProxyNerdbankMessagePackTests.cs +++ b/test/StreamJsonRpc.Tests/MarshalableProxyNerdbankMessagePackTests.cs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -#if NBMSGPACK_MARSHALING_SUPPORT +using Nerdbank.MessagePack; +using PolyType; + public partial class MarshalableProxyNerdbankMessagePackTests : MarshalableProxyTests { public MarshalableProxyNerdbankMessagePackTests(ITestOutputHelper logger) @@ -39,4 +41,3 @@ public MarshalableProxyNerdbankMessagePackTests(ITestOutputHelper logger) [GenerateShapeFor>] private partial class Witness; } -#endif diff --git a/test/StreamJsonRpc.Tests/MarshalableProxyTests.cs b/test/StreamJsonRpc.Tests/MarshalableProxyTests.cs index a1cb36695..5b2d49ed5 100644 --- a/test/StreamJsonRpc.Tests/MarshalableProxyTests.cs +++ b/test/StreamJsonRpc.Tests/MarshalableProxyTests.cs @@ -2,6 +2,7 @@ // 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; @@ -9,7 +10,6 @@ using Nerdbank.MessagePack; using Nerdbank.Streams; using Newtonsoft.Json; -using PolyType; /// /// Tests the proxying of interfaces marked with . @@ -41,7 +41,7 @@ protected MarshalableProxyTests(ITestOutputHelper logger) this.serverRpc.StartListening(); } - [RpcMarshalable] + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] [JsonConverter(typeof(MarshalableConverter))] [MessagePackFormatter(typeof(MarshalableFormatter))] [MessagePackConverter(typeof(MarshalableNerdbankConverter))] @@ -97,17 +97,18 @@ public interface INonMarshalable : IDisposable Task DoSomethingAsync(); } - [RpcMarshalable] + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IMarshalable : INonMarshalable { } - [RpcMarshalable(CallScopedLifetime = true)] + [RpcMarshalable(CallScopedLifetime = true), GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IMarshalableWithCallScopedLifetime : IMarshalable { } - [RpcMarshalable] + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] + [JsonRpcProxy>] public partial interface IGenericMarshalable : IMarshalable { Task DoSomethingWithParameterAsync(T parameter); @@ -117,14 +118,14 @@ public interface INonMarshalableDerivedFromMarshalable : IMarshalable { } - [RpcMarshalable] + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] #pragma warning disable StreamJsonRpc0005 // RpcMarshalable are IDisposable -- runtime fail mode test public partial interface INonDisposableMarshalable #pragma warning restore StreamJsonRpc0005 // RpcMarshalable are IDisposable -- runtime fail mode test { } - [RpcMarshalable] + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IMarshalableWithProperties : IDisposable { #pragma warning disable StreamJsonRpc0012 // Unsupported member -- runtime fail mode test @@ -132,7 +133,7 @@ public partial interface IMarshalableWithProperties : IDisposable #pragma warning restore StreamJsonRpc0012 // Unsupported member } - [RpcMarshalable] + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IMarshalableWithEvents : IDisposable { #pragma warning disable StreamJsonRpc0012 // Unsupported member -- runtime fail mode test @@ -140,7 +141,7 @@ public partial interface IMarshalableWithEvents : IDisposable #pragma warning restore StreamJsonRpc0012 // Unsupported member -- runtime fail mode test } - [RpcMarshalable] + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] [RpcMarshalableOptionalInterface(1, typeof(IMarshalableSubType1))] [RpcMarshalableOptionalInterface(2, typeof(IMarshalableSubType2))] [RpcMarshalableOptionalInterface(3, typeof(IMarshalableSubType1Extended))] @@ -152,18 +153,18 @@ public partial interface IMarshalableWithOptionalInterfaces : IDisposable { Task GetAsync(int value); - [JsonRpcMethod("RemamedAsync")] + [JsonRpcMethod("RenamedAsync")] Task ToBeRenamedAsync(string s); } - [RpcMarshalable] + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] [RpcMarshalableOptionalInterface(1, typeof(IMarshalableSubTypeWithIntermediateInterface2))] [RpcMarshalableOptionalInterface(2, typeof(IMarshalableSubTypeWithIntermediateInterface))] public partial interface IMarshalableWithOptionalInterfaces2 : IMarshalableWithOptionalInterfaces { } - [RpcMarshalable] + [RpcMarshalable(IsOptional = true), TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IMarshalableNonExtendingBase : IDisposable { Task GetPlusFourAsync(int value); @@ -177,7 +178,7 @@ public interface IMarshalableSubTypeIntermediateInterface : IMarshalableWithOpti Task GetPlusTwoAsync(int value); } - [RpcMarshalable] + [RpcMarshalable(IsOptional = true), TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IMarshalableSubTypeWithIntermediateInterface : IMarshalableSubTypeIntermediateInterface { new Task GetPlusTwoAsync(int value); @@ -185,13 +186,13 @@ public partial interface IMarshalableSubTypeWithIntermediateInterface : IMarshal Task GetPlusThreeAsync(int value); } - [RpcMarshalable] + [RpcMarshalable(IsOptional = true), TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IMarshalableSubTypeWithIntermediateInterface2 : IMarshalableSubTypeIntermediateInterface { new Task GetPlusTwoAsync(int value); } - [RpcMarshalable] + [RpcMarshalable(IsOptional = true), TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IMarshalableSubType1 : IMarshalableWithOptionalInterfaces2 { Task GetPlusOneAsync(int value); @@ -199,7 +200,7 @@ public partial interface IMarshalableSubType1 : IMarshalableWithOptionalInterfac Task GetMinusOneAsync(int value); } - [RpcMarshalable] + [RpcMarshalable(IsOptional = true), TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IMarshalableSubType1Extended : IMarshalableSubType1 { new Task GetAsync(int value); @@ -215,13 +216,14 @@ public partial interface IMarshalableSubType1Extended : IMarshalableSubType1 Task GetMinusTwoAsync(int value); } - [RpcMarshalable] + [RpcMarshalable(IsOptional = true)] + [SuppressMessage("Usage", "StreamJsonRpc0008", Justification = "Blocked by https://github.com/eiriktsarpalis/PolyType/issues/233")] public partial interface IMarshalableSubTypesCombined : IMarshalableSubType1Extended, IMarshalableSubType2, IMarshalableNonExtendingBase { Task GetPlusFiveAsync(int value); } - [RpcMarshalable] + [RpcMarshalable(IsOptional = true), TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] [RpcMarshalableOptionalInterface(1, typeof(IMarshalableSubType2Extended))] public partial interface IMarshalableSubType2 : IMarshalableWithOptionalInterfaces2 { @@ -230,18 +232,18 @@ public partial interface IMarshalableSubType2 : IMarshalableWithOptionalInterfac Task GetMinusTwoAsync(int value); } - [RpcMarshalable] + [RpcMarshalable(IsOptional = true), TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IMarshalableSubType2Extended : IMarshalableSubType2 { Task GetPlusThreeAsync(int value); } - [RpcMarshalable] + [RpcMarshalable, TypeShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IMarshalableUnknownSubType : IMarshalableWithOptionalInterfaces2 { } - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IServer { Task GetMarshalableAsync(bool returnNull = false); @@ -362,6 +364,7 @@ public async Task MarshalableInterfaceCannotHaveProperties() [Fact] public async Task MarshalableInterfaceCannotHaveEvents() { + Assert.SkipWhen(this is MarshalableProxyNerdbankMessagePackTests, "Events are not yet detectable by PolyType."); // remove when https://github.com/eiriktsarpalis/PolyType/issues/226 is fixed. var ex = await Assert.ThrowsAnyAsync(() => this.client.AcceptMarshalableWithEventsAsync(new MarshalableWithEvents())); Assert.True(IsExceptionOrInnerOfType(ex)); } @@ -704,16 +707,16 @@ public async Task RpcMarshalableOptionalInterface() this.server.ReturnedMarshalableWithOptionalInterfaces = new MarshalableWithOptionalInterfaces(); IMarshalableWithOptionalInterfaces? proxy = await this.client.GetMarshalableWithOptionalInterfacesAsync(); Assert.Equal(1, await proxy!.GetAsync(1)); - AssertIsNot(proxy, typeof(IMarshalableSubType1)); - AssertIsNot(proxy, typeof(IMarshalableSubType2)); - AssertIsNot(proxy, typeof(IMarshalableSubType2Extended)); + this.AssertIsNot(proxy, typeof(IMarshalableSubType1)); + this.AssertIsNot(proxy, typeof(IMarshalableSubType2)); + this.AssertIsNot(proxy, typeof(IMarshalableSubType2Extended)); this.server.ReturnedMarshalableWithOptionalInterfaces = new MarshalableSubType1(); - IMarshalableSubType1? proxy1 = (IMarshalableSubType1?)await this.client.GetMarshalableWithOptionalInterfacesAsync(); + IMarshalableSubType1? proxy1 = (await this.client.GetMarshalableWithOptionalInterfacesAsync())?.As(); Assert.Equal(1, await proxy1!.GetAsync(1)); Assert.Equal(2, await proxy1.GetPlusOneAsync(1)); - AssertIsNot(proxy1, typeof(IMarshalableSubType2)); - AssertIsNot(proxy1, typeof(IMarshalableSubType2Extended)); + this.AssertIsNot(proxy1, typeof(IMarshalableSubType2)); + this.AssertIsNot(proxy1, typeof(IMarshalableSubType2Extended)); } [Fact] @@ -723,15 +726,15 @@ public async Task RpcMarshalableOptionalInterface_JsonRpcMethodAttribute() IMarshalableWithOptionalInterfaces? proxy = await this.client.GetMarshalableWithOptionalInterfacesAsync(); Assert.Equal("foo", await proxy!.ToBeRenamedAsync("foo")); - Assert.Equal("foo", await this.clientRpc.InvokeAsync("$/invokeProxy/0/RemamedAsync", "foo")); + Assert.Equal("foo", await this.clientRpc.InvokeAsync("$/invokeProxy/0/RenamedAsync", "foo")); this.server.ReturnedMarshalableWithOptionalInterfaces = new MarshalableSubType1(); IMarshalableWithOptionalInterfaces? proxy1 = await this.client.GetMarshalableWithOptionalInterfacesAsync(); Assert.Equal("foo", await proxy1!.ToBeRenamedAsync("foo")); - Assert.Equal("foo", await ((IMarshalableSubType1)proxy1)!.ToBeRenamedAsync("foo")); + Assert.Equal("foo", await proxy1!.As()!.ToBeRenamedAsync("foo")); - Assert.Equal("foo", await this.clientRpc.InvokeAsync("$/invokeProxy/1/RemamedAsync", "foo")); - Assert.Equal("foo", await this.clientRpc.InvokeAsync("$/invokeProxy/1/1.RemamedAsync", "foo")); + Assert.Equal("foo", await this.clientRpc.InvokeAsync("$/invokeProxy/1/RenamedAsync", "foo")); + Assert.Equal("foo", await this.clientRpc.InvokeAsync("$/invokeProxy/1/1.RenamedAsync", "foo")); } [Fact] @@ -752,10 +755,10 @@ public async Task RpcMarshalableOptionalInterface_MethodNameTransform_Prefix() var proxy = await localRpc.InvokeAsync("one." + nameof(IServer.GetMarshalableWithOptionalInterfacesAsync)); Assert.Equal(1, await proxy!.GetAsync(1)); - Assert.Equal(1, await ((IMarshalableSubType1)proxy).GetAsync(1)); - Assert.Equal(2, await ((IMarshalableSubType1)proxy).GetPlusOneAsync(1)); - AssertIsNot(proxy, typeof(IMarshalableSubType2)); - AssertIsNot(proxy, typeof(IMarshalableSubType2Extended)); + Assert.Equal(1, await proxy.As()!.GetAsync(1)); + Assert.Equal(2, await proxy.As()!.GetPlusOneAsync(1)); + this.AssertIsNot(proxy, typeof(IMarshalableSubType2)); + this.AssertIsNot(proxy, typeof(IMarshalableSubType2Extended)); // The MethodNameTransform doesn't apply to the marshaled objects Assert.Equal(1, await localRpc.InvokeAsync("$/invokeProxy/0/GetAsync", 1)); @@ -780,10 +783,10 @@ public async Task RpcMarshalableOptionalInterface_MethodNameTransform_CamelCase( var proxy = await localRpc.InvokeAsync("getMarshalableWithOptionalInterfacesAsync"); Assert.Equal(1, await proxy!.GetAsync(1)); - Assert.Equal(1, await ((IMarshalableSubType1)proxy).GetAsync(1)); - Assert.Equal(2, await ((IMarshalableSubType1)proxy).GetPlusOneAsync(1)); - AssertIsNot(proxy, typeof(IMarshalableSubType2)); - AssertIsNot(proxy, typeof(IMarshalableSubType2Extended)); + Assert.Equal(1, await proxy!.As()!.GetAsync(1)); + Assert.Equal(2, await proxy!.As()!.GetPlusOneAsync(1)); + this.AssertIsNot(proxy, typeof(IMarshalableSubType2)); + this.AssertIsNot(proxy, typeof(IMarshalableSubType2Extended)); // The MethodNameTransform doesn't apply to the marshaled objects Assert.Equal(1, await localRpc.InvokeAsync("$/invokeProxy/0/GetAsync", 1)); @@ -802,22 +805,22 @@ public async Task RpcMarshalableOptionalInterface_Null() public async Task RpcMarshalableOptionalInterface_IndirectInterfaceImplementation() { this.server.ReturnedMarshalableWithOptionalInterfaces = new MarshalableSubType1Indirect(); - IMarshalableSubType1? proxy = (IMarshalableSubType1?)await this.client.GetMarshalableWithOptionalInterfacesAsync(); + IMarshalableSubType1? proxy = (await this.client.GetMarshalableWithOptionalInterfacesAsync())?.As(); Assert.Equal(1, await proxy!.GetAsync(1)); Assert.Equal(2, await proxy.GetPlusOneAsync(1)); - AssertIsNot(proxy, typeof(IMarshalableSubType2)); - AssertIsNot(proxy, typeof(IMarshalableSubType2Extended)); + this.AssertIsNot(proxy, typeof(IMarshalableSubType2)); + this.AssertIsNot(proxy, typeof(IMarshalableSubType2Extended)); } [Fact] public async Task RpcMarshalableOptionalInterface_WithExplicitImplementation() { this.server.ReturnedMarshalableWithOptionalInterfaces = new MarshalableSubType2(); - IMarshalableSubType2? proxy = (IMarshalableSubType2?)await this.client.GetMarshalableWithOptionalInterfacesAsync(); + IMarshalableSubType2? proxy = (await this.client.GetMarshalableWithOptionalInterfacesAsync())?.As(); Assert.Equal(1, await proxy!.GetAsync(1)); Assert.Equal(3, await proxy.GetPlusTwoAsync(1)); - AssertIsNot(proxy, typeof(IMarshalableSubType1)); - AssertIsNot(proxy, typeof(IMarshalableSubType2Extended)); + this.AssertIsNot(proxy, typeof(IMarshalableSubType1)); + this.AssertIsNot(proxy, typeof(IMarshalableSubType2Extended)); } [Fact] @@ -826,24 +829,24 @@ public async Task RpcMarshalableOptionalInterface_UnknownSubType() this.server.ReturnedMarshalableWithOptionalInterfaces = new MarshalableUnknownSubType(); IMarshalableWithOptionalInterfaces? proxy = await this.client.GetMarshalableWithOptionalInterfacesAsync(); Assert.Equal(1, await proxy!.GetAsync(1)); - AssertIsNot(proxy, typeof(IMarshalableSubType1)); - AssertIsNot(proxy, typeof(IMarshalableSubType2)); - AssertIsNot(proxy, typeof(IMarshalableSubType2Extended)); + this.AssertIsNot(proxy, typeof(IMarshalableSubType1)); + this.AssertIsNot(proxy, typeof(IMarshalableSubType2)); + this.AssertIsNot(proxy, typeof(IMarshalableSubType2Extended)); } [Fact] public async Task RpcMarshalableOptionalInterface_OnlyAttibutesOnDeclaredTypeAreHonored() { this.server.ReturnedMarshalableWithOptionalInterfaces = new MarshalableSubType2Extended(); - IMarshalableSubType2? proxy = (IMarshalableSubType2?)await this.client.GetMarshalableWithOptionalInterfacesAsync(); + IMarshalableSubType2? proxy = (await this.client.GetMarshalableWithOptionalInterfacesAsync())?.As(); Assert.Equal(1, await proxy!.GetAsync(1)); Assert.Equal(3, await proxy.GetPlusTwoAsync(1)); - AssertIsNot(proxy, typeof(IMarshalableSubType2Extended)); + this.AssertIsNot(proxy, typeof(IMarshalableSubType2Extended)); IMarshalableSubType2? proxy1 = await this.client.GetMarshalableSubType2Async(); Assert.Equal(1, await proxy1!.GetAsync(1)); Assert.Equal(3, await proxy1.GetPlusTwoAsync(1)); - Assert.Equal(4, await ((IMarshalableSubType2Extended)proxy1).GetPlusThreeAsync(1)); + Assert.Equal(4, await proxy1.As()!.GetPlusThreeAsync(1)); } [Fact] @@ -853,7 +856,7 @@ public async Task RpcMarshalableOptionalInterface_OptionalInterfaceNotExtendingB IMarshalableWithOptionalInterfaces? proxy = await this.client.GetMarshalableWithOptionalInterfacesAsync(); Assert.Equal(1, await proxy!.GetAsync(1)); - Assert.Equal(5, await ((IMarshalableNonExtendingBase)proxy).GetPlusFourAsync(1)); + Assert.Equal(5, await proxy!.As()!.GetPlusFourAsync(1)); } [Fact] @@ -871,10 +874,10 @@ public async Task RpcMarshalableOptionalInterface_IntermediateNonMarshalableInte // of the RPC contract, so IMarshalableSubTypeWithIntermediateInterface.GetPlusTwoAsync is invoked instead. Assert.Equal(-3, await ((IMarshalableSubTypeIntermediateInterface)proxy).GetPlusTwoAsync(1)); - Assert.Equal(1, await ((IMarshalableSubTypeWithIntermediateInterface)proxy).GetAsync(1)); - Assert.Equal(2, await ((IMarshalableSubTypeWithIntermediateInterface)proxy).GetPlusOneAsync(1)); - Assert.Equal(-3, await ((IMarshalableSubTypeWithIntermediateInterface)proxy).GetPlusTwoAsync(1)); // This method negates the result - Assert.Equal(4, await ((IMarshalableSubTypeWithIntermediateInterface)proxy).GetPlusThreeAsync(1)); + Assert.Equal(1, await proxy.As()!.GetAsync(1)); + Assert.Equal(2, await proxy.As()!.GetPlusOneAsync(1)); + Assert.Equal(-3, await proxy.As()!.GetPlusTwoAsync(1)); // This method negates the result + Assert.Equal(4, await proxy.As()!.GetPlusThreeAsync(1)); } [Fact] @@ -884,11 +887,11 @@ public async Task RpcMarshalableOptionalInterface_MultipleIntermediateInterfaces IMarshalableWithOptionalInterfaces? proxy1 = await this.client.GetMarshalableWithOptionalInterfacesAsync(); IMarshalableWithOptionalInterfaces2? proxy2 = await this.client.GetMarshalableWithOptionalInterfaces2Async(); - Assert.Equal(3, await ((IMarshalableSubTypeWithIntermediateInterface)proxy1!).GetPlusTwoAsync(1)); - Assert.Equal(-3, await ((IMarshalableSubTypeWithIntermediateInterface2)proxy1).GetPlusTwoAsync(1)); + Assert.Equal(3, await proxy1!.As()!.GetPlusTwoAsync(1)); + Assert.Equal(-3, await proxy1!.As()!.GetPlusTwoAsync(1)); - Assert.Equal(3, await ((IMarshalableSubTypeWithIntermediateInterface)proxy2!).GetPlusTwoAsync(1)); - Assert.Equal(-3, await ((IMarshalableSubTypeWithIntermediateInterface2)proxy2).GetPlusTwoAsync(1)); + Assert.Equal(3, await proxy2!.As()!.GetPlusTwoAsync(1)); + Assert.Equal(-3, await proxy2!.As()!.GetPlusTwoAsync(1)); // Since MarshalableSubTypeWithIntermediateInterface1And2 implements the GetPlusTwoAsync methods explicitly // and IMarshalableSubTypeIntermediateInterface is not a known optional interface, a call to @@ -900,8 +903,8 @@ public async Task RpcMarshalableOptionalInterface_MultipleIntermediateInterfaces // IMarshalableWithOptionalInterfaces and IMarshalableWithOptionalInterfaces2 have opposite // RpcMarshalableOptionalInterface definitions (the order of the optionalInterfaceCode values is inverted) // resulting in inverted dispatching. - Assert.Equal(3, await ((IMarshalableSubTypeIntermediateInterface)proxy1).GetPlusTwoAsync(1)); - Assert.Equal(-3, await ((IMarshalableSubTypeIntermediateInterface)proxy2).GetPlusTwoAsync(1)); + Assert.Equal(3, await proxy1!.As()!.GetPlusTwoAsync(1)); + Assert.Equal(-3, await proxy2!.As()!.GetPlusTwoAsync(1)); } [Fact] @@ -911,22 +914,22 @@ public async Task RpcMarshalableOptionalInterface_MultipleImplementations() IMarshalableWithOptionalInterfaces? proxy = await this.client.GetMarshalableWithOptionalInterfacesAsync(); Assert.Equal(1, await proxy!.GetAsync(1)); - Assert.Equal(5, await ((IMarshalableNonExtendingBase)proxy).GetPlusFourAsync(1)); + Assert.Equal(5, await proxy.As()!.GetPlusFourAsync(1)); - Assert.Equal(1, await ((IMarshalableSubType1)proxy).GetAsync(1)); - Assert.Equal(2, await ((IMarshalableSubType1)proxy).GetPlusOneAsync(1)); - Assert.Equal(1, await ((IMarshalableSubType1)proxy).GetMinusOneAsync(2)); + Assert.Equal(1, await proxy.As()!.GetAsync(1)); + Assert.Equal(2, await proxy.As()!.GetPlusOneAsync(1)); + Assert.Equal(1, await proxy.As()!.GetMinusOneAsync(2)); - Assert.Equal(1, await ((IMarshalableSubType1Extended)proxy).GetAsync(1)); - Assert.Equal(-2, await ((IMarshalableSubType1Extended)proxy).GetPlusOneAsync(1)); // This method negates the result - Assert.Equal(-1, await ((IMarshalableSubType1Extended)proxy).GetMinusOneAsync(2)); // This method negates the result - Assert.Equal(-3, await ((IMarshalableSubType1Extended)proxy).GetPlusTwoAsync(1)); // This method negates the result - Assert.Equal(4, await ((IMarshalableSubType1Extended)proxy).GetPlusThreeAsync(1)); - Assert.Equal(-1, await ((IMarshalableSubType1Extended)proxy).GetMinusTwoAsync(1)); + Assert.Equal(1, await proxy.As()!.GetAsync(1)); + Assert.Equal(-2, await proxy.As()!.GetPlusOneAsync(1)); // This method negates the result + Assert.Equal(-1, await proxy.As()!.GetMinusOneAsync(2)); // This method negates the result + Assert.Equal(-3, await proxy.As()!.GetPlusTwoAsync(1)); // This method negates the result + Assert.Equal(4, await proxy.As()!.GetPlusThreeAsync(1)); + Assert.Equal(-1, await proxy.As()!.GetMinusTwoAsync(1)); - Assert.Equal(1, await ((IMarshalableSubType2)proxy).GetAsync(1)); - Assert.Equal(3, await ((IMarshalableSubType2)proxy).GetPlusTwoAsync(1)); - Assert.Equal(-1, await ((IMarshalableSubType2)proxy).GetMinusTwoAsync(1)); + Assert.Equal(1, await proxy.As()!.GetAsync(1)); + Assert.Equal(3, await proxy.As()!.GetPlusTwoAsync(1)); + Assert.Equal(-1, await proxy.As()!.GetMinusTwoAsync(1)); } [Fact] @@ -936,28 +939,28 @@ public async Task RpcMarshalableOptionalInterface_MultipleImplementationsCombine IMarshalableWithOptionalInterfaces? proxy = await this.client.GetMarshalableWithOptionalInterfacesAsync(); Assert.Equal(1, await proxy!.GetAsync(1)); - Assert.Equal(5, await ((IMarshalableNonExtendingBase)proxy).GetPlusFourAsync(1)); + Assert.Equal(5, await proxy.As()!.GetPlusFourAsync(1)); - Assert.Equal(1, await ((IMarshalableSubType1)proxy).GetAsync(1)); - Assert.Equal(2, await ((IMarshalableSubType1)proxy).GetPlusOneAsync(1)); - Assert.Equal(1, await ((IMarshalableSubType1)proxy).GetMinusOneAsync(2)); + Assert.Equal(1, await proxy.As()!.GetAsync(1)); + Assert.Equal(2, await proxy.As()!.GetPlusOneAsync(1)); + Assert.Equal(1, await proxy.As()!.GetMinusOneAsync(2)); - Assert.Equal(1, await ((IMarshalableSubType1Extended)proxy).GetAsync(1)); - Assert.Equal(-2, await ((IMarshalableSubType1Extended)proxy).GetPlusOneAsync(1)); // This method negates the result - Assert.Equal(-1, await ((IMarshalableSubType1Extended)proxy).GetMinusOneAsync(2)); // This method negates the result - Assert.Equal(-3, await ((IMarshalableSubType1Extended)proxy).GetPlusTwoAsync(1)); // This method negates the result - Assert.Equal(4, await ((IMarshalableSubType1Extended)proxy).GetPlusThreeAsync(1)); - Assert.Equal(-1, await ((IMarshalableSubType1Extended)proxy).GetMinusTwoAsync(1)); + Assert.Equal(1, await proxy.As()!.GetAsync(1)); + Assert.Equal(-2, await proxy.As()!.GetPlusOneAsync(1)); // This method negates the result + Assert.Equal(-1, await proxy.As()!.GetMinusOneAsync(2)); // This method negates the result + Assert.Equal(-3, await proxy.As()!.GetPlusTwoAsync(1)); // This method negates the result + Assert.Equal(4, await proxy.As()!.GetPlusThreeAsync(1)); + Assert.Equal(-1, await proxy.As()!.GetMinusTwoAsync(1)); - Assert.Equal(1, await ((IMarshalableSubType2)proxy).GetAsync(1)); - Assert.Equal(3, await ((IMarshalableSubType2)proxy).GetPlusTwoAsync(1)); - Assert.Equal(-1, await ((IMarshalableSubType2)proxy).GetMinusTwoAsync(1)); + Assert.Equal(1, await proxy.As()!.GetAsync(1)); + Assert.Equal(3, await proxy.As()!.GetPlusTwoAsync(1)); + Assert.Equal(-1, await proxy.As()!.GetMinusTwoAsync(1)); - Assert.Equal(1, await ((IMarshalableSubTypesCombined)proxy).GetAsync(1)); - Assert.Equal(-2, await ((IMarshalableSubTypesCombined)proxy).GetPlusOneAsync(1)); // This method negates the result - Assert.Equal(-1, await ((IMarshalableSubTypesCombined)proxy).GetMinusOneAsync(2)); // This method negates the result - Assert.Equal(4, await ((IMarshalableSubTypesCombined)proxy).GetPlusThreeAsync(1)); - Assert.Equal(6, await ((IMarshalableSubTypesCombined)proxy).GetPlusFiveAsync(1)); + Assert.Equal(1, await proxy.As()!.GetAsync(1)); + Assert.Equal(-2, await proxy.As()!.GetPlusOneAsync(1)); // This method negates the result + Assert.Equal(-1, await proxy.As()!.GetMinusOneAsync(2)); // This method negates the result + Assert.Equal(4, await proxy.As()!.GetPlusThreeAsync(1)); + Assert.Equal(6, await proxy.As()!.GetPlusFiveAsync(1)); } [Fact] @@ -1047,9 +1050,13 @@ public async Task RpcMarshalable_CallScopedLifetime_ObjectReturned() protected abstract IJsonRpcMessageFormatter CreateFormatter(); - private static void AssertIsNot(object obj, Type type) + private void AssertIsNot(object obj, Type type) { - Assert.False(type.IsAssignableFrom(obj.GetType()), $"Object of type {obj.GetType().FullName} is not assignable to {type.FullName}"); + Assert.False(((IJsonRpcClientProxy)obj).Is(type), $"Object of type {obj.GetType().FullName} is not expected to be assignable to {type.FullName}"); + if (this is not MarshalableProxyNerdbankMessagePackTests) + { + Assert.False(type.IsAssignableFrom(obj.GetType()), $"Object of type {obj.GetType().FullName} is not expected to be assignable to {type.FullName}"); + } } public class Server : IServer @@ -1081,12 +1088,12 @@ public class Server : IServer public Task GetMarshalableWithOptionalInterfaces2Async() { - return Task.FromResult((IMarshalableWithOptionalInterfaces2?)this.ReturnedMarshalableWithOptionalInterfaces); + return Task.FromResult(this.ReturnedMarshalableWithOptionalInterfaces?.As()); } public Task GetMarshalableSubType2Async() { - return Task.FromResult((IMarshalableSubType2?)this.ReturnedMarshalableWithOptionalInterfaces); + return Task.FromResult(this.ReturnedMarshalableWithOptionalInterfaces?.As()); } public Task?> GetGenericMarshalableAsync(bool returnNull) diff --git a/test/StreamJsonRpc.Tests/ObserverMarshalingNerdbankMessagePackTests.cs b/test/StreamJsonRpc.Tests/ObserverMarshalingNerdbankMessagePackTests.cs index addb10ffa..493c6c57a 100644 --- a/test/StreamJsonRpc.Tests/ObserverMarshalingNerdbankMessagePackTests.cs +++ b/test/StreamJsonRpc.Tests/ObserverMarshalingNerdbankMessagePackTests.cs @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -#if NBMSGPACK_MARSHALING_SUPPORT +using PolyType; + public partial class ObserverMarshalingNerdbankMessagePackTests(ITestOutputHelper logger) : ObserverMarshalingTests(logger) { protected override IJsonRpcMessageFormatter CreateFormatter() => new NerdbankMessagePackFormatter { TypeShapeProvider = Witness.ShapeProvider }; @@ -10,4 +11,3 @@ public partial class ObserverMarshalingNerdbankMessagePackTests(ITestOutputHelpe [GenerateShapeFor>] private partial class Witness; } -#endif diff --git a/test/StreamJsonRpc.Tests/ObserverMarshalingTests.cs b/test/StreamJsonRpc.Tests/ObserverMarshalingTests.cs index 9e2e2fe1d..ce6efc563 100644 --- a/test/StreamJsonRpc.Tests/ObserverMarshalingTests.cs +++ b/test/StreamJsonRpc.Tests/ObserverMarshalingTests.cs @@ -38,7 +38,7 @@ protected ObserverMarshalingTests(ITestOutputHelper logger) this.serverRpc.StartListening(); } - [JsonRpcContract] + [JsonRpcContract, GenerateShape(IncludeMethods = MethodShapeFlags.PublicInstance)] public partial interface IServer : IDisposable { Task PushCompleteAndReturn(IObserver observer); diff --git a/test/StreamJsonRpc.Tests/Usings.cs b/test/StreamJsonRpc.Tests/Usings.cs index 6c94157d0..a69dc413e 100644 --- a/test/StreamJsonRpc.Tests/Usings.cs +++ b/test/StreamJsonRpc.Tests/Usings.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. global using Microsoft; +global using PolyType; global using StreamJsonRpc; global using StreamJsonRpc.Protocol; global using Xunit;