NServiceBus.RavenDB: Delay Init Until Endpoint Startup?

Hi,

I’m using the StartableEndpointWithExternallyManagedContainer with RavenDB as my persistence store in a .Net Core 2.2. application that uses the built in IoC container. I’m using the PersistenceExtensions<RavenDBPersistence>.SetDefaultDocumentStore overload that takes in a Func<ReadOnlySettings, IDocumentStore>.

In my integration tests I’m then attempting to use the ConfigureTestServices method to override my IDocumentStore implementation so that I can test with the in memory RavenDB TestDriver, but I’m hitting an issue whereby the IDocumentStore is being resolved at endpoint initialization (ConfigureServices) and not at endpoint startup. This means that my real/original RavenDB IDocumentStore implementation is used to communicate with, and configure, the data store since ConfigureTestServices isn’t called until after ConfigureServices has completed.

I’d hoped by using the Func, rather than directly injecting the IDocumentStore, it would allow resolution to be deferred until startup, but it doesn’t appear to be the case. I was just wondering if there is a way that the RavenDbOutboxStorage feature can delay doing any work until the endpoint is actually started, and not at registration/configuration time?

Thanks

Hi Ross!

I looked at the code and I can confirm that the RavenDB persister is calling the Func provided at during configuration and there doesn’t seem to be a workaround.

How do you create the document store?

I want to understand more about your use case, would you be able to make the code available online?

Cheers,

Andreas

Hi Andreas - thanks for the reply.

I’ve tried to summarise our methodology below, but the heart of it is at the bottom where we try to defer building the service provider in order to resolve the IDocumentStore (via the IDocumentStoreContainer). Because the RavenDB feature is resolving the IDocumentStore immediately it means that the service provider is built before the test double replaces the real implementation via the ConfigureTestServices call:

public class DocumentStoreContainer : IDocumentStoreContainer
{
    public DocumentStoreContainer(IOptions<Options.RavenDb> options)
    {
        if (options == null)
        {
            throw new ArgumentNullException(nameof(options));
        }

        options.Value.Guard();

        X509Certificate2 certificate = null;
        if (!string.IsNullOrWhiteSpace(options.Value.CertificateAsBase64String))
        {
            certificate = new X509Certificate2(Convert.FromBase64String(options.Value.CertificateAsBase64String), options.Value.CertificatePassword, X509KeyStorageFlags.MachineKeySet);
        }

        DocumentStore = new DocumentStore
                        {
                                Urls = options.Value.Urls.ToArray(),
                                Database = options.Value.DatabaseName,
                                Certificate = certificate
                        }.Initialize();
    }

    public IDocumentStore DocumentStore { get; }
}

internal class TestDocumentStoreContainer : IDocumentStoreContainer
{
    public TestDocumentStoreContainer()
    {
        DriverWrapper = new RavenTestDriverWrapper();
    }

    public IDocumentStore DocumentStore => DriverWrapper.GetDocumentStore();
	
    private RavenTestDriverWrapper DriverWrapper { get; }
}

internal class RavenTestDriverWrapper : RavenTestDriver
{
    protected override void PreInitialize(IDocumentStore documentStore)
    {
        documentStore.Conventions.MaxNumberOfRequestsPerSession = 50;
    }

    public IDocumentStore GetDocumentStore()
    {
        return base.GetDocumentStore();
    }
}

public class NServiceBusService : IHostedService
{
    public NServiceBusService(IStartableEndpointWithExternallyManagedContainer startableEndpoint, IServiceProvider serviceProvider)
    {
        StartableEndpoint = startableEndpoint ?? throw new ArgumentNullException(nameof(startableEndpoint));
        ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
    }

    private IEndpointInstance EndpointInstance { get; set; }

    private IServiceProvider ServiceProvider { get; }

    private IStartableEndpointWithExternallyManagedContainer StartableEndpoint { get; }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        if (EndpointInstance != null)
        {
            return;
        }

        EndpointInstance = await StartableEndpoint.Start(new ServiceProviderAdapter(ServiceProvider));
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        if (EndpointInstance == null)
        {
            return;
        }

        await EndpointInstance.Stop();
        EndpointInstance = null;
    }
}

public class Startup
{
	public void ConfigureServices(IServiceCollection services)
	{
		var endpointConfiguration = ... // Configure endpoint

		var persistence = endpointConfiguration.UsePersistence<RavenDBPersistence>();
		                                       .SetDefaultDocumentStore(_ => services.BuildServiceProvider().GetRequiredService<IDocumentStoreContainer>().DocumentStore);

		var startableEndpoint = EndpointWithExternallyManagedContainer.Create(endpointConfiguration, new ServiceCollectionAdapter(services));
		services.AddSingleton<IHostedService>(serviceProvider => new NServiceBusService(startableEndpoint, serviceProvider));
		services.AddSingleton(_ => startableEndpoint.MessageSession);
	}
}

// In integration tests we replace the IDocumentStoreContainer with the TestDriver container like so:
return WebHost.CreateDefaultBuilder().ConfigureTestServices(services =>services.AddSingleton<IDocumentStoreContainer, TestDocumentStoreContainer>());

Hopefully that helps shed some light on our approach, but if not I can create am example project and upload it to a git repo.

Thanks again,

Ross

Thanks for the details. I have created a repro

and raised an issue to further triage this.

Feel free to subscribe to the issue to join the discussion. We will still post status updates here as well.

Thanks for reporting this.

Cheers,

Andreas

We’ve triage this as a bug and will address it in our next maintenance release.

We’ve also raised Allow users to provide a IDocumentStore resolved from an externally managed container · Issue #445 · Particular/NServiceBus.RavenDB · GitHub to further improve the way we resolve the document store to avoid relying on the _ => services.BuildServiceProvider() workaround as well.

I will let you know once I have a more firm timeline for the fixes.

Cheers,

Andreas

That’s great Andreas, thanks.

Hey @Ross,

Just a few questions to get a better understanding of what you’re trying to achieve and the code snippets you provided.

You mentioned that Startup.ConfigureServices is called before ConfigureTestServices has been able to replace the test implementation with the real one. In the last line of your snippets it looks like ConfigureTestServices is called as the first thing before returning the IWebHostBuilder. It’s not clear at which point you’d register DocumentStoreContainer but if it happens later, the production configuration will basically overwrite your test configuration as with the default DI container, the last registration for IDocumentStoreContainer will win.

Based on this, even if NServiceBus would build the document store at Endpoint start time, it would still resolve the production registration for IDocumentStoreContainer in case this has been registered first. How do you deal with this ordering issue in your code?

We just released NServiceBus.RavenDB 6.1 which provides a new overload when configuring the document store with access to the IBuilder. This should let you allow resolving the document store from the container and the this factory is only invoked at the actual endpoint startup time. You can read more about this API on the docs here: Connection Options • RavenDB Persistence • Particular Docs.

Please let us know whether that helps.