Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions azure-pipelines/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions docfx/analyzers/StreamJsonRpc0007.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# StreamJsonRpc0007: Use RpcMarshalableAttribute on optional marshalable interface

An interface specified as an argument to <xref:StreamJsonRpc.RpcMarshalableOptionalInterfaceAttribute> must itself be attributed with <xref:StreamJsonRpc.RpcMarshalableAttribute>.
An interface specified as an argument to <xref:StreamJsonRpc.RpcMarshalableOptionalInterfaceAttribute> must itself be attributed with <xref:StreamJsonRpc.RpcMarshalableAttribute> with <xref:StreamJsonRpc.RpcMarshalableAttribute.IsOptional> set to true.

## Example violation

Expand All @@ -11,6 +11,7 @@ The other is designated as optional and thus needs the attribute.

## Resolution

Add <xref:StreamJsonRpc.RpcMarshalableAttribute> to the optional interface.
Add <xref:StreamJsonRpc.RpcMarshalableAttribute> to the optional interface, taking care to set <xref:StreamJsonRpc.RpcMarshalableAttribute.IsOptional> to true.
We also add <xref:PolyType.TypeShapeAttribute> as required by [StreamJsonRpc0008](StreamJsonRpc0008.md).

[!code-csharp[](../../samples/Analyzers/StreamJsonRpc0007.cs#Fix)]
18 changes: 18 additions & 0 deletions docfx/analyzers/StreamJsonRpc0008.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# StreamJsonRpc0008: Add methods to PolyType shape for RPC contract interface

RPC interfaces attributed with <xref:StreamJsonRpc.JsonRpcContractAttribute> or <xref:StreamJsonRpc.RpcMarshalableAttribute> 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 <xref:StreamJsonRpc.NerdbankMessagePackFormatter>.

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 <xref:StreamJsonRpc.NerdbankMessagePackFormatter>.

## 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 <xref:PolyType.TypeShapeAttribute> to the interface and set its <xref:PolyType.TypeShapeAttribute.IncludeMethods> named argument to <xref:PolyType.MethodShapeFlags.PublicInstance> (or a superset of that).

[!code-csharp[](../../samples/Analyzers/StreamJsonRpc0008.cs#Fix)]
15 changes: 15 additions & 0 deletions docfx/analyzers/StreamJsonRpc0030.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# StreamJsonRpc0030: `[JsonRpcProxy<T>]` should be applied only to generic interfaces

The <xref:StreamJsonRpc.JsonRpcProxyAttribute`1> 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)]
15 changes: 15 additions & 0 deletions docfx/analyzers/StreamJsonRpc0031.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# StreamJsonRpc0031: `[JsonRpcProxy<T>]` type argument should be a closed instance of the applied type

The <xref:StreamJsonRpc.JsonRpcProxyAttribute`1> 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 <xref:StreamJsonRpc.JsonRpcProxyAttribute`1> 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)]
15 changes: 15 additions & 0 deletions docfx/analyzers/StreamJsonRpc0032.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# StreamJsonRpc0032: `[JsonRpcProxy<T>]` should be accompanied by another contract attribute

The <xref:StreamJsonRpc.JsonRpcProxyAttribute`1> 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 <xref:StreamJsonRpc.JsonRpcProxyAttribute`1> applied but no RPC contract attribute:

[!code-csharp[](../../samples/Analyzers/StreamJsonRpc0032.cs#Violation)]

## Resolution

Add either <xref:StreamJsonRpc.JsonRpcContractAttribute> or <xref:StreamJsonRpc.RpcMarshalableAttribute> to the interface:

[!code-csharp[](../../samples/Analyzers/StreamJsonRpc0032.cs#Fix)]
22 changes: 22 additions & 0 deletions docfx/analyzers/StreamJsonRpc0050.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# StreamJsonRpc0050: Use `IClientProxy.Is` or `JsonRpcExtensions.As`

When casting or type checking between two <xref:StreamJsonRpc.RpcMarshalableAttribute>-annotated interfaces, it is preferable to use the <xref:StreamJsonRpc.IClientProxy.Is(System.Type)?displayProperty=nameWithType> or <xref:StreamJsonRpc.JsonRpcExtensions.As``1(StreamJsonRpc.IClientProxy)?displayProperty=nameWithType> 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 <xref:StreamJsonRpc.IClientProxy.Is(System.Type)> or <xref:StreamJsonRpc.JsonRpcExtensions.As``1(StreamJsonRpc.IClientProxy)> 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 <xref:StreamJsonRpc.IClientProxy> interface on the same object to test for interface availability on the server, but will gracefully fallback to traditional type checks if <xref:StreamJsonRpc.IClientProxy> 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 <xref:StreamJsonRpc.IClientProxy> to participate in similar dynamic type checking.
5 changes: 5 additions & 0 deletions docfx/analyzers/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | <xref:StreamJsonRpc.JsonRpcProxyAttribute`1> should be applied only to generic interfaces
| [StreamJsonRpc0031](StreamJsonRpc0031.md) | Usage | Error | <xref:StreamJsonRpc.JsonRpcProxyAttribute`1> type argument should be a closed instance of the applied type
| [StreamJsonRpc0032](StreamJsonRpc0032.md) | Usage | Error | <xref:StreamJsonRpc.JsonRpcProxyAttribute`1> should be accompanied by another contract attribute
| [StreamJsonRpc0050](StreamJsonRpc0050.md) | Usage | Warning | Use <xref:StreamJsonRpc.IClientProxy.Is(System.Type)?displayProperty=nameWithType> or <xref:StreamJsonRpc.JsonRpcExtensions.As``1(StreamJsonRpc.IClientProxy)?displayProperty=nameWithType>
5 changes: 5 additions & 0 deletions docfx/analyzers/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions docfx/docfx.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions docfx/docs/proxies.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ Applying the <xref:StreamJsonRpc.JsonRpcContractAttribute> 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 <xref:StreamJsonRpc.JsonRpcContractAttribute> or <xref:StreamJsonRpc.RpcMarshalableAttribute> applied should also apply <xref:PolyType.GenerateShapeAttribute> with its <xref:PolyType.GenerateShapeAttribute.IncludeMethods> set to <xref:PolyType.MethodShapeFlags.PublicInstance>.

### Server-side concerns

On the server side, these same methods may be simple and naturally synchronous.
Expand Down
35 changes: 21 additions & 14 deletions docfx/exotic_types/rpc_marshalable_objects.md
Original file line number Diff line number Diff line change
@@ -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 <xref:StreamJsonRpc.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).

StreamJsonRpc allows transmitting marshalable objects (i.e., objects implementing a marshalable interface) in arguments and return values.

Expand All @@ -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 <xref:StreamJsonRpc.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 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.

Expand Down Expand Up @@ -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`
## <xref:StreamJsonRpc.RpcMarshalableOptionalInterfaceAttribute>

StreamJsonRpc provides the `RpcMarshalableOptionalInterfaceAttribute` to specify that marshalable objects implementing an RPC interface can optionally implement additional interfaces.
StreamJsonRpc provides the <xref:StreamJsonRpc.RpcMarshalableOptionalInterfaceAttribute> 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`.
<xref:StreamJsonRpc.RpcMarshalableOptionalInterfaceAttribute> is applied to the interface used in the RPC contract.
Such an interface must also apply the <xref:StreamJsonRpc.RpcMarshalableAttribute>.

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 <xref:StreamJsonRpc.RpcMarshalableOptionalInterfaceAttribute> references must have the <xref:StreamJsonRpc.RpcMarshalableAttribute> attribute applied, set <xref:StreamJsonRpc.RpcMarshalableAttribute.IsOptional> 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 <xref:StreamJsonRpc.RpcMarshalableOptionalInterfaceAttribute> 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 <xref:StreamJsonRpc.IClientProxy.Is(System.Type)> method or <xref:StreamJsonRpc.JsonRpcExtensions.As``1(StreamJsonRpc.IClientProxy)> extension method.

An <xref:StreamJsonRpc.RpcMarshalableAttribute> interface that also carries <xref:StreamJsonRpc.RpcMarshalableOptionalInterfaceAttribute> attributes will lead to generation of extension methods for itself and for each of the optional interfaces.
These extension methods expose the <xref:StreamJsonRpc.IClientProxy.Is(System.Type)> and <xref:StreamJsonRpc.JsonRpcExtensions.As``1(StreamJsonRpc.IClientProxy)> methods so they are conveniently accessible via these RPC interfaces more directly so that conditional casting to <xref:StreamJsonRpc.IClientProxy> 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:
<xref:StreamJsonRpc.RpcMarshalableOptionalInterfaceAttribute> 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 <xref:StreamJsonRpc.RpcMarshalableOptionalInterfaceAttribute> can be added to the `ICounter` interface from the earlier sample:

```cs
[RpcMarshalable]
Expand Down Expand Up @@ -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 <xref:StreamJsonRpc.RpcMarshalableOptionalInterfaceAttribute> are used as part of the wire protocol.
While it can be backward compatible to remove an <xref:StreamJsonRpc.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.

#### Method name conflicts and non-marshalable interfaces

Expand Down Expand Up @@ -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 <xref:StreamJsonRpc.RpcMarshalableOptionalInterfaceAttribute> of `IFoo`.

A call to `((IBar)proxy).DoFooAsync()` would result in the following behavior:

Expand Down
6 changes: 6 additions & 0 deletions samples/Analyzers/NoProxyAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@ namespace Samples.Analyzers.NoProxy;
/// </summary>
[AttributeUsage(AttributeTargets.Interface)]
internal class JsonRpcContractAttribute : Attribute;

[AttributeUsage(AttributeTargets.Interface)]
internal class GenerateShapeAttribute : Attribute
{
public MethodShapeFlags IncludeMethods { get; init; }
}
Loading