NServiceBus icon indicating copy to clipboard operation
NServiceBus copied to clipboard

Altering NSB components within a Feature not resolving proper dependency

Open seankearney opened this issue 3 years ago • 7 comments

While trying to work around #6058 we created a Feature to replace the DefaultDataBusSerializer.

internal class BsonDataBusSerializerFeature : Feature
{
    public BsonDataBusSerializerFeature()
    {
        DependsOn<NServiceBus.Features.DataBus>();
        EnableByDefault();
    }

    protected override void Setup(FeatureConfigurationContext context)
    {
        context.Container.ConfigureComponent<IDataBusSerializer>(_ => new BsonDataBusSerializer(), DependencyLifecycle.SingleInstance);
    }
}

We have added a reference to the project containing this feature to three different types of projects.

  1. .NET Core Console App
  2. Azure Function App
  3. ASP.NET Core Web API

Using #1 we can successfully publish our message. #2 will successfully handle the message.

For #3, the Feature is executing Setup when the application starts up, however trying to send a message with a DataBusProperty fails with "BinaryFormatter serialization and deserialization are disabled within this application...".

If we explicitly register the custom serializer like this in Program.cs

var endpointConfiguration = new EndpointConfiguration("API");
endpointConfiguration.RegisterComponents(configur => configur.RegisterSingleton<IDataBusSerializer>(new BsonDataBusSerializer()));

Our web app project will publish the message successfully.

This feels like an issue with NSB with Features and dependency resolution.

seankearney avatar Apr 29 '22 21:04 seankearney

@seankearney How are you integrating NServiceBus with the ASP.NET Core Web API host? Are they sharing a container?

Using DependsOn should guarantee that the original DataBus feature Setup is run first, which will register the default serializer in the container. Then the BsonDataBusSerializerFeature.Setup is run which adds a second implementation. It looks like the container is getting the first registration instead of the last.

I'm guessing that scenarios (1) and (2) are using the NServiceBus internal container which is returning the latest registration.

Part of the reason that explicitly registering the serializer works is because the DataBus feature explicitly checks for an existing registration and only adds the default one if there isn't a custom one registered.

mikeminutillo avatar May 02 '22 02:05 mikeminutillo

@mikeminutillo so despite overriding the component registration from a custom feature by calling

context.Container.ConfigureComponent<IDataBusSerializer>(_ => 
  new BsonDataBusSerializer(), DependencyLifecycle.SingleInstance);

The DefaultDataBusSerializer is not replaced. Is there a way to remove the registered default implementation prior to adding a new one from within the Feature's Setup?

SeanFeldman avatar May 04 '22 06:05 SeanFeldman

Our container abstraction doesn't expose a way to remove a registration but the underlying container might. ConfigureComponent is just an abstraction over whatever container is being used under the covers. What it does with multiple registrations is not under the control of NServiceBus. That's why I was asking about how the endpoint is integrated into the framework and which container is being used.

mikeminutillo avatar May 04 '22 06:05 mikeminutillo

So in the case of Azure Functions, it's a bit more complicated than just IServiceCollection. NSB adapts a type that is already an adapter for Functions. I don't see any other way of accessing the underlying service collection other than going into nasty reflection to access the member field NSB holds on to.

SeanFeldman avatar May 04 '22 06:05 SeanFeldman

Azure Functions appears to use a last-registration-in-wins strategy so it works. It's only scenario 3 that is having an issue and that is ASP.NET Core Web API. I would have thought that would be using the default Microsoft container as well. Until I know more about that third (non-working scenario), I can't tell why it might not be working.

mikeminutillo avatar May 04 '22 07:05 mikeminutillo

Ironically, it appears that Functions also has an issue with this. The only way to address it is to remove the default IDataBusSerializer found in the IServiceCollection passed into the adapter, ServiceCollectionAdapter. As ServiceCollectionAdapter doesn't expose any way to remove registrations, reflection is required.

In the future, it would be nice to expose IServiceCollection or remove the adapter altogether.

SeanFeldman avatar May 06 '22 16:05 SeanFeldman

@SeanFeldman another approach could be to use INeedInitialization to hook into the endpoint creation process. This would allow you to register the bson databus serializer before the databus feature is set up. Something like this:

public class ReplaceDefaultDataBusSerializer : INeedInitialization
{
  public void Customize(EndpointConfiguration endpointConfiguration)
  {
    endpointConfiguration.RegisterComponents(components =>
      components.RegisterSingleton<IDataBusSerializer>(new BsonDataBusSerializer()));
  }
}

This class should get picked up by the type scanner and applied very early in the endpoint creation pipeline.

mikeminutillo avatar May 18 '22 06:05 mikeminutillo