-
Notifications
You must be signed in to change notification settings - Fork 651
Description
Describe the bug
Description
AssemblyScanner only adds types from assemblies that reference NServiceBus.Core either explicitly or via other referenced assemblies:
NServiceBus/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanner.cs
Lines 222 to 239 in f66e5f7
| if (assembly.GetName().Name == CoreAssemblyName) | |
| { | |
| return processed[assembly.FullName] = true; | |
| } | |
| if (ShouldScanDependencies(assembly)) | |
| { | |
| foreach (var referencedAssemblyName in assembly.GetReferencedAssemblies()) | |
| { | |
| var referencedAssembly = GetReferencedAssembly(referencedAssemblyName); | |
| var referencesCore = ScanAssembly(referencedAssembly, processed); | |
| if (referencesCore) | |
| { | |
| processed[assembly.FullName] = true; | |
| break; | |
| } | |
| } | |
| } |
This means, as far as I can tell, that an assembly containing message contracts but using unobtrusive mode (and therefore not referencing NServiceBus.Core) will not be scanned and types from it will never be added to the MessageMetadaRegistry even if the custom IMessageConvention defines those types as message. The only types from these assemblies that will be added are those that are handled by an IHandleMessages<> (which automatically belongs to a scanned assembly by virtue of referencing NServiceBus.Core in order to implement IHandleMessages<>).
In the case that the handled message contracts from the unobtrusive assembly are simple interfaces, this is perhaps fine because concrete types can be created dynamically in MessageMapper.cs, but any interface that contains--as a property--another interface will not be deserializable.
This is true even when the NServiceBus.EnclosedMessageTypes explicitly defines the concrete/implementation type and when that concrete type lives in the same assembly as the interface:
Expected behavior
There needs to be some way for a user to ensure that unobtrusive assemblies are scanned for concrete types, especially if the concrete types are in the same assembly as the handled type.
Actual behavior
AssemblyScanner does not scan the unobtrusive assembly and does not find these types, and therefore it is impossible to send messages that are handled by a complex interface contract type.
Versions
8.1.6
SystemJsonSerializer for sure, although I suspect it would also apply to NewtonsoftJsonSerializer.
Steps to reproduce
- Create an unobtrusive contracts assembly
- Add a complex interface contract type, such as:
public interface IComplexContract
{
IComplexChild Child { get; }
}
public interface IComplexChild
{
bool IsComplex { get; }
}
- Add concrete implementations for your contract:
public class ConcreteContract : IComplexContract
{
public ConcreteChild Child { get; set; }
public IComplexContract.Child => Child;
}
public class ConcreteChild : IComplexChild
{
public bool IsComplex => true;
}
- Add a handler for IConcreteContract:
public class ComplexContractHandler : IHandleMessages<IComplexContract>
{
public Task Handle(IComplexContract message)
{
// unreachable
return Task.CompletedTask;
}
}
- Send an IComplexContract message. Even if
ConcreteContractis in theNServiceBus.EnclosedMessageTypes, theAssemblyScannerwill not scan the unobtrusive assembly and will therefore never registerConcreteChild. While a dynamic type will be created to deserialize theIComplexContract, the same will not be down forIComplexChildandMessageDeserializationExceptionwill be thrown saying it is unable to deserialize$.Child.
Relevant log output
NServiceBus.MessageDeserializationException: An error occurred while attempting to extract logical messages from incoming physical message {GUID}
---> System.NotSupportedException: Deserialization of interface types is not supported. Type 'IComplexChild'. Path: $.Child | LineNumber: 0 | BytePositionInLine: ##.
---> System.NotSupportedException: Deserialization of interface types is not supported. Type 'IComplexChild'.
--- End of inner exception stack trace ---
at System.Text.Json.ThrowHelper.ThrowNotSupportedException(ReadStack& state, Utf8JsonReader& reader, NotSupportedException ex)
at System.Text.Json.ThrowHelper.ThrowNotSupportedException_DeserializeNoConstructor(Type type, Utf8JsonReader& reader, ReadStack& state)
at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
at System.Text.Json.Serialization.Metadata.JsonPropertyInfo`1.ReadJsonAndSetMember(Object obj, ReadStack& state, Utf8JsonReader& reader)
at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
at System.Text.Json.Serialization.JsonConverter`1.ReadCoreAsObject(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
at System.Text.Json.JsonSerializer.ReadCore[TValue](Utf8JsonReader& reader, JsonTypeInfo jsonTypeInfo, ReadStack& state)
at System.Text.Json.JsonSerializer.ContinueDeserialize[TValue](ReadBufferState& bufferState, JsonReaderState& jsonReaderState, ReadStack& readStack, JsonTypeInfo jsonTypeInfo)
at System.Text.Json.JsonSerializer.ReadFromStream[TValue](Stream utf8Json, JsonTypeInfo jsonTypeInfo)
at System.Text.Json.JsonSerializer.Deserialize(Stream utf8Json, Type returnType, JsonSerializerOptions options)
at NServiceBus.Serializers.SystemJson.JsonMessageSerializer.Deserialize(ReadOnlyMemory`1 body, Type type) in /_/src/NServiceBus.Core/Serializers/SystemJson/JsonMessageSerializer.cs:line 53
at NServiceBus.Serializers.SystemJson.JsonMessageSerializer.Deserialize(ReadOnlyMemory`1 body, IList`1 messageTypes) in /_/src/NServiceBus.Core/Serializers/SystemJson/JsonMessageSerializer.cs:line 41
at NServiceBus.DeserializeMessageConnector.Extract(IncomingMessage physicalMessage) in /_/src/NServiceBus.Core/Pipeline/Incoming/DeserializeMessageConnector.cs:line 112
at NServiceBus.DeserializeMessageConnector.ExtractWithExceptionHandling(IncomingMessage message) in /_/src/NServiceBus.Core/Pipeline/Incoming/DeserializeMessageConnector.cs:line 46
--- End of inner exception stack trace ---
at NServiceBus.DeserializeMessageConnector.ExtractWithExceptionHandling(IncomingMessage message) in /_/src/NServiceBus.Core/Pipeline/Incoming/DeserializeMessageConnector.cs:line 50
at NServiceBus.DeserializeMessageConnector.Invoke(IIncomingPhysicalMessageContext context, Func`2 stage) in /_/src/NServiceBus.Core/Pipeline/Incoming/DeserializeMessageConnector.cs:line 28
at NServiceBus.ProcessingStatisticsBehavior.Invoke(IIncomingPhysicalMessageContext context, Func`2 next) in /_/src/NServiceBus.Core/Performance/Statistics/ProcessingStatisticsBehavior.cs:line 25
at NServiceBus.ReceiveDiagnosticsBehavior.Invoke(IIncomingPhysicalMessageContext context, Func`2 next) in /_/src/NServiceBus.Core/OpenTelemetry/Metrics/ReceiveDiagnosticsBehavior.cs:line 33
at NServiceBus.TransportReceiveToPhysicalMessageConnector.Invoke(ITransportReceiveContext context, Func`2 next) in /_/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs:line 35
at NServiceBus.RetryAcknowledgementBehavior.Invoke(ITransportReceiveContext context, Func`2 next) in /_/src/NServiceBus.Core/ServicePlatform/Retries/RetryAcknowledgementBehavior.cs:line 25
at NServiceBus.MainPipelineExecutor.Invoke(MessageContext messageContext, CancellationToken cancellationToken) in /_/src/NServiceBus.Core/Pipeline/MainPipelineExecutor.cs:line 49
at NServiceBus.MainPipelineExecutor.Invoke(MessageContext messageContext, CancellationToken cancellationToken) in /_/src/NServiceBus.Core/Pipeline/MainPipelineExecutor.cs:line 68
at NServiceBus.Transport.AzureServiceBus.MessagePump.ProcessMessage(ServiceBusReceivedMessage message, ProcessMessageEventArgs processMessageEventArgs, String messageId, Dictionary`2 headers, BinaryData body, CancellationToken messageProcessingCancellationToken) in /_/src/Transport/Receiving/MessagePump.cs:line 285Additional Information
Workarounds
Perhaps adding a concrete type for the complex interface to an assembly that references NServiceBus.Core to ensure it available to the DeserializeMessageConnector ?
Possible solutions
My janky solution just to prove the point was to add a delegate predicate to AssemblyScannerConfiguration to tell it the assembly was an unobtrusive package that should for sure be scanned:
...
/// <summary>
/// Defines an additional path for assembly scanning.
/// </summary>
public string? AdditionalAssemblyScanningPath { get; set; }
/// <summary>
/// Register assembly types based on a predicate.
/// </summary>
public Func<string, bool>? AssemblyContainsMessages { get; set; }
/// <summary>
/// A list of <see cref="Assembly" />s to ignore in the assembly scanning.
/// </summary>
/// <param name="assemblies">The file name of the assembly.</param>
public void ExcludeAssemblies(params string[] assemblies)
{
...
And then passing this delegate through to the AssemblyScanner.ScanAssembly():
...
processed[assembly.FullName] = false;
var assemblyName = assembly.GetName().Name;
if (assemblyName == CoreAssemblyName
|| AssemblyContainsMessages?.Invoke(assemblyName) == true)
{
return processed[assembly.FullName] = true;
}
if (ShouldScanDependencies(assembly))
{
...
A nicer solution that is essentially the same would be to add something in the same vein as IMessageConvention rather than a raw delegate.
But the better solution, IMHO, would be to automatically scan assemblies that contain any types defined by the IMessageConvention to be messages. A naive solution to this would add a lot of overhead in scanning all the types from all those assemblies, but there are already some inefficiencies there due to loading every referenced assembly just to tell whether it's referencing NServiceBus.Core
NServiceBus/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanner.cs
Lines 227 to 239 in f66e5f7
| if (ShouldScanDependencies(assembly)) | |
| { | |
| foreach (var referencedAssemblyName in assembly.GetReferencedAssemblies()) | |
| { | |
| var referencedAssembly = GetReferencedAssembly(referencedAssemblyName); | |
| var referencesCore = ScanAssembly(referencedAssembly, processed); | |
| if (referencesCore) | |
| { | |
| processed[assembly.FullName] = true; | |
| break; | |
| } | |
| } | |
| } |
Either that, or expose the AssemblyScanningComponent.Configuration.UserProvidedTypes outside of acceptance tests and let the user do the assembly scanning themselves.