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 https://github.com/Particular/NServiceBus.RavenDB/issues/445 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.