diff --git a/src/StreamJsonRpc.Analyzers/GeneratorModels/InterfaceModel.cs b/src/StreamJsonRpc.Analyzers/GeneratorModels/InterfaceModel.cs
index 96b43572..53108042 100644
--- a/src/StreamJsonRpc.Analyzers/GeneratorModels/InterfaceModel.cs
+++ b/src/StreamJsonRpc.Analyzers/GeneratorModels/InterfaceModel.cs
@@ -18,7 +18,7 @@ namespace StreamJsonRpc.Analyzers.GeneratorModels;
/// The methods in the interface.
/// The events in the interface.
/// Indicates whether the interface has additional members that are not supported.
-internal record InterfaceModel(string FullName, string Name, ImmutableEquatableArray TypeParameters, Container? Container, ImmutableEquatableArray Methods, ImmutableEquatableArray Events, bool HasUnsupportedMemberTypes)
+internal record InterfaceModel(string FullName, string Name, ImmutableEquatableArray<(VarianceKind Variance, string Identifier)> TypeParameters, Container? Container, ImmutableEquatableArray Methods, ImmutableEquatableArray Events, bool HasUnsupportedMemberTypes)
{
internal required bool IsPartial { get; init; }
@@ -77,7 +77,7 @@ internal static InterfaceModel Create(INamedTypeSymbol iface, KnownSymbols symbo
return new InterfaceModel(
iface.ToDisplayString(ProxyGenerator.FullyQualifiedNoGlobalWithNullableFormat),
iface.Name,
- [.. iface.TypeParameters.Select(tp => tp.Name)],
+ [.. iface.TypeParameters.Select(tp => (tp.Variance, tp.Name))],
Container.CreateFor((INamespaceOrTypeSymbol?)iface.ContainingType ?? iface.ContainingNamespace, cancellationToken),
methods.ToImmutableEquatableArray(),
events.ToImmutableEquatableArray(),
diff --git a/src/StreamJsonRpc.Analyzers/GeneratorModels/ProxyModel.cs b/src/StreamJsonRpc.Analyzers/GeneratorModels/ProxyModel.cs
index fa085795..50071307 100644
--- a/src/StreamJsonRpc.Analyzers/GeneratorModels/ProxyModel.cs
+++ b/src/StreamJsonRpc.Analyzers/GeneratorModels/ProxyModel.cs
@@ -74,7 +74,7 @@ internal ProxyModel(ImmutableEquatableSet interfaces, string? ex
internal void WriteInterfaceMapping(SourceWriter writer, InterfaceModel iface)
{
string genericTypeParameters = iface.TypeParameters.Length > 0
- ? $"<{string.Join(", ", iface.TypeParameters)}>"
+ ? $"<{string.Join(", ", iface.TypeParameters.Select(WriteTypeParameter))}>"
: string.Empty;
writer.WriteLine($$"""
[global::StreamJsonRpc.Reflection.JsonRpcProxyMappingAttribute(typeof({{ProxyGenerator.GenerationNamespace}}.{{this.Name}}{{this.GenericTypeDefinitionSuffix}}))]
@@ -291,4 +291,12 @@ private static string CreateProxyName(ImmutableEquatableSet inte
string additionalInterfaceHashString = Convert.ToBase64String(additionalInterfaceHash).TrimEnd('=').Replace('+', '_').Replace('/', '_');
return $"{sorted[0]}{additionalInterfaceHashString[..8]}";
}
+
+ private static string WriteTypeParameter((VarianceKind Variance, string Identifier) typeParameter) => typeParameter.Variance switch
+ {
+ VarianceKind.None => typeParameter.Identifier,
+ VarianceKind.In => $"in {typeParameter.Identifier}",
+ VarianceKind.Out => $"out {typeParameter.Identifier}",
+ _ => throw new InvalidOperationException($"Unknown variance kind: {typeParameter.Variance}."),
+ };
}
diff --git a/test/StreamJsonRpc.Analyzer.Tests/ProxyGeneratorTests.cs b/test/StreamJsonRpc.Analyzer.Tests/ProxyGeneratorTests.cs
index de1f6204..630c4825 100644
--- a/test/StreamJsonRpc.Analyzer.Tests/ProxyGeneratorTests.cs
+++ b/test/StreamJsonRpc.Analyzer.Tests/ProxyGeneratorTests.cs
@@ -375,6 +375,30 @@ public partial interface IGenericMarshalable
""");
}
+ [Fact]
+ public async Task RpcMarshalable_Generic_WithInModifier()
+ {
+ await VerifyCS.RunDefaultAsync("""
+ [RpcMarshalable]
+ public partial interface IGenericMarshalable
+ {
+ Task DoSomethingWithParameterAsync(T parameter);
+ }
+ """);
+ }
+
+ [Fact]
+ public async Task RpcMarshalable_Generic_WithOutModifier()
+ {
+ await VerifyCS.RunDefaultAsync("""
+ [RpcMarshalable]
+ public partial interface IGenericMarshalable
+ {
+ Task DoSomethingWithParameterAsync();
+ }
+ """);
+ }
+
[Fact]
public async Task RpcMarshalable_GenericWithClosedPrescriptions()
{
diff --git a/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_Generic_WithInModifier/IGenericMarshalable_T_.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_Generic_WithInModifier/IGenericMarshalable_T_.g.cs
new file mode 100644
index 00000000..f0a406ed
--- /dev/null
+++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_Generic_WithInModifier/IGenericMarshalable_T_.g.cs
@@ -0,0 +1,56 @@
+//
+
+#nullable enable
+#pragma warning disable CS0436 // prefer local types to imported ones
+
+[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_Generic_WithOutModifier/IGenericMarshalable_T_.g.cs b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_Generic_WithOutModifier/IGenericMarshalable_T_.g.cs
new file mode 100644
index 00000000..0e8127c0
--- /dev/null
+++ b/test/StreamJsonRpc.Analyzer.Tests/Resources/RpcMarshalable_Generic_WithOutModifier/IGenericMarshalable_T_.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.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
+ {
+ };
+
+ private static readonly global::System.Collections.Generic.IReadOnlyList DoSomethingWithParameterAsyncPositionalArgumentDeclaredTypes1 = new global::System.Collections.Generic.List
+ {
+ };
+
+ 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()
+ {
+ 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, [], DoSomethingWithParameterAsyncPositionalArgumentDeclaredTypes1, default);
+ this.OnCalledMethod("DoSomethingWithParameterAsync");
+
+ return result;
+
+ global::System.Collections.Generic.Dictionary ConstructNamedArgs()
+ => new()
+ {
+ };
+ }
+ }
+}