Multiple function endpoints

Hi, I’m trying to use an instance of a function endpoint in 2 different classes, however within the scope of one azure function - it seems it’s not possible. I’m getting The transport is already started exception.

Here’s the code. The ProductFunction class contains Run method (azure function) that sends a command. This works. If I add another dependency IContextPublisher using IFunctionEndpoint and call Publish method on it that publishes an event, the event is sent but then the Send
method in the Run method fails with The transport is already started exception.

public class ProductFunction
{
    private readonly IFunctionEndpoint functionEndpoint;
    private readonly IContextPublisher contextPublisher;

    public OnboardUserFunction(
        IFunctionEndpoint functionEndpoint,
        IContextPublisher contextPublisher)
    {
        this.functionEndpoint = functionEndpoint;
        this.contextPublisher = contextPublisher;
    }

    [FunctionName("AddProduct")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest request,
        ExecutionContext executionContext,
        ILogger logger)
    {
      this.contextPublisher.Publish(); // works (it uses the first IFunctionEndpoint instance)
      await this.functionEndpoint.Send(commandMessage, executionContext, logger); // throws the exception (it uses the second IFunctionEndpoint instance)
    }
}

public class ContextPublisher : IContextPublisher
{
    private readonly IFunctionEndpoint functionEndpoint;

    public ContextPublisher(IFunctionEndpoint functionEndpoint)
    {
        this.functionEndpoint = functionEndpoint;
    }

    public async Task Publish() {
      await this.functionEndpoint.Publish(eventMessage, null); // executionContext is null, but it doesn't work with the valid context either - is it required? 
    }
}

I found out that it’s because UseNServiceBus method registers IFunctionEndpoint as factory method:

// for backward compatibility
functionsHostBuilder.Services.AddSingleton(endpointFactory);
functionsHostBuilder.Services.AddSingleton<IFunctionEndpoint>(sp => sp.GetRequiredService<FunctionEndpoint>());

That means every class that depends on IFunctionEndpoint gets a new instance of the function endpoint. The problem is that only the first instance works.

It creates the pipeline if the pipeline is null. In my case 2 function endpoints instances are created. Both have null pipeline at the beginning. When the first endpoint publishes the event, the pipeline is created and the event sent. When the second endpoint sends the command, the pipeline is also created but perhaps it shares the transport receiver with the first one and so it fails with The transport is already started. The TransportReceiver:

    public Task Start()
    {
        if (isStarted)
        {
            throw new InvalidOperationException("The transport is already started");
        }
    ...

Why the factory is used for the function endpoints and there isn’t only one registered? I would expect one per azure function (registered as scope). Now it behaves as a transient dependency.

If the IFunctionEndpoint instances are different, why it then shares the transport receiver?

How I can inject the same instance of IFunctionEndpoint to classes so I can use more classes sending/publishing messages within a scope of one Azure Function?

Hi @pinggi

Are you using the Inprocess version with the v4 host? Azure Functions v4 Host has a bug that causes singleton factory registrations to be resolved by the child scope and not by the root scope as they should be.

We are working on a fix, https://github.com/Particular/NServiceBus.AzureFunctions.InProcess.ServiceBus/pull/361 for the host at the moment. It might take a while though until it is ready and fully releasable.

Regards,
Daniel

I use Inprocess version and Azure Functions v3 host.

    <TargetFramework>netcoreapp3.1</TargetFramework>
    <AzureFunctionsVersion>v3</AzureFunctionsVersion>

They mention in related information: Works as expected in Host V3 so this is a regression bug.

However I have different instances injected to IFunctionEndpoint ctor params there.
It doesn’t work in v3 either, or it’s a different issue.

@pinggi I have created a similar sample to your code available at

I was unable to reproduce the “endpoint was already started” problem you are facing. I was able though to reproduce again the issue of “object disposed” that is exhibited when using the v4 runtime. Can you share how you register your context publisher? Can you maybe point out on the PR above how your code is structured so that we can reproduce it?

Thanks for sharing your insights
daniel

I tested the linked sample and it worked. I returned to my code then to check the registration and found the issue. I use third party sw that requires the publisher instance to be configured. In fact a factory method returning the instance.

It looks basically like this in the Configure(IFunctionsHostBuilder builder) method after UseNServiceBus:

var serviceProvider = builder.Services.BuildServiceProvider();
Setup3rdParty()
    .Use(() => serviceProvider.GetRequiredService<IContextPublisher>());

When the factory registered in the 3rd party component is called, the GetRequiredService returns the context publisher with IFunctionEndpoint instance different from the IFunctionEndpoint instance injected to the Azure Function class.

Simply, if you get an instance of a class that depends on IFunctionEndpoint by service provider and then publish a message on the endpoint, it will work. However another class dependent on the IFunctionEndpoint will get different endpoint instance and publishing on it fails with the mentioned error: The transport is already started exception.

You can reproduce it by adding:

var serviceProvider = builder.Services.BuildServiceProvider();
var contextPublisher = serviceProvider.GetRequiredService<IContextPublisher>();
contextPublisher.Do(null, null);

to the Configure(IFunctionsHostBuilder builder) method in the Startup.cs file of the sample you linked. Just remove the logging since I show calling Do with null params because the execution context and the logger are not accessible there.

I hope I don’t do anything wrong with getting the instance by the service provider.
Btw: can the execution context passed to the Publish method be null?