diff --git a/src/HotChocolate/Core/src/Subscriptions.InMemory/InMemoryPubSub.cs b/src/HotChocolate/Core/src/Subscriptions.InMemory/InMemoryPubSub.cs index 399726c7726..f2ccfe11b68 100644 --- a/src/HotChocolate/Core/src/Subscriptions.InMemory/InMemoryPubSub.cs +++ b/src/HotChocolate/Core/src/Subscriptions.InMemory/InMemoryPubSub.cs @@ -24,7 +24,7 @@ protected override async ValueTask OnSendAsync( TMessage message, CancellationToken cancellationToken = default) { - if (TryGetTopic>(formattedTopic, out var topic)) + if (TryGetTopic(formattedTopic, out var topic)) { await topic.PublishAsync(message, cancellationToken).ConfigureAwait(false); } @@ -32,7 +32,7 @@ protected override async ValueTask OnSendAsync( protected override ValueTask OnCompleteAsync(string formattedTopic) { - if (TryGetTopic(formattedTopic, out var topic)) + if (TryGetTopic(formattedTopic, out var topic)) { topic.TryComplete(); } diff --git a/src/HotChocolate/Core/src/Subscriptions/DefaultPubSub.cs b/src/HotChocolate/Core/src/Subscriptions/DefaultPubSub.cs index f7beaf1b607..6043523a584 100644 --- a/src/HotChocolate/Core/src/Subscriptions/DefaultPubSub.cs +++ b/src/HotChocolate/Core/src/Subscriptions/DefaultPubSub.cs @@ -12,7 +12,7 @@ namespace HotChocolate.Subscriptions; public abstract class DefaultPubSub : ITopicEventReceiver, ITopicEventSender, IDisposable { private readonly SemaphoreSlim _subscribeSemaphore = new(1, 1); - private readonly ConcurrentDictionary _topics = new(Ordinal); + private readonly ConcurrentDictionary _topics = new(Ordinal); private readonly TopicFormatter _topicFormatter; private readonly ISubscriptionDiagnosticEvents _diagnosticEvents; private bool _disposed; @@ -119,7 +119,7 @@ await CreateTopicAsync( return sourceStream; static async ValueTask?> TryCreateSourceStream( - IDisposable topic, + ITopic topic, CancellationToken cancellationToken) { if (topic is DefaultTopic et) @@ -129,7 +129,7 @@ await CreateTopicAsync( // we found a topic with the same name but a different message type. // this is an invalid state and we will except. - throw new InvalidMessageTypeException(); + throw new InvalidMessageTypeException(topic.MessageType, typeof(TMessage)); } } @@ -195,19 +195,33 @@ protected abstract DefaultTopic OnCreateTopic( protected virtual string FormatTopicName(string topic) => _topicFormatter.Format(topic); - protected bool TryGetTopic( + protected bool TryGetTopic( string formattedTopic, - [NotNullWhen(true)] out TTopic? topic) + [NotNullWhen(true)] out DefaultTopic? topic) { if (_topics.TryGetValue(formattedTopic, out var value)) { - if (value is TTopic casted) + if (value is DefaultTopic casted) { topic = casted; return true; } - throw new InvalidMessageTypeException(); + throw new InvalidMessageTypeException(value.MessageType, typeof(TMessage)); + } + + topic = default; + return false; + } + + protected bool TryGetTopic( + string formattedTopic, + [NotNullWhen(true)] out ITopic? topic) + { + if (_topics.TryGetValue(formattedTopic, out var value)) + { + topic = value; + return true; } topic = default; diff --git a/src/HotChocolate/Core/src/Subscriptions/DefaultTopic.cs b/src/HotChocolate/Core/src/Subscriptions/DefaultTopic.cs index 8b2cdc215d7..9de722e9f23 100644 --- a/src/HotChocolate/Core/src/Subscriptions/DefaultTopic.cs +++ b/src/HotChocolate/Core/src/Subscriptions/DefaultTopic.cs @@ -67,10 +67,15 @@ protected DefaultTopic( } /// - /// The name of this topic. + /// Gets the name of this topic. /// public string Name { get; } + /// + /// Gets the message type of this topic. + /// + public Type MessageType => typeof(TMessage); + /// /// Allows access to the diagnostic events. /// @@ -461,8 +466,3 @@ public void Dispose() { } public static readonly DefaultSession Instance = new(); } } - -public interface ITopic -{ - void TryComplete(); -} diff --git a/src/HotChocolate/Core/src/Subscriptions/ITopic.cs b/src/HotChocolate/Core/src/Subscriptions/ITopic.cs new file mode 100644 index 00000000000..c3fc868099b --- /dev/null +++ b/src/HotChocolate/Core/src/Subscriptions/ITopic.cs @@ -0,0 +1,17 @@ +namespace HotChocolate.Subscriptions; + +/// +/// Represents a topic. +/// +public interface ITopic : IDisposable +{ + /// + /// Gets the message type of this topic. + /// + public Type MessageType { get; } + + /// + /// Allows to complete a topic. + /// + void TryComplete(); +} diff --git a/src/HotChocolate/Core/src/Subscriptions/InvalidMessageTypeException.cs b/src/HotChocolate/Core/src/Subscriptions/InvalidMessageTypeException.cs index 2084307b207..fc676c957b3 100644 --- a/src/HotChocolate/Core/src/Subscriptions/InvalidMessageTypeException.cs +++ b/src/HotChocolate/Core/src/Subscriptions/InvalidMessageTypeException.cs @@ -1,4 +1,4 @@ -using System.Runtime.Serialization; +using HotChocolate.Subscriptions.Properties; namespace HotChocolate.Subscriptions; @@ -9,16 +9,37 @@ namespace HotChocolate.Subscriptions; [Serializable] public class InvalidMessageTypeException : Exception { - public InvalidMessageTypeException() { } + /// + /// Initializes a new instance of the class. + /// + /// + /// The topic message type. + /// + /// + /// The requested message type. + /// + public InvalidMessageTypeException(Type topicMessageType, Type requestedMessageType) + : base(CreateMessage(topicMessageType, requestedMessageType)) + { + TopicMessageType = topicMessageType; + RequestedMessageType = requestedMessageType; + } - public InvalidMessageTypeException(string message) - : base(message) { } + /// + /// Gets the topic message type. + /// + public Type TopicMessageType { get; } - public InvalidMessageTypeException(string message, Exception inner) - : base(message, inner) { } + /// + /// Gets the requested message type. + /// + public Type RequestedMessageType { get; } - protected InvalidMessageTypeException( - SerializationInfo info, - StreamingContext context) - : base(info, context) { } + private static string CreateMessage( + Type topicMessageType, + Type requestedMessageType) + => string.Format( + Resources.InvalidMessageTypeException_Message, + topicMessageType.FullName, + requestedMessageType.FullName); } diff --git a/src/HotChocolate/Core/src/Subscriptions/Properties/Resources.Designer.cs b/src/HotChocolate/Core/src/Subscriptions/Properties/Resources.Designer.cs index 65f1ab80471..df04c56a197 100644 --- a/src/HotChocolate/Core/src/Subscriptions/Properties/Resources.Designer.cs +++ b/src/HotChocolate/Core/src/Subscriptions/Properties/Resources.Designer.cs @@ -74,5 +74,11 @@ internal static string MessageEnvelope_UnsubscribeAndComplete_DoNotHaveBody { return ResourceManager.GetString("MessageEnvelope_UnsubscribeAndComplete_DoNotHaveBody", resourceCulture); } } + + internal static string InvalidMessageTypeException_Message { + get { + return ResourceManager.GetString("InvalidMessageTypeException_Message", resourceCulture); + } + } } } diff --git a/src/HotChocolate/Core/src/Subscriptions/Properties/Resources.resx b/src/HotChocolate/Core/src/Subscriptions/Properties/Resources.resx index b75f63bbb34..6b6f65c302c 100644 --- a/src/HotChocolate/Core/src/Subscriptions/Properties/Resources.resx +++ b/src/HotChocolate/Core/src/Subscriptions/Properties/Resources.resx @@ -113,4 +113,7 @@ Complete and Unsubscribe messages do not have a body. + + The topic already exists with a different message type. Topic message type: {0}. Requested message type: {1}. + diff --git a/src/HotChocolate/Core/test/Subscriptions.InMemory.Tests/InMemoryIntegrationTests.cs b/src/HotChocolate/Core/test/Subscriptions.InMemory.Tests/InMemoryIntegrationTests.cs index 9c65784e245..1c0ef96cf54 100644 --- a/src/HotChocolate/Core/test/Subscriptions.InMemory.Tests/InMemoryIntegrationTests.cs +++ b/src/HotChocolate/Core/test/Subscriptions.InMemory.Tests/InMemoryIntegrationTests.cs @@ -1,4 +1,6 @@ -using System.Threading.Tasks; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate.Execution; using Microsoft.Extensions.DependencyInjection; using HotChocolate.Execution.Configuration; using Xunit.Abstractions; @@ -36,6 +38,32 @@ public override Task Subscribe_Topic_With_Arguments_2_Topics() public override Task Subscribe_Topic_With_2_Arguments() => base.Subscribe_Topic_With_2_Arguments(); + [Fact] + public virtual async Task Invalid_Message_Type() + { + // arrange + using var cts = new CancellationTokenSource(5000); + await using var services = CreateServer(); + var sender = services.GetRequiredService(); + + var result = await services.ExecuteRequestAsync( + "subscription { onMessage2(arg1: \"a\", arg2: \"b\") }", + cancellationToken: cts.Token) + .ConfigureAwait(false); + + // we need to execute the read for the subscription to start receiving. + await using var responseStream = result.ExpectResponseStream(); + var results = responseStream.ReadResultsAsync().ConfigureAwait(false); + + // act + async Task Send() => await sender.SendAsync("OnMessage2_a_b", 1, cts.Token).ConfigureAwait(false); + + // assert + var exception = await Assert.ThrowsAsync(Send); + Assert.Equal(typeof(string), exception.TopicMessageType); + Assert.Equal(typeof(int), exception.RequestedMessageType); + } + protected override void ConfigurePubSub(IRequestExecutorBuilder graphqlBuilder) => graphqlBuilder.AddInMemorySubscriptions(); }