Skip to content

Undocumented incompatibility of Unobtrusive Mode and Assembly Scanning features #7875

Description

@JarkoDubbeldam

Describe the bug

Description

We're building a Feature that relies on the results of assembly scanning to set up the TopicTopology for the Azure Service Bus transport. However, we are running into an issue where our messagebus contracts assemblies are not being scanned properly. This results in exceptions being thrown down the line due to ThrowIfUnmappedEventTypes = true.

Our contracts libs use custom defined unobtrusive mode marker interfaces, with a convention configured in a separate assembly. As a result, our contracts don't have any reference to NServiceBus.Core or NServiceBus.MessageInterfaces.

As an aside, our unobtrusive mode interfaces narrowly predate the NServiceBus.MessageInterfaces package, but the biggest reason we haven't switched to that package (yet) is the fact that TimeToBeReceivedAttribute is still defined in NServiceBus. We pulled its functionality to our unobtrusive mode package.

Our EndpointConfiguration then sets up a feature, which resolves MessageMetadataRegistry from the settings and loops over GetAllMessages().

Expected behavior

The contracts libs are scanned and the contained messages are registered in MessageMetadataRegistry.

OR

The assembly is listed in .diagnostics/<endpointname>-configuration.txt output with the reason why it was skipped (does not reference NServiceBus.Core or NServiceBus.MessageInterfaces.

Actual behavior

The messages are not in the registry. The containing assembly is not mentioned in the diagnostics log.

Versions

  • NServiceBus 10.2.7

Steps to reproduce

  • Project `MessageAbstractions:

MessageAbstractions.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

IEvent.cs

namespace MessageAbstractions;

public interface IEvent;
  • Project MessageContracts:

MessageContracts.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\MessageAbstractions\MessageAbstractions.csproj" />
  </ItemGroup>

</Project>

MyEvent.cs

using MessageAbstractions;

namespace MessageContracts;

public class MyEvent : IEvent;
  • Project AssemblyScanningRepro:

AssemblyScanningRepro.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.10" />
    <PackageReference Include="NServiceBus" Version="10.2.7" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\MessageAbstractions\MessageAbstractions.csproj" />
    <ProjectReference Include="..\MessageContracts\MessageContracts.csproj" />
  </ItemGroup>

</Project>

Program.cs

using AssemblyScanningRepro;
using Microsoft.Extensions.Hosting;

var builder = Host.CreateApplicationBuilder(args);

var endpointConfiguration = new EndpointConfiguration("AssemblyScanningRepro");
endpointConfiguration.Conventions().DefiningEventsAs(type => typeof(MessageAbstractions.IEvent).IsAssignableTo(type));
endpointConfiguration.UseTransport<LearningTransport>();
endpointConfiguration.UseSerialization<SystemJsonSerializer>();
endpointConfiguration.EnableFeature<ConventionsReadingFeature>();

builder.Services.AddNServiceBusEndpoint(endpointConfiguration);


var host = builder.Build();

await host.RunAsync();

ConventionsReadingFeature.cs

using NServiceBus.Features;
using NServiceBus.Unicast.Messages;

namespace AssemblyScanningRepro;

internal class ConventionsReadingFeature : Feature {
  protected override void Setup(FeatureConfigurationContext context) {
    var registry = context.Settings.Get<MessageMetadataRegistry>();
    var messages = registry.GetAllMessages();

    if (messages.Length == 0) {
      Console.WriteLine("MyEvent was not found!");
    }
  }
}

Relevant log output

Additional Information

Workarounds

A quick-fix workaround for us right now is to manually enumerate the types into the MessageMetadataRegistry:

  var registry = endpointConfiguration.GetSettings().GetOrCreate<MessageMetadataRegistry>();
  registry.RegisterMessageTypes([typeof(Message1), typeof(Message2), ...]);

We could also move our contracts to NServiceBus.MessageInterfaces, but that would still leave the TimeToBeReceivedAttribute wrinkle.

Possible solutions

I'm not quite sure whether this is a bug/oversight or intended behaviour. But in the latter case, I would like to suggest a few improvements in visibility of this behaviour:

  1. Neither the docs on Unobtrusive Mode nor the docs on Assembly Scanning, mention this incompatibility. Especially the latter might benefit with a mention that only assemblies directly or indirectly referencing NServiceBus.Core or NServiceBus.MessageInterfaces are actually scanned.
  2. There is currently no diagnostic output on assemblies where bool ScanAssembly(Assembly assembly, Dictionary<AssemblyIdentity, bool> processed) returns false. Scanned assemblies are mentioned, excluded Particular or .Net runtime assemblies are mentioned, but the remainder are excluded.

As a potential fix for our problem:

  1. The AssemblyScanner configuration currently already allows us to exclude certain assemblies from being scanned. Would it make sense to add a configuration for including assemblies into the dictionary that seeds processedAssemblies with true values?. Then we could bundle that into the extension methods setting up the conventions related to our contracts interfaces.

Additional information

...

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions